From add7f5e515d2d6ba86cb3bffb530c594800699b4 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Wed, 23 Oct 2024 15:33:41 -0500 Subject: [PATCH 1/5] Ported notifications component from 7.x --- components/Notifications/OpenSolution.bat | 3 + .../src/Adaptive/AdaptiveGroup.cs | 41 + .../src/Adaptive/AdaptiveHelper.cs | 41 + .../src/Adaptive/AdaptiveImage.cs | 92 + .../src/Adaptive/AdaptiveImageEnums.cs | 58 + .../src/Adaptive/AdaptiveProgressBar.cs | 84 + .../AdaptiveProgressBarBindableProperty.cs | 35 + .../src/Adaptive/AdaptiveProgressBarValue.cs | 96 + .../src/Adaptive/AdaptiveSubgroup.cs | 77 + .../src/Adaptive/AdaptiveSubgroupEnums.cs | 32 + .../src/Adaptive/AdaptiveText.cs | 150 ++ .../Adaptive/AdaptiveTextBindableProperty.cs | 20 + .../src/Adaptive/AdaptiveTextEnums.cs | 142 ++ .../src/Adaptive/BaseImageHelper.cs | 37 + .../src/Adaptive/BaseTextHelper.cs | 20 + .../BindableProgressBarValue.cs | 92 + .../Adaptive/BindableValues/BindableString.cs | 72 + .../Elements/Element_AdaptiveGroup.cs | 34 + .../Elements/Element_AdaptiveImage.cs | 93 + .../Elements/Element_AdaptiveImageEnums.cs | 15 + .../Elements/Element_AdaptiveProgressBar.cs | 31 + .../Elements/Element_AdaptiveSubgroup.cs | 73 + .../Adaptive/Elements/Element_AdaptiveText.cs | 116 + .../src/Adaptive/IAdaptiveChild.cs | 14 + .../src/Adaptive/IAdaptiveSubgroupChild.cs | 14 + .../Notifications/src/Adaptive/IBaseImage.cs | 27 + .../Notifications/src/Adaptive/IBaseText.cs | 22 + .../src/Badges/BadgeGlyphContent.cs | 98 + .../src/Badges/BadgeGlyphValue.cs | 78 + .../src/Badges/BadgeNumericContent.cs | 80 + .../src/Common/ArgumentValidator.cs | 30 + .../Notifications/src/Common/BaseElement.cs | 56 + .../Notifications/src/Common/EnumFormatter.cs | 57 + .../src/Common/INotificationContent.cs | 30 + .../src/Common/LimitedList{T}.cs | 116 + .../NotificationContentValidationException.cs | 22 + .../IHaveXmlAdditionalProperties.cs | 21 + .../Common/Serialization/IHaveXmlChildren.cs | 21 + .../src/Common/Serialization/IHaveXmlName.cs | 17 + .../Serialization/IHaveXmlNamedProperties.cs | 23 + .../src/Common/Serialization/IHaveXmlText.cs | 17 + .../src/Common/XmlWriterHelper.cs | 87 + .../src/CommunityToolkit.Notifications.csproj | 21 + .../Notifications/src/Dependencies.props | 31 + .../Notifications/src/MultiTarget.props | 9 + .../TileContentBuilder.SpecialTiles.cs | 163 ++ .../src/Tiles/Builder/TileContentBuilder.cs | 531 ++++ .../src/Tiles/Elements/Element_Tile.cs | 19 + .../src/Tiles/Elements/Element_TileBinding.cs | 146 ++ .../src/Tiles/Elements/Element_TileVisual.cs | 54 + .../src/Tiles/Elements/TileElementsCommon.cs | 20 + .../Tiles/ITileBindingContentAdaptiveChild.cs | 14 + .../TileBindingContentContact.cs | 43 + .../TileBindingContentIconic.cs | 44 + .../TileBindingContentPeople.cs | 35 + .../TileBindingContentPhotos.cs | 34 + .../src/Tiles/TileBackgroundImage.cs | 90 + .../Notifications/src/Tiles/TileBasicImage.cs | 42 + .../Notifications/src/Tiles/TileBasicText.cs | 42 + .../Notifications/src/Tiles/TileBinding.cs | 168 ++ .../src/Tiles/TileBindingContentAdaptive.cs | 72 + .../Notifications/src/Tiles/TileBranding.cs | 37 + .../Notifications/src/Tiles/TileCommon.cs | 35 + .../Notifications/src/Tiles/TileContent.cs | 57 + .../Notifications/src/Tiles/TileImages.cs | 48 + .../Notifications/src/Tiles/TilePeekImage.cs | 91 + .../TileSizeToAdaptiveTemplateConverter.cs | 32 + .../src/Tiles/TileTemplateNameV3.cs | 92 + .../src/Tiles/TileTextStacking.cs | 27 + .../Notifications/src/Tiles/TileVisual.cs | 219 ++ .../src/Toasts/Builder/CustomizeToast.cs | 39 + .../Builder/ToastContentBuilder.Actions.cs | 319 +++ .../Builder/ToastContentBuilder.Visuals.cs | 594 +++++ .../src/Toasts/Builder/ToastContentBuilder.cs | 505 ++++ .../Compat/Desktop/DesktopBridgeHelpers.cs | 99 + .../DesktopNotificationHistoryCompat.cs | 108 + .../DesktopNotificationManagerCompat.cs | 223 ++ .../Desktop/InternalNotificationActivator.cs | 71 + .../Toasts/Compat/Desktop/ManifestHelper.cs | 91 + .../Compat/Desktop/Native/CAppResolver.cs | 20 + .../Desktop/Native/IApplicationResolver.cs | 80 + .../Compat/Desktop/Native/IShellItem.cs | 41 + .../Desktop/Native/IShellItemImageFactory.cs | 24 + .../Compat/Desktop/Native/NativeMethods.cs | 32 + .../Toasts/Compat/Desktop/Native/SIIGBF.cs | 24 + .../src/Toasts/Compat/Desktop/Native/SIZE.cs | 25 + .../Compat/Desktop/NotificationActivator.cs | 85 + .../Compat/Desktop/NotificationUserInput.cs | 103 + .../src/Toasts/Compat/Desktop/OnActivated.cs | 16 + ...astNotificationActivatedEventArgsCompat.cs | 32 + .../src/Toasts/Compat/Desktop/Win32AppInfo.cs | 296 +++ .../Compat/ToastNotificationHistoryCompat.cs | 105 + .../Compat/ToastNotificationManagerCompat.cs | 657 +++++ .../src/Toasts/Compat/ToastNotifierCompat.cs | 172 ++ .../src/Toasts/Elements/Element_Toast.cs | 156 ++ .../Toasts/Elements/Element_ToastAction.cs | 106 + .../Toasts/Elements/Element_ToastActions.cs | 42 + .../src/Toasts/Elements/Element_ToastAudio.cs | 46 + .../Toasts/Elements/Element_ToastBinding.cs | 77 + .../Toasts/Elements/Element_ToastHeader.cs | 61 + .../src/Toasts/Elements/Element_ToastImage.cs | 73 + .../src/Toasts/Elements/Element_ToastInput.cs | 61 + .../Toasts/Elements/Element_ToastSelection.cs | 31 + .../src/Toasts/Elements/Element_ToastText.cs | 39 + .../Toasts/Elements/Element_ToastVisual.cs | 39 + .../Elements/IElement_ToastActivatable.cs | 15 + .../Notifications/src/Toasts/IToastActions.cs | 19 + .../src/Toasts/IToastActivateableBuilder.cs | 96 + .../src/Toasts/IToastBindingGenericChild.cs | 14 + .../Notifications/src/Toasts/IToastButton.cs | 24 + .../Notifications/src/Toasts/IToastInput.cs | 13 + .../src/Toasts/ToastActionsCustom.cs | 94 + .../Toasts/ToastActionsSnoozeAndDismiss.cs | 41 + .../src/Toasts/ToastActivationOptions.cs | 38 + .../src/Toasts/ToastArguments.cs | 462 ++++ .../Notifications/src/Toasts/ToastAudio.cs | 39 + .../src/Toasts/ToastBindingGeneric.cs | 99 + .../src/Toasts/ToastBindingShoulderTap.cs | 56 + .../Notifications/src/Toasts/ToastButton.cs | 466 ++++ .../src/Toasts/ToastButtonDismiss.cs | 67 + .../src/Toasts/ToastButtonSnooze.cs | 77 + .../Notifications/src/Toasts/ToastCommon.cs | 43 + .../Notifications/src/Toasts/ToastContent.cs | 192 ++ .../src/Toasts/ToastContextMenuItem.cs | 79 + .../src/Toasts/ToastGenericAppLogo.cs | 73 + .../src/Toasts/ToastGenericAppLogoEnums.cs | 27 + .../src/Toasts/ToastGenericAttributionText.cs | 41 + .../src/Toasts/ToastGenericHeroImage.cs | 52 + .../Notifications/src/Toasts/ToastHeader.cs | 116 + .../Notifications/src/Toasts/ToastPeople.cs | 56 + .../src/Toasts/ToastSelectionBox.cs | 63 + .../src/Toasts/ToastSelectionBoxItem.cs | 45 + .../src/Toasts/ToastShoulderTapImage.cs | 57 + .../src/Toasts/ToastSpriteSheet.cs | 56 + .../Notifications/src/Toasts/ToastTextBox.cs | 56 + .../Notifications/src/Toasts/ToastVisual.cs | 66 + .../tests/Notifications.Tests.projitems | 24 + .../tests/Notifications.Tests.shproj | 13 + .../Notifications/tests/TestAssertHelper.cs | 457 ++++ components/Notifications/tests/TestMail.cs | 168 ++ .../tests/TestTileContentBuilder.cs | 114 + .../Notifications/tests/TestToastArguments.cs | 511 ++++ .../tests/TestToastContentBuilder.cs | 1201 +++++++++ components/Notifications/tests/TestWeather.cs | 292 +++ .../Notifications/tests/Test_Adaptive_Xml.cs | 604 +++++ .../Notifications/tests/Test_Badge_Xml.cs | 77 + .../Notifications/tests/Test_Tile_Xml.cs | 1676 +++++++++++++ .../Notifications/tests/Test_Toast_Xml.cs | 2212 +++++++++++++++++ .../Notifications/tests/TextXboxModern.cs | 163 ++ 149 files changed, 19146 insertions(+) create mode 100644 components/Notifications/OpenSolution.bat create mode 100644 components/Notifications/src/Adaptive/AdaptiveGroup.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveHelper.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveImage.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveImageEnums.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveProgressBar.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveProgressBarBindableProperty.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveSubgroup.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveText.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveTextBindableProperty.cs create mode 100644 components/Notifications/src/Adaptive/AdaptiveTextEnums.cs create mode 100644 components/Notifications/src/Adaptive/BaseImageHelper.cs create mode 100644 components/Notifications/src/Adaptive/BaseTextHelper.cs create mode 100644 components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs create mode 100644 components/Notifications/src/Adaptive/BindableValues/BindableString.cs create mode 100644 components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs create mode 100644 components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs create mode 100644 components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs create mode 100644 components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs create mode 100644 components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs create mode 100644 components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs create mode 100644 components/Notifications/src/Adaptive/IAdaptiveChild.cs create mode 100644 components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs create mode 100644 components/Notifications/src/Adaptive/IBaseImage.cs create mode 100644 components/Notifications/src/Adaptive/IBaseText.cs create mode 100644 components/Notifications/src/Badges/BadgeGlyphContent.cs create mode 100644 components/Notifications/src/Badges/BadgeGlyphValue.cs create mode 100644 components/Notifications/src/Badges/BadgeNumericContent.cs create mode 100644 components/Notifications/src/Common/ArgumentValidator.cs create mode 100644 components/Notifications/src/Common/BaseElement.cs create mode 100644 components/Notifications/src/Common/EnumFormatter.cs create mode 100644 components/Notifications/src/Common/INotificationContent.cs create mode 100644 components/Notifications/src/Common/LimitedList{T}.cs create mode 100644 components/Notifications/src/Common/NotificationContentValidationException.cs create mode 100644 components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs create mode 100644 components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs create mode 100644 components/Notifications/src/Common/Serialization/IHaveXmlName.cs create mode 100644 components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs create mode 100644 components/Notifications/src/Common/Serialization/IHaveXmlText.cs create mode 100644 components/Notifications/src/Common/XmlWriterHelper.cs create mode 100644 components/Notifications/src/CommunityToolkit.Notifications.csproj create mode 100644 components/Notifications/src/Dependencies.props create mode 100644 components/Notifications/src/MultiTarget.props create mode 100644 components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs create mode 100644 components/Notifications/src/Tiles/Builder/TileContentBuilder.cs create mode 100644 components/Notifications/src/Tiles/Elements/Element_Tile.cs create mode 100644 components/Notifications/src/Tiles/Elements/Element_TileBinding.cs create mode 100644 components/Notifications/src/Tiles/Elements/Element_TileVisual.cs create mode 100644 components/Notifications/src/Tiles/Elements/TileElementsCommon.cs create mode 100644 components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs create mode 100644 components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs create mode 100644 components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs create mode 100644 components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs create mode 100644 components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs create mode 100644 components/Notifications/src/Tiles/TileBackgroundImage.cs create mode 100644 components/Notifications/src/Tiles/TileBasicImage.cs create mode 100644 components/Notifications/src/Tiles/TileBasicText.cs create mode 100644 components/Notifications/src/Tiles/TileBinding.cs create mode 100644 components/Notifications/src/Tiles/TileBindingContentAdaptive.cs create mode 100644 components/Notifications/src/Tiles/TileBranding.cs create mode 100644 components/Notifications/src/Tiles/TileCommon.cs create mode 100644 components/Notifications/src/Tiles/TileContent.cs create mode 100644 components/Notifications/src/Tiles/TileImages.cs create mode 100644 components/Notifications/src/Tiles/TilePeekImage.cs create mode 100644 components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs create mode 100644 components/Notifications/src/Tiles/TileTemplateNameV3.cs create mode 100644 components/Notifications/src/Tiles/TileTextStacking.cs create mode 100644 components/Notifications/src/Tiles/TileVisual.cs create mode 100644 components/Notifications/src/Toasts/Builder/CustomizeToast.cs create mode 100644 components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs create mode 100644 components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs create mode 100644 components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/DesktopBridgeHelpers.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationHistoryCompat.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationManagerCompat.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/InternalNotificationActivator.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/ManifestHelper.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/CAppResolver.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/IApplicationResolver.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItem.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItemImageFactory.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/NativeMethods.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/SIIGBF.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Native/SIZE.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/NotificationActivator.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/NotificationUserInput.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/OnActivated.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/ToastNotificationActivatedEventArgsCompat.cs create mode 100644 components/Notifications/src/Toasts/Compat/Desktop/Win32AppInfo.cs create mode 100644 components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs create mode 100644 components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs create mode 100644 components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_Toast.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastAction.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastActions.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastImage.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastInput.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastText.cs create mode 100644 components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs create mode 100644 components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs create mode 100644 components/Notifications/src/Toasts/IToastActions.cs create mode 100644 components/Notifications/src/Toasts/IToastActivateableBuilder.cs create mode 100644 components/Notifications/src/Toasts/IToastBindingGenericChild.cs create mode 100644 components/Notifications/src/Toasts/IToastButton.cs create mode 100644 components/Notifications/src/Toasts/IToastInput.cs create mode 100644 components/Notifications/src/Toasts/ToastActionsCustom.cs create mode 100644 components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs create mode 100644 components/Notifications/src/Toasts/ToastActivationOptions.cs create mode 100644 components/Notifications/src/Toasts/ToastArguments.cs create mode 100644 components/Notifications/src/Toasts/ToastAudio.cs create mode 100644 components/Notifications/src/Toasts/ToastBindingGeneric.cs create mode 100644 components/Notifications/src/Toasts/ToastBindingShoulderTap.cs create mode 100644 components/Notifications/src/Toasts/ToastButton.cs create mode 100644 components/Notifications/src/Toasts/ToastButtonDismiss.cs create mode 100644 components/Notifications/src/Toasts/ToastButtonSnooze.cs create mode 100644 components/Notifications/src/Toasts/ToastCommon.cs create mode 100644 components/Notifications/src/Toasts/ToastContent.cs create mode 100644 components/Notifications/src/Toasts/ToastContextMenuItem.cs create mode 100644 components/Notifications/src/Toasts/ToastGenericAppLogo.cs create mode 100644 components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs create mode 100644 components/Notifications/src/Toasts/ToastGenericAttributionText.cs create mode 100644 components/Notifications/src/Toasts/ToastGenericHeroImage.cs create mode 100644 components/Notifications/src/Toasts/ToastHeader.cs create mode 100644 components/Notifications/src/Toasts/ToastPeople.cs create mode 100644 components/Notifications/src/Toasts/ToastSelectionBox.cs create mode 100644 components/Notifications/src/Toasts/ToastSelectionBoxItem.cs create mode 100644 components/Notifications/src/Toasts/ToastShoulderTapImage.cs create mode 100644 components/Notifications/src/Toasts/ToastSpriteSheet.cs create mode 100644 components/Notifications/src/Toasts/ToastTextBox.cs create mode 100644 components/Notifications/src/Toasts/ToastVisual.cs create mode 100644 components/Notifications/tests/Notifications.Tests.projitems create mode 100644 components/Notifications/tests/Notifications.Tests.shproj create mode 100644 components/Notifications/tests/TestAssertHelper.cs create mode 100644 components/Notifications/tests/TestMail.cs create mode 100644 components/Notifications/tests/TestTileContentBuilder.cs create mode 100644 components/Notifications/tests/TestToastArguments.cs create mode 100644 components/Notifications/tests/TestToastContentBuilder.cs create mode 100644 components/Notifications/tests/TestWeather.cs create mode 100644 components/Notifications/tests/Test_Adaptive_Xml.cs create mode 100644 components/Notifications/tests/Test_Badge_Xml.cs create mode 100644 components/Notifications/tests/Test_Tile_Xml.cs create mode 100644 components/Notifications/tests/Test_Toast_Xml.cs create mode 100644 components/Notifications/tests/TextXboxModern.cs diff --git a/components/Notifications/OpenSolution.bat b/components/Notifications/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/Notifications/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveGroup.cs b/components/Notifications/src/Adaptive/AdaptiveGroup.cs new file mode 100644 index 000000000..e4f6310cc --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveGroup.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// Groups semantically identify that the content in the group must either be displayed as a whole, or not displayed if it cannot fit. Groups also allow creating multiple columns. Supported on Tiles since RTM. Supported on Toasts since Anniversary Update. + /// + public sealed class AdaptiveGroup : ITileBindingContentAdaptiveChild, IAdaptiveChild, IToastBindingGenericChild + { + /// + /// Gets the only valid children of groups are . + /// Each subgroup is displayed as a separate vertical column. Note that you must + /// include at least one subgroup in your group, otherwise an + /// will be thrown when you try to retrieve the XML for the notification. + /// + public IList Children { get; private set; } = new List(); + + internal Element_AdaptiveGroup ConvertToElement() + { + if (Children.Count == 0) + { + throw new InvalidOperationException("Groups must have at least one child subgroup. The Children property had zero items in it."); + } + + Element_AdaptiveGroup group = new Element_AdaptiveGroup(); + + foreach (var subgroup in Children) + { + group.Children.Add(subgroup.ConvertToElement()); + } + + return group; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveHelper.cs b/components/Notifications/src/Adaptive/AdaptiveHelper.cs new file mode 100644 index 000000000..67b4db649 --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveHelper.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications.Adaptive +{ + internal static class AdaptiveHelper + { + internal static object ConvertToElement(object obj) + { + if (obj is AdaptiveText) + { + return (obj as AdaptiveText).ConvertToElement(); + } + + if (obj is AdaptiveImage) + { + return (obj as AdaptiveImage).ConvertToElement(); + } + + if (obj is AdaptiveGroup) + { + return (obj as AdaptiveGroup).ConvertToElement(); + } + + if (obj is AdaptiveSubgroup) + { + return (obj as AdaptiveSubgroup).ConvertToElement(); + } + + if (obj is AdaptiveProgressBar) + { + return (obj as AdaptiveProgressBar).ConvertToElement(); + } + + throw new NotImplementedException("Unknown object: " + obj.GetType()); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveImage.cs b/components/Notifications/src/Adaptive/AdaptiveImage.cs new file mode 100644 index 000000000..2a5a756f2 --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveImage.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// An inline image. + /// + public sealed class AdaptiveImage + : IBaseImage, + IToastBindingGenericChild, + ITileBindingContentAdaptiveChild, + IAdaptiveChild, + IAdaptiveSubgroupChild + { + /// + /// Gets or sets the desired cropping of the image. + /// Supported on Tiles since RTM. Supported on Toast since Anniversary Update. + /// + public AdaptiveImageCrop HintCrop { get; set; } + + /// + /// Gets or sets a value whether a margin is removed. images have an 8px margin around them. + /// You can remove this margin by setting this property to true. + /// Supported on Tiles since RTM. Supported on Toast since Anniversary Update. + /// + public bool? HintRemoveMargin { get; set; } + + /// + /// Gets or sets the horizontal alignment of the image. + /// For Toast, this is only supported when inside an . + /// + public AdaptiveImageAlign HintAlign { get; set; } + + private string _source; + + /// + /// Gets or sets the URI of the image (Required). + /// Can be from your application package, application data, or the internet. + /// Internet images must be less than 200 KB in size. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets set to true to allow Windows to append a query string to the image URI + /// supplied in the Tile notification. Use this attribute if your server hosts + /// images and can handle query strings, either by retrieving an image variant based + /// on the query strings or by ignoring the query string and returning the image + /// as specified without the query string. This query string specifies scale, + /// contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + /// + /// Returns the image's source string. + /// + /// The image's source string. + public override string ToString() + { + if (Source == null) + { + return "Source is null"; + } + + return Source; + } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this); + + image.Crop = HintCrop; + image.RemoveMargin = HintRemoveMargin; + image.Align = HintAlign; + image.Placement = AdaptiveImagePlacement.Inline; + + return image; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveImageEnums.cs b/components/Notifications/src/Adaptive/AdaptiveImageEnums.cs new file mode 100644 index 000000000..498471c97 --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveImageEnums.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Specifies the horizontal alignment for an image. + /// + public enum AdaptiveImageAlign + { + /// + /// Default value, alignment behavior determined by renderer. + /// + Default, + + /// + /// Image stretches to fill available width (and potentially available height too, depending on where the image is). + /// + Stretch, + + /// + /// Align the image to the left, displaying the image at its native resolution. + /// + Left, + + /// + /// Align the image in the center horizontally, displaying the image at its native resolution. + /// + Center, + + /// + /// Align the image to the right, displaying the image at its native resolution. + /// + Right + } + + /// + /// Specify the desired cropping of the image. + /// + public enum AdaptiveImageCrop + { + /// + /// Default value, cropping behavior determined by renderer. + /// + Default, + + /// + /// Image is not cropped. + /// + None, + + /// + /// Image is cropped to a circle shape. + /// + Circle + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveProgressBar.cs b/components/Notifications/src/Adaptive/AdaptiveProgressBar.cs new file mode 100644 index 000000000..957765dc4 --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveProgressBar.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma warning disable SA1121 // UseBuiltInTypeAlias + +using System; +using CommunityToolkit.Notifications.Adaptive.Elements; + +#if WINRT +using System.Collections.Generic; +using BindableProgressBarValue = CommunityToolkit.Notifications.AdaptiveProgressBarValue; +using BindableString = System.String; +#else +using BindableProgressBarValue = CommunityToolkit.Notifications.BindableProgressBarValue; +using BindableString = CommunityToolkit.Notifications.BindableString; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// New in Creators Update: A progress bar. Only supported on toasts on Desktop, build 15007 or newer. + /// + public sealed class AdaptiveProgressBar : IToastBindingGenericChild + { +#if WINRT + /// + /// Gets a dictionary of the current data bindings, where you can assign new bindings. + /// + public IDictionary Bindings { get; private set; } = new Dictionary(); +#endif + + /// + /// Gets or sets an optional title string. Supports data binding. + /// + public BindableString Title { get; set; } + + /// + /// Gets or sets the value of the progress bar. Supports data binding. Defaults to 0. + /// + public BindableProgressBarValue Value { get; set; } = AdaptiveProgressBarValue.FromValue(0); + + /// + /// Gets or sets an optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed. + /// + public BindableString ValueStringOverride { get; set; } + + /// + /// Gets or sets a status string (Required), which is displayed underneath the progress bar. This string should reflect the status of the operation, like "Downloading..." or "Installing..." + /// + public BindableString Status { get; set; } + + internal Element_AdaptiveProgressBar ConvertToElement() + { + // If Value not provided, we use 0 + var val = Value; + if (val == null) + { + val = AdaptiveProgressBarValue.FromValue(0); + } + + var answer = new Element_AdaptiveProgressBar(); + +#if WINRT + answer.Title = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.Title, Title); + answer.Value = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.Value, val.ToXmlString()); + answer.ValueStringOverride = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.ValueStringOverride, ValueStringOverride); + answer.Status = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveProgressBarBindableProperty.Status, Status); +#else + answer.Title = Title?.ToXmlString(); + answer.Value = val.ToXmlString(); + answer.ValueStringOverride = ValueStringOverride?.ToXmlString(); + answer.Status = Status?.ToXmlString(); +#endif + + if (answer.Status == null) + { + throw new NullReferenceException("Status property is required."); + } + + return answer; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveProgressBarBindableProperty.cs b/components/Notifications/src/Adaptive/AdaptiveProgressBarBindableProperty.cs new file mode 100644 index 000000000..25065a16e --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveProgressBarBindableProperty.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + // Note that this code is only compiled for WinRT. It is not compiled in any of the other projects. +#if WINRT + /// + /// An enumeration of the properties that support data binding on . + /// + public enum AdaptiveProgressBarBindableProperty + { + /// + /// An optional title string + /// + Title, + + /// + /// The value of the progress bar. + /// + Value, + + /// + /// An optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed. + /// + ValueStringOverride, + + /// + /// An optional status string, which is displayed underneath the progress bar. If provided, this string should reflect the status of the download, like "Downloading..." or "Installing...". + /// + Status + } +#endif +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs b/components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs new file mode 100644 index 000000000..a6e6880cd --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// A class that represents the progress bar's value. + /// + public sealed class AdaptiveProgressBarValue + { + /// + /// Gets or sets the property name to bind to. + /// + public string BindingName { get; set; } + + /// + /// Gets or sets the value (0-1) representing the percent complete. + /// + public double Value { get; set; } + + /// + /// Gets or sets a value indicating whether the progress bar is indeterminate. + /// + public bool IsIndeterminate { get; set; } + + /// + /// Initializes a new instance of the class. + /// + private AdaptiveProgressBarValue() + { + } + + internal string ToXmlString() + { + if (IsIndeterminate) + { + return "indeterminate"; + } + + if (BindingName != null) + { + return "{" + BindingName + "}"; + } + + return Value.ToString(); + } + + /// + /// Gets an indeterminate progress bar value. + /// + public static AdaptiveProgressBarValue Indeterminate + { + get + { + return new AdaptiveProgressBarValue() + { + IsIndeterminate = true + }; + } + } + + /// + /// Returns a progress bar value using the specified value (0-1) representing the percent complete. + /// + /// The value, 0-1, inclusive. + /// A progress bar value. + public static AdaptiveProgressBarValue FromValue(double d) + { + if (d < 0 || d > 1) + { + throw new ArgumentOutOfRangeException("d", "Value must be between 0 and 1, inclusive."); + } + + return new AdaptiveProgressBarValue() + { + Value = d + }; + } + + /// + /// Returns a progress bar value using the specified binding name. + /// + /// The property to bind to. + /// A progress bar value. + public static AdaptiveProgressBarValue FromBinding(string bindingName) + { + return new AdaptiveProgressBarValue() + { + BindingName = bindingName + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveSubgroup.cs b/components/Notifications/src/Adaptive/AdaptiveSubgroup.cs new file mode 100644 index 000000000..9a136c843 --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveSubgroup.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// Subgroups are vertical columns that can contain text and images. Supported on Tiles since RTM. Supported on Toasts since Anniversary Update. + /// + public sealed class AdaptiveSubgroup + { + /// + /// Gets a list of Children. and are valid children of subgroups. + /// + public IList Children { get; private set; } = new List(); + + private int? _hintWeight; + + /// + /// Gets or sets the width of this subgroup column by specifying the weight, relative to the other subgroups. + /// + public int? HintWeight + { + get + { + return _hintWeight; + } + + set + { + Element_AdaptiveSubgroup.CheckWeight(value); + + _hintWeight = value; + } + } + + /// + /// Gets or sets the vertical alignment of this subgroup's content. + /// + public AdaptiveSubgroupTextStacking HintTextStacking { get; set; } = Element_AdaptiveSubgroup.DEFAULT_TEXT_STACKING; + + internal Element_AdaptiveSubgroup ConvertToElement() + { + var subgroup = new Element_AdaptiveSubgroup() + { + Weight = HintWeight, + TextStacking = HintTextStacking + }; + + foreach (var child in Children) + { + subgroup.Children.Add(ConvertToSubgroupChildElement(child)); + } + + return subgroup; + } + + private static IElement_AdaptiveSubgroupChild ConvertToSubgroupChildElement(IAdaptiveSubgroupChild child) + { + if (child is AdaptiveText) + { + return (child as AdaptiveText).ConvertToElement(); + } + + if (child is AdaptiveImage) + { + return (child as AdaptiveImage).ConvertToElement(); + } + + throw new NotImplementedException("Unknown child: " + child.GetType()); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs b/components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs new file mode 100644 index 000000000..ce41b799c --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// TextStacking specifies the vertical alignment of content. + /// + public enum AdaptiveSubgroupTextStacking + { + /// + /// Renderer automatically selects the default vertical alignment. + /// + Default, + + /// + /// Vertical align to the top. + /// + Top, + + /// + /// Vertical align to the center. + /// + Center, + + /// + /// Vertical align to the bottom. + /// + Bottom + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveText.cs b/components/Notifications/src/Adaptive/AdaptiveText.cs new file mode 100644 index 000000000..2cf9039c1 --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveText.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma warning disable SA1121 // UseBuiltInTypeAlias + +using System; +using CommunityToolkit.Notifications.Adaptive.Elements; + +#if WINRT +using System.Collections.Generic; +using BindableString = System.String; +#else +using BindableString = CommunityToolkit.Notifications.BindableString; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// An adaptive text element. + /// + public sealed class AdaptiveText + : IAdaptiveChild, + IAdaptiveSubgroupChild, + ITileBindingContentAdaptiveChild, + IToastBindingGenericChild + { +#if WINRT + /// + /// Gets a dictionary of the current data bindings, where you can assign new bindings. + /// + public IDictionary Bindings { get; private set; } = new Dictionary(); +#endif + + /// + /// Gets or sets the text to display. Data binding support added in Creators Update, + /// only works for toast top-level text elements. + /// + public BindableString Text { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags + /// such as "en-US" or "fr-FR". The locale specified here overrides any other specified + /// locale, such as that in binding or visual. If this value is a literal string, + /// this attribute defaults to the user's UI language. If this value is a string reference, + /// this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Language { get; set; } + + /// + /// Gets or sets the style that controls the text's font size, weight, and opacity. + /// Note that for Toast, the style will only take effect if the text is inside an . + /// + public AdaptiveTextStyle HintStyle { get; set; } + + /// + /// Gets or sets a value whether text wrapping is enabled. For Tiles, this is false by default. + /// For Toasts, this is true on top-level text elements, and false inside an . + /// Note that for Toast, setting wrap will only take effect if the text is inside an + /// (you can use HintMaxLines = 1 to prevent top-level text elements from wrapping). + /// + public bool? HintWrap { get; set; } + + private int? _hintMaxLines; + + /// + /// Gets or sets the maximum number of lines the text element is allowed to display. + /// For Tiles, this is infinity by default. For Toasts, top-level text elements will + /// have varying max line amounts (and in the Anniversary Update you can change the max lines). + /// Text on a Toast inside an will behave identically to Tiles (default to infinity). + /// + public int? HintMaxLines + { + get + { + return _hintMaxLines; + } + + set + { + if (value != null) + { + Element_AdaptiveText.CheckMaxLinesValue(value.Value); + } + + _hintMaxLines = value; + } + } + + private int? _hintMinLines; + + /// + /// Gets or sets the minimum number of lines the text element must display. + /// Note that for Toast, this property will only take effect if the text is inside an . + /// + public int? HintMinLines + { + get + { + return _hintMinLines; + } + + set + { + if (value != null) + { + Element_AdaptiveText.CheckMinLinesValue(value.Value); + } + + _hintMinLines = value; + } + } + + /// + /// Gets or sets the horizontal alignment of the text. Note that for Toast, this property will + /// only take effect if the text is inside an . + /// + public AdaptiveTextAlign HintAlign { get; set; } + + internal Element_AdaptiveText ConvertToElement() + { + var answer = new Element_AdaptiveText() + { + Lang = Language, + Style = HintStyle, + Wrap = HintWrap, + MaxLines = HintMaxLines, + MinLines = HintMinLines, + Align = HintAlign + }; + +#if WINRT + answer.Text = XmlWriterHelper.GetBindingOrAbsoluteXmlValue(Bindings, AdaptiveTextBindableProperty.Text, Text); +#else + answer.Text = Text?.ToXmlString(); +#endif + + return answer; + } + + /// + /// Returns the value of the Text property. + /// + /// The value of the Text property. + public override string ToString() + { + return Text; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveTextBindableProperty.cs b/components/Notifications/src/Adaptive/AdaptiveTextBindableProperty.cs new file mode 100644 index 000000000..cdc948efc --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveTextBindableProperty.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + // Note that this code is only compiled for WinRT. It is not compiled in any of the other projects. +#if WINRT + /// + /// An enumeration of the properties that support data binding on . + /// + public enum AdaptiveTextBindableProperty + { + /// + /// The text to display. Added in Creators Update only for toast top-level elements. + /// + Text + } +#endif +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/AdaptiveTextEnums.cs b/components/Notifications/src/Adaptive/AdaptiveTextEnums.cs new file mode 100644 index 000000000..60eb0d72e --- /dev/null +++ b/components/Notifications/src/Adaptive/AdaptiveTextEnums.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Text style controls font size, weight, and opacity. + /// + public enum AdaptiveTextStyle + { + /// + /// Style is determined by the renderer. + /// + Default, + + /// + /// Default value. Paragraph font size, normal weight and opacity. + /// + Caption, + + /// + /// Same as Caption but with subtle opacity. + /// + CaptionSubtle, + + /// + /// H5 font size. + /// + Body, + + /// + /// Same as Body but with subtle opacity. + /// + BodySubtle, + + /// + /// H5 font size, bold weight. Essentially the bold version of Body. + /// + Base, + + /// + /// Same as Base but with subtle opacity. + /// + BaseSubtle, + + /// + /// H4 font size. + /// + Subtitle, + + /// + /// Same as Subtitle but with subtle opacity. + /// + SubtitleSubtle, + + /// + /// H3 font size. + /// + Title, + + /// + /// Same as Title but with subtle opacity. + /// + TitleSubtle, + + /// + /// Same as Title but with top/bottom padding removed. + /// + TitleNumeral, + + /// + /// H2 font size. + /// + Subheader, + + /// + /// Same as Subheader but with subtle opacity. + /// + SubheaderSubtle, + + /// + /// Same as Subheader but with top/bottom padding removed. + /// + SubheaderNumeral, + + /// + /// H1 font size. + /// + Header, + + /// + /// Same as Header but with subtle opacity. + /// + HeaderSubtle, + + /// + /// Same as Header but with top/bottom padding removed. + /// + HeaderNumeral + } + + /// + /// Controls the horizontal alignment of text. + /// + public enum AdaptiveTextAlign + { + /// + /// Alignment is automatically determined by + /// + Default, + + /// + /// The system automatically decides the alignment based on the language and culture. + /// + Auto, + + /// + /// Horizontally align the text to the left. + /// + Left, + + /// + /// Horizontally align the text in the center. + /// + Center, + + /// + /// Horizontally align the text to the right. + /// + Right + } + + internal enum AdaptiveTextPlacement + { + /// + /// Default value + /// + Inline, + Attribution + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/BaseImageHelper.cs b/components/Notifications/src/Adaptive/BaseImageHelper.cs new file mode 100644 index 000000000..778aedbb1 --- /dev/null +++ b/components/Notifications/src/Adaptive/BaseImageHelper.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + internal static class BaseImageHelper + { + internal static void SetSource(ref string destination, string value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + destination = value; + } + + internal static Element_AdaptiveImage CreateBaseElement(IBaseImage current) + { + if (current.Source == null) + { + throw new NullReferenceException("Source property is required."); + } + + return new Element_AdaptiveImage() + { + Src = current.Source, + Alt = current.AlternateText, + AddImageQuery = current.AddImageQuery + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/BaseTextHelper.cs b/components/Notifications/src/Adaptive/BaseTextHelper.cs new file mode 100644 index 000000000..a65f43d93 --- /dev/null +++ b/components/Notifications/src/Adaptive/BaseTextHelper.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + internal class BaseTextHelper + { + internal static Element_AdaptiveText CreateBaseElement(IBaseText current) + { + return new Element_AdaptiveText() + { + Text = current.Text, + Lang = current.Language + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs b/components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs new file mode 100644 index 000000000..b471d6187 --- /dev/null +++ b/components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + // Note that this code is NOT compiled for WinRT. + // WinRT uses a different binding system since it doesn't support implicit type converters. +#if !WINRT + /// + /// A binding value for doubles. + /// + public sealed class BindableProgressBarValue + { + /// + /// Gets raw value used for the implicit converter case, where dev provided a raw double. We store the raw value, + /// so that later on when generating the XML, we can provide this value rather than binding syntax. + /// + internal AdaptiveProgressBarValue RawValue { get; private set; } + + internal bool RawIsIndeterminate { get; private set; } + + /// + /// Gets or sets the name that maps to your binding data value. + /// + public string BindingName { get; set; } + + /// + /// Initializes a new instance of the class. + /// A new binding for a double value, with the required binding value name. Do NOT include surrounding {} brackets. + /// + /// The name that maps to your binding data value. + public BindableProgressBarValue(string bindingName) + { + BindingName = bindingName; + } + + /// + /// Initializes a new instance of the class. + /// Private constructor used by the implicit converter to assign the raw value. + /// + private BindableProgressBarValue() + { + } + + internal string ToXmlString() + { + if (BindingName != null) + { + return "{" + BindingName + "}"; + } + + if (RawValue != null) + { + return RawValue.ToXmlString(); + } + + return null; + } + + /// + /// Creates a that has a raw value assigned. + /// + /// The raw value + public static implicit operator BindableProgressBarValue(AdaptiveProgressBarValue v) + { + return new BindableProgressBarValue() + { + RawValue = v + }; + } + + /// + /// Returns the raw value of the . + /// + /// The to obtain the raw value from. + public static implicit operator AdaptiveProgressBarValue(BindableProgressBarValue b) + { + return b.RawValue; + } + + /// + /// Creates an that has the raw double value. + /// + /// The raw value + public static implicit operator BindableProgressBarValue(double d) + { + return AdaptiveProgressBarValue.FromValue(d); + } + } +#endif +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/BindableValues/BindableString.cs b/components/Notifications/src/Adaptive/BindableValues/BindableString.cs new file mode 100644 index 000000000..db010ea34 --- /dev/null +++ b/components/Notifications/src/Adaptive/BindableValues/BindableString.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + // Note that this code is NOT compiled for WinRT. + // WinRT uses a different binding system since it doesn't support implicit type converters. +#if !WINRT + /// + /// A binding value for strings. + /// + public sealed class BindableString + { + internal string RawValue { get; private set; } + + /// + /// Gets or sets the name that maps to your binding data value. + /// + public string BindingName { get; set; } + + /// + /// Initializes a new instance of the class. + /// A new binding for a string value, with the required binding name. Do NOT include surrounding {} brackets. + /// + /// The name that maps to your data binding value. + public BindableString(string bindingName) + { + BindingName = bindingName; + } + + /// + /// Initializes a new instance of the class. + /// Private constructor used by the implicit converter to assign the raw value. + /// + private BindableString() + { + } + + internal string ToXmlString() + { + if (BindingName != null) + { + return "{" + BindingName + "}"; + } + + return RawValue; + } + + /// + /// Creates a that has a raw value assigned. + /// + /// The raw value + public static implicit operator BindableString(string d) + { + return new BindableString() + { + RawValue = d + }; + } + + /// + /// Returns the raw value of the . + /// + /// The to obtain the raw value from. + public static implicit operator string(BindableString b) + { + return b.RawValue; + } + } +#endif +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs new file mode 100644 index 000000000..339ca0a03 --- /dev/null +++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications.Adaptive.Elements +{ + internal sealed class Element_AdaptiveGroup : IElement_TileBindingChild, IElement_ToastBindingChild, IElementWithDescendants, IHaveXmlName, IHaveXmlChildren + { + public IList Children { get; private set; } = new List(); + + public IEnumerable Descendants() + { + foreach (Element_AdaptiveSubgroup subgroup in Children) + { + // Return the subgroup + yield return subgroup; + + // And also return its descendants + foreach (object descendant in subgroup.Descendants()) + { + yield return descendant; + } + } + } + + /// + string IHaveXmlName.Name => "group"; + + /// + IEnumerable IHaveXmlChildren.Children => Children; + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs new file mode 100644 index 000000000..3ffe0b0f5 --- /dev/null +++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications.Adaptive.Elements +{ + internal sealed class Element_AdaptiveImage : IElement_TileBindingChild, IElement_ToastBindingChild, IElement_AdaptiveSubgroupChild, IHaveXmlName, IHaveXmlNamedProperties + { + internal const AdaptiveImagePlacement DEFAULT_PLACEMENT = AdaptiveImagePlacement.Inline; + internal const AdaptiveImageCrop DEFAULT_CROP = AdaptiveImageCrop.Default; + internal const AdaptiveImageAlign DEFAULT_ALIGN = AdaptiveImageAlign.Default; + + public int? Id { get; set; } + + public string Src { get; set; } + + public string Alt { get; set; } + + public bool? AddImageQuery { get; set; } + + public AdaptiveImagePlacement Placement { get; set; } = DEFAULT_PLACEMENT; + + public AdaptiveImageAlign Align { get; set; } = DEFAULT_ALIGN; + + public AdaptiveImageCrop Crop { get; set; } = DEFAULT_CROP; + + public bool? RemoveMargin { get; set; } + + private int? _overlay; + + public int? Overlay + { + get + { + return _overlay; + } + + set + { + if (value != null) + { + Element_TileBinding.CheckOverlayValue(value.Value); + } + + _overlay = value; + } + } + + public string SpriteSheetSrc { get; set; } + + public uint? SpriteSheetHeight { get; set; } + + public uint? SpriteSheetFps { get; set; } + + public uint? SpriteSheetStartingFrame { get; set; } + + /// + string IHaveXmlName.Name => "image"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("id", Id); + yield return new("src", Src); + yield return new("alt", Alt); + yield return new("addImageQuery", AddImageQuery); + + if (Placement != DEFAULT_PLACEMENT) + { + yield return new("placement", Placement.ToPascalCaseString()); + } + + if (Align != DEFAULT_ALIGN) + { + yield return new("hint-align", Align.ToPascalCaseString()); + } + + if (Crop != DEFAULT_CROP) + { + yield return new("hint-crop", Crop.ToPascalCaseString()); + } + + yield return new("hint-removeMargin", RemoveMargin); + yield return new("hint-overlay", Overlay); + yield return new("spritesheet-src", SpriteSheetSrc); + yield return new("spritesheet-height", SpriteSheetHeight); + yield return new("spritesheet-fps", SpriteSheetFps); + yield return new("spritesheet-startingFrame", SpriteSheetStartingFrame); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs new file mode 100644 index 000000000..e817f880c --- /dev/null +++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications.Adaptive.Elements +{ + internal enum AdaptiveImagePlacement + { + Inline, + Background, + Peek, + Hero, + AppLogoOverride + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs new file mode 100644 index 000000000..d26e96ee4 --- /dev/null +++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications.Adaptive.Elements +{ + internal sealed class Element_AdaptiveProgressBar : IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties + { + public string Value { get; set; } + + public string Title { get; set; } + + public string ValueStringOverride { get; set; } + + public string Status { get; set; } + + /// + string IHaveXmlName.Name => "progress"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("value", Value); + yield return new("title", Title); + yield return new("valueStringOverride", ValueStringOverride); + yield return new("status", Status); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs new file mode 100644 index 000000000..be3a0953c --- /dev/null +++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications.Adaptive.Elements +{ + internal sealed class Element_AdaptiveSubgroup : IElementWithDescendants, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + internal const AdaptiveSubgroupTextStacking DEFAULT_TEXT_STACKING = AdaptiveSubgroupTextStacking.Default; + + public AdaptiveSubgroupTextStacking TextStacking { get; set; } = DEFAULT_TEXT_STACKING; + + private int? _weight; + + public int? Weight + { + get + { + return _weight; + } + + set + { + CheckWeight(value); + + _weight = value; + } + } + + internal static void CheckWeight(int? weight) + { + if (weight != null && weight.Value < 1) + { + throw new ArgumentOutOfRangeException("Weight must be between 1 and int.MaxValue, inclusive (or null)"); + } + } + + public IList Children { get; private set; } = new List(); + + public IEnumerable Descendants() + { + foreach (IElement_AdaptiveSubgroupChild child in Children) + { + // Return each child (we know there's no further descendants) + yield return child; + } + } + + /// + string IHaveXmlName.Name => "subgroup"; + + /// + IEnumerable IHaveXmlChildren.Children => Children; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + if (TextStacking != DEFAULT_TEXT_STACKING) + { + yield return new("hint-textStacking", TextStacking.ToPascalCaseString()); + } + + yield return new("hint-weight", Weight); + } + } + + internal interface IElement_AdaptiveSubgroupChild + { + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs new file mode 100644 index 000000000..4b87fe9cb --- /dev/null +++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications.Adaptive.Elements +{ + internal sealed class Element_AdaptiveText : IElement_TileBindingChild, IElement_AdaptiveSubgroupChild, IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlText + { + internal const AdaptiveTextStyle DEFAULT_STYLE = AdaptiveTextStyle.Default; + internal const AdaptiveTextAlign DEFAULT_ALIGN = AdaptiveTextAlign.Default; + internal const AdaptiveTextPlacement DEFAULT_PLACEMENT = AdaptiveTextPlacement.Inline; + + public string Text { get; set; } + + public int? Id { get; set; } + + public string Lang { get; set; } + + public AdaptiveTextAlign Align { get; set; } = DEFAULT_ALIGN; + + private int? _maxLines; + + public int? MaxLines + { + get + { + return _maxLines; + } + + set + { + if (value != null) + { + CheckMaxLinesValue(value.Value); + } + + _maxLines = value; + } + } + + internal static void CheckMaxLinesValue(int value) + { + if (value < 1) + { + throw new ArgumentOutOfRangeException("MaxLines must be between 1 and int.MaxValue, inclusive."); + } + } + + private int? _minLines; + + public int? MinLines + { + get + { + return _minLines; + } + + set + { + if (value != null) + { + CheckMinLinesValue(value.Value); + } + + _minLines = value; + } + } + + internal static void CheckMinLinesValue(int value) + { + if (value < 1) + { + throw new ArgumentOutOfRangeException("MinLines must be between 1 and int.MaxValue, inclusive."); + } + } + + public AdaptiveTextStyle Style { get; set; } = DEFAULT_STYLE; + + public bool? Wrap { get; set; } + + public AdaptiveTextPlacement Placement { get; set; } = DEFAULT_PLACEMENT; + + /// + string IHaveXmlName.Name => "text"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("id", Id); + yield return new("lang", Lang); + + if (Align != DEFAULT_ALIGN) + { + yield return new("hint-align", Align.ToPascalCaseString()); + } + + yield return new("hint-maxLines", MaxLines); + yield return new("hint-minLines", MinLines); + + if (Style != DEFAULT_STYLE) + { + yield return new("hint-style", Style.ToPascalCaseString()); + } + + yield return new("hint-wrap", Wrap); + + if (Placement != DEFAULT_PLACEMENT) + { + yield return new("placement", Placement.ToPascalCaseString()); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/IAdaptiveChild.cs b/components/Notifications/src/Adaptive/IAdaptiveChild.cs new file mode 100644 index 000000000..1c345e518 --- /dev/null +++ b/components/Notifications/src/Adaptive/IAdaptiveChild.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Elements that can be direct children of adaptive content, including (, , and ). + /// + public interface IAdaptiveChild + { + // Blank interface simply for compile-enforcing the child types in the list. + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs b/components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs new file mode 100644 index 000000000..9d257f032 --- /dev/null +++ b/components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Elements that can be direct children of an , including ( and ). + /// + public interface IAdaptiveSubgroupChild + { + // Blank interface simply for compile-enforcing the child types in the list. + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/IBaseImage.cs b/components/Notifications/src/Adaptive/IBaseImage.cs new file mode 100644 index 000000000..c20127153 --- /dev/null +++ b/components/Notifications/src/Adaptive/IBaseImage.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Contains the base properties that an image needs. + /// + public interface IBaseImage + { + /// + /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// + string Source { get; set; } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + string AlternateText { get; set; } + + /// + /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + bool? AddImageQuery { get; set; } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Adaptive/IBaseText.cs b/components/Notifications/src/Adaptive/IBaseText.cs new file mode 100644 index 000000000..38eb7bab4 --- /dev/null +++ b/components/Notifications/src/Adaptive/IBaseText.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Defines the basic properties of a text element. + /// + public interface IBaseText + { + /// + /// Gets or sets the text to display. + /// + string Text { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + string Language { get; set; } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Badges/BadgeGlyphContent.cs b/components/Notifications/src/Badges/BadgeGlyphContent.cs new file mode 100644 index 000000000..7e61b9068 --- /dev/null +++ b/components/Notifications/src/Badges/BadgeGlyphContent.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +#if WINDOWS_UWP +using Windows.Data.Xml.Dom; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Notification content object to display a glyph on a Tile's badge. + /// + public sealed class BadgeGlyphContent : INotificationContent + { + /// + /// Initializes a new instance of the class. + /// Default constructor to create a glyph badge content object. + /// + public BadgeGlyphContent() + { + } + + /// + /// Initializes a new instance of the class. + /// Constructor to create a glyph badge content object with a glyph. + /// + /// The glyph to be displayed on the badge. + public BadgeGlyphContent(BadgeGlyphValue glyph) + { + _glyph = glyph; + } + + /// + /// Gets or sets the glyph to be displayed on the badge. + /// + public BadgeGlyphValue Glyph + { + get + { + return _glyph; + } + + set + { + if (!Enum.IsDefined(typeof(BadgeGlyphValue), value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _glyph = value; + } + } + + /// + /// Retrieves the notification Xml content as a string. + /// + /// The notification Xml content as a string. + public string GetContent() + { + if (!Enum.IsDefined(typeof(BadgeGlyphValue), _glyph)) + { + throw new NotificationContentValidationException("The badge glyph property was left unset."); + } + + string glyphString = _glyph.ToString(); + + // lower case the first character of the enum value to match the Xml schema + glyphString = string.Format("{0}{1}", char.ToLowerInvariant(glyphString[0]), glyphString.Substring(1)); + return string.Format("", glyphString); + } + + /// + /// Retrieves the notification XML content as a string. + /// + /// The notification XML content as a string. + public override string ToString() + { + return GetContent(); + } + +#if WINDOWS_UWP + /// + /// Retrieves the notification XML content as a WinRT Xml document. + /// + /// The notification XML content as a WinRT Xml document. + public XmlDocument GetXml() + { + XmlDocument xml = new XmlDocument(); + xml.LoadXml(GetContent()); + return xml; + } +#endif + + private BadgeGlyphValue _glyph = (BadgeGlyphValue)(-1); + } +} \ No newline at end of file diff --git a/components/Notifications/src/Badges/BadgeGlyphValue.cs b/components/Notifications/src/Badges/BadgeGlyphValue.cs new file mode 100644 index 000000000..17bcb61b8 --- /dev/null +++ b/components/Notifications/src/Badges/BadgeGlyphValue.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// The types of glyphs that can be placed on a badge. + /// + public enum BadgeGlyphValue + { + /// + /// No glyph. If there is a numeric badge, or a glyph currently on the badge, + /// it will be removed. + /// + None = 0, + + /// + /// A glyph representing application activity. + /// + Activity, + + /// + /// A glyph representing an alert. + /// + Alert, + + /// + /// A glyph representing an alarm. + /// + Alarm, + + /// + /// A glyph representing availability status. + /// + Available, + + /// + /// A glyph representing away status + /// + Away, + + /// + /// A glyph representing busy status. + /// + Busy, + + /// + /// A glyph representing that a new message is available. + /// + NewMessage, + + /// + /// A glyph representing that media is paused. + /// + Paused, + + /// + /// A glyph representing that media is playing. + /// + Playing, + + /// + /// A glyph representing unavailable status. + /// + Unavailable, + + /// + /// A glyph representing an error. + /// + Error, + + /// + /// A glyph representing attention status. + /// + Attention + } +} \ No newline at end of file diff --git a/components/Notifications/src/Badges/BadgeNumericContent.cs b/components/Notifications/src/Badges/BadgeNumericContent.cs new file mode 100644 index 000000000..03048c732 --- /dev/null +++ b/components/Notifications/src/Badges/BadgeNumericContent.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP +using Windows.Data.Xml.Dom; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Notification content object to display a number on a Tile's badge. + /// + public sealed class BadgeNumericContent : INotificationContent + { + /// + /// Initializes a new instance of the class. + /// Default constructor to create a numeric badge content object. + /// + public BadgeNumericContent() + { + } + + /// + /// Initializes a new instance of the class. + /// Constructor to create a numeric badge content object with a number. + /// + /// + /// The number that will appear on the badge. If the number is 0, the badge + /// will be removed. + /// + public BadgeNumericContent(uint number) + { + _number = number; + } + + /// + /// Gets or sets the number that will appear on the badge. If the number is 0, the badge + /// will be removed. + /// + public uint Number + { + get { return _number; } + set { _number = value; } + } + + /// + /// Retrieves the notification Xml content as a string. + /// + /// The notification Xml content as a string. + public string GetContent() + { + return string.Format("", _number); + } + + /// + /// Retrieves the notification Xml content as a string. + /// + /// The notification Xml content as a string. + public override string ToString() + { + return GetContent(); + } + +#if WINDOWS_UWP + /// + /// Retrieves the notification Xml content as a WinRT Xml document. + /// + /// The notification Xml content as a WinRT Xml document. + public XmlDocument GetXml() + { + XmlDocument xml = new XmlDocument(); + xml.LoadXml(GetContent()); + return xml; + } +#endif + + private uint _number = 0; + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/ArgumentValidator.cs b/components/Notifications/src/Common/ArgumentValidator.cs new file mode 100644 index 000000000..c1f9c4d82 --- /dev/null +++ b/components/Notifications/src/Common/ArgumentValidator.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + internal static class ArgumentValidator + { + public static void SetProperty(ref T property, T value, string propertyName, ArgumentValidatorOptions options) + { + if (options.HasFlag(ArgumentValidatorOptions.NotNull)) + { + if (value == null) + { + throw new ArgumentNullException(propertyName); + } + } + + property = value; + } + } + + [Flags] + internal enum ArgumentValidatorOptions + { + NotNull + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/BaseElement.cs b/components/Notifications/src/Common/BaseElement.cs new file mode 100644 index 000000000..bf607551e --- /dev/null +++ b/components/Notifications/src/Common/BaseElement.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Text; + +#if WINDOWS_UWP +using Windows.Data.Xml.Dom; +#endif + +namespace CommunityToolkit.Notifications +{ + internal abstract class BaseElement + { + /// + /// Retrieves the notification XML content as a string. + /// + /// The notification XML content as a string. + public string GetContent() + { + using (MemoryStream stream = new MemoryStream()) + { + using (System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(stream, new System.Xml.XmlWriterSettings() + { + Encoding = Encoding.UTF8, // Use UTF-8 encoding to save space (it defaults to UTF-16 which is 2x the size) + Indent = false, + NewLineOnAttributes = false + })) + { + XmlWriterHelper.Write(writer, this); + } + + stream.Position = 0; + + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + +#if WINDOWS_UWP + /// + /// Retrieves the notification XML content as a WinRT XML document. + /// + /// The notification XML content as a WinRT XML document. + public XmlDocument GetXml() + { + XmlDocument xml = new XmlDocument(); + xml.LoadXml(GetContent()); + return xml; + } +#endif + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/EnumFormatter.cs b/components/Notifications/src/Common/EnumFormatter.cs new file mode 100644 index 000000000..117a98a4f --- /dev/null +++ b/components/Notifications/src/Common/EnumFormatter.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +#nullable enable + +namespace CommunityToolkit.Notifications +{ + /// + /// A helper class that can be used to format values. + /// + internal static class EnumFormatter + { + /// + /// Returns a representation of an enum value with pascal casing. + /// + /// The type to format. + /// The value to format. + /// The pascal case representation of . + public static string? ToPascalCaseString(this T? value) + where T : unmanaged, Enum + { + if (value is null) + { + return null; + } + + return ToPascalCaseString(value.Value); + } + + /// + /// Returns a representation of an enum value with pascal casing. + /// + /// The type to format. + /// The value to format. + /// The pascal case representation of . + public static string? ToPascalCaseString(this T value) + where T : unmanaged, Enum + { + string? text = value.ToString(); + + if (text is null or { Length: 0 }) + { + return text; + } + + if (text is { Length: 1 }) + { + return text.ToLowerInvariant(); + } + + return $"{char.ToLowerInvariant(text[0])}{text.Substring(1)}"; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/INotificationContent.cs b/components/Notifications/src/Common/INotificationContent.cs new file mode 100644 index 000000000..a025624aa --- /dev/null +++ b/components/Notifications/src/Common/INotificationContent.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP +using Windows.Data.Xml.Dom; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Base notification content interface to retrieve notification Xml as a string. + /// + public interface INotificationContent + { + /// + /// Retrieves the notification Xml content as a string. + /// + /// The notification Xml content as a string. + string GetContent(); + +#if WINDOWS_UWP + /// + /// Retrieves the notification Xml content as a WinRT Xml document. + /// + /// The notification Xml content as a WinRT Xml document. + XmlDocument GetXml(); +#endif + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/LimitedList{T}.cs b/components/Notifications/src/Common/LimitedList{T}.cs new file mode 100644 index 000000000..f8d6045de --- /dev/null +++ b/components/Notifications/src/Common/LimitedList{T}.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +#if WINDOWS_UWP + +#endif + +namespace CommunityToolkit.Notifications +{ + internal sealed class LimitedList : IList + { + private List _list; + + public int Limit { get; private set; } + + public LimitedList(int limit) + { + _list = new List(limit); + + Limit = limit; + } + + public T this[int index] + { + get + { + return _list[index]; + } + + set + { + _list[index] = value; + } + } + + public int Count + { + get + { + return _list.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public void Add(T item) + { + if (_list.Count >= Limit) + { + throw new Exception("This list is limited to " + Limit + " items. You cannot add more items."); + } + + _list.Add(item); + } + + public void Clear() + { + _list.Clear(); + } + + public bool Contains(T item) + { + return _list.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + public int IndexOf(T item) + { + return _list.IndexOf(item); + } + + public void Insert(int index, T item) + { + _list.Insert(index, item); + } + + public bool Remove(T item) + { + return _list.Remove(item); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + internal interface IElementWithDescendants + { + IEnumerable Descendants(); + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/NotificationContentValidationException.cs b/components/Notifications/src/Common/NotificationContentValidationException.cs new file mode 100644 index 000000000..0aaf1e0ba --- /dev/null +++ b/components/Notifications/src/Common/NotificationContentValidationException.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +#if WINDOWS_UWP + +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Exception returned when invalid notification content is provided. + /// + internal sealed class NotificationContentValidationException : Exception + { + public NotificationContentValidationException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs b/components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs new file mode 100644 index 000000000..757d1f5d7 --- /dev/null +++ b/components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +#nullable enable + +namespace CommunityToolkit.Notifications +{ + /// + /// An interface for a notification XML element with additional properties. + /// + internal interface IHaveXmlAdditionalProperties + { + /// + /// Gets the mapping of additional properties. + /// + IReadOnlyDictionary AdditionalProperties { get; } + } +} diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs b/components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs new file mode 100644 index 000000000..12723a323 --- /dev/null +++ b/components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +#nullable enable + +namespace CommunityToolkit.Notifications +{ + /// + /// An interface for a notification XML element with additional children. + /// + internal interface IHaveXmlChildren + { + /// + /// Gets the children of the current element. + /// + IEnumerable Children { get; } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlName.cs b/components/Notifications/src/Common/Serialization/IHaveXmlName.cs new file mode 100644 index 000000000..f9e96c99d --- /dev/null +++ b/components/Notifications/src/Common/Serialization/IHaveXmlName.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// An interface for a notification XML element with a name. + /// + internal interface IHaveXmlName + { + /// + /// Gets the name of the current element. + /// + string Name { get; } + } +} diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs b/components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs new file mode 100644 index 000000000..29c7e17b5 --- /dev/null +++ b/components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +#nullable enable + +namespace CommunityToolkit.Notifications +{ + /// + /// An interface for a notification XML element with named properties. + /// + internal interface IHaveXmlNamedProperties + { + /// + /// Enumerates the available named properties for the element. + /// + /// A sequence of named properties for the element. + /// The returned values must be valid XML values when is called on them. + IEnumerable> EnumerateNamedProperties(); + } +} diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlText.cs b/components/Notifications/src/Common/Serialization/IHaveXmlText.cs new file mode 100644 index 000000000..358afa31b --- /dev/null +++ b/components/Notifications/src/Common/Serialization/IHaveXmlText.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// An interface for a notification XML element with an explicit XML text content. + /// + internal interface IHaveXmlText + { + /// + /// Gets the text content of the current element. + /// + string Text { get; } + } +} diff --git a/components/Notifications/src/Common/XmlWriterHelper.cs b/components/Notifications/src/Common/XmlWriterHelper.cs new file mode 100644 index 000000000..6111d2bda --- /dev/null +++ b/components/Notifications/src/Common/XmlWriterHelper.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace CommunityToolkit.Notifications +{ + internal static class XmlWriterHelper + { + public static void Write(XmlWriter writer, object element) + { + // If it isn't an XML element, don't write anything + if (element is not IHaveXmlName xmlElement) + { + return; + } + + writer.WriteStartElement(xmlElement.Name); + + // Write all named properties + foreach (var property in (element as IHaveXmlNamedProperties)?.EnumerateNamedProperties() ?? Enumerable.Empty>()) + { + if (property.Value is not null) + { + writer.WriteAttributeString(property.Key, PropertyValueToString(property.Value)); + } + } + + // Write all additional properties + foreach (var property in (element as IHaveXmlAdditionalProperties)?.AdditionalProperties ?? Enumerable.Empty>()) + { + writer.WriteAttributeString(property.Key, property.Value); + } + + // Write the inner text, if any + if ((element as IHaveXmlText)?.Text is string { Length: > 0 } text) + { + writer.WriteString(text); + } + + // Write all children, if any + foreach (var child in (element as IHaveXmlChildren)?.Children ?? Enumerable.Empty()) + { + Write(writer, child); + } + + writer.WriteEndElement(); + } + + private static string PropertyValueToString(object propertyValue) + { + return propertyValue switch + { + true => "true", + false => "false", + DateTimeOffset dateTime => XmlConvert.ToString(dateTime), // ISO 8601 format + { } value => value.ToString(), + _ => null + }; + } + + /// + /// Gets the provided binding value, if it exists. Otherwise, falls back to the absolute value. + /// + /// The type of the enum of the class properties. + /// The collection of data-bound values. + /// The property to obtain. + /// The absolute value, if any. + /// The provided binding value, if it exists. Otherwise, falls back to the absolute value. + internal static string GetBindingOrAbsoluteXmlValue(IDictionary bindings, T bindableProperty, string absoluteValue) + { + // If a binding is provided, use the binding value + string bindingValue; + if (bindings.TryGetValue(bindableProperty, out bindingValue)) + { + return "{" + bindingValue + "}"; + } + + // Otherwise fallback to the absolute value + return absoluteValue; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj new file mode 100644 index 000000000..4112bc6c6 --- /dev/null +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -0,0 +1,21 @@ + + + + + Notifications + + The official way to send toast notifications on Windows 10 via code rather than XML, with the help of IntelliSense. Supports all C# app types, including WPF, UWP, WinForms, and Console, even without packaging your app as MSIX. Also supports C++ UWP apps. + + Additionally, generate notification payloads from your ASP.NET web server to send as push notifications, or generate notification payloads from class libraries. + + For UWP/MSIX apps, you can also generate tile and badge notifications. + + CommunityToolkit.Notifications + $(PackageIdPrefix).$(ToolkitComponentName) + disable + false + + + + + diff --git a/components/Notifications/src/Dependencies.props b/components/Notifications/src/Dependencies.props new file mode 100644 index 000000000..e622e1df4 --- /dev/null +++ b/components/Notifications/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Notifications/src/MultiTarget.props b/components/Notifications/src/MultiTarget.props new file mode 100644 index 000000000..e63031231 --- /dev/null +++ b/components/Notifications/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp; + + \ No newline at end of file diff --git a/components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs b/components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs new file mode 100644 index 000000000..216ce096b --- /dev/null +++ b/components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CommunityToolkit.Notifications +{ +#if !WINRT +#pragma warning disable SA1008 +#pragma warning disable SA1009 + /// + /// Builder class used to create + /// + public partial class TileContentBuilder + { + /// + /// Helper method for creating a tile notification content for using Contact tile template. + /// + /// Source for the contact picture + /// Name of the contact + /// A description of the contact image, for users of assistive technologies. + /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification. + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentContact CreateContactTileContent(Uri contactImageUri, string contactName, string contactImageAltText = default(string), bool? contactImageAddImageQuery = default(bool?), string textLanguage = default(string)) + { + var contactTileContent = new TileBindingContentContact(); + contactTileContent.Image = CreateTileBasicImage(contactImageUri, contactImageAltText, contactImageAddImageQuery); + + contactTileContent.Text = new TileBasicText(); + contactTileContent.Text.Text = contactName; + + if (textLanguage != default(string)) + { + contactTileContent.Text.Lang = textLanguage; + } + + return contactTileContent; + } + + /// + /// Helper method for creating a tile notification content for using Iconic tile template. + /// + /// Source of the icon image. + /// A description of the icon image, for users of assistive technologies. + /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentIconic CreateIconicTileContent(Uri iconImageUri, string iconImageAltText = default(string), bool? iconImageAddImageQuery = default(bool?)) + { + var iconicTileContent = new TileBindingContentIconic(); + iconicTileContent.Icon = CreateTileBasicImage(iconImageUri, iconImageAltText, iconImageAddImageQuery); + + return iconicTileContent; + } + + /// + /// Helper method for creating a tile notification content for using People tile template. + /// + /// Sources of pictures that will be used on the people tile. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentPeople CreatePeopleTileContent(params Uri[] peoplePictureSources) + { + IEnumerable images = peoplePictureSources.Select(u => CreateTileBasicImage(u, default(string), default(bool?))); + + return CreatePeopleTileContent(images); + } + + /// + /// Helper method for creating a tile notification content for using People tile template. + /// + /// Sources of pictures with description and image query indicator that will be used on the people tile. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentPeople CreatePeopleTileContent(params (Uri source, string imageAltText, bool? addImageQuery)[] peoplePictures) + { + IEnumerable images = peoplePictures.Select(t => CreateTileBasicImage(t.source, t.imageAltText, t.addImageQuery)); + + return CreatePeopleTileContent(images); + } + + /// + /// Helper method for creating a tile notification content for using People tile template. + /// + /// Pictures that will be used on the people tile. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentPeople CreatePeopleTileContent(IEnumerable peoplePictures) + { + var peopleTileContent = new TileBindingContentPeople(); + + foreach (var image in peoplePictures) + { + peopleTileContent.Images.Add(image); + } + + return peopleTileContent; + } + + /// + /// Helper method for creating a tile notification content for using Photos tile template. + /// + /// Sources of pictures that will be used on the photos tile. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentPhotos CreatePhotosTileContent(params Uri[] photoSources) + { + IEnumerable images = photoSources.Select(u => CreateTileBasicImage(u, default(string), default(bool?))); + + return CreatePhotosTileContent(images); + } + + /// + /// Helper method for creating a tile notification content for using Photos tile template. + /// + /// Sources of pictures with description and addImageQuery indicator that will be used for the photos tile. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentPhotos CreatePhotosTileContent(params (Uri source, string imageAltText, bool? addImageQuery)[] photos) + { + IEnumerable images = photos.Select(t => CreateTileBasicImage(t.source, t.imageAltText, t.addImageQuery)); + + return CreatePhotosTileContent(images); + } + + /// + /// Helper method for creating a tile notification content for using Photos tile template. + /// + /// Pictures that will be used for the photos tile. + /// An instance of represent a payload of a tile notification. + public static TileBindingContentPhotos CreatePhotosTileContent(IEnumerable photos) + { + var photoTileContent = new TileBindingContentPhotos(); + + foreach (var image in photos) + { + photoTileContent.Images.Add(image); + } + + return photoTileContent; + } + + private static TileBasicImage CreateTileBasicImage(Uri imageUri, string imageAltText, bool? addImageQuery) + { + var tileImage = new TileBasicImage(); + tileImage.Source = imageUri.OriginalString; + + if (imageAltText != default(string)) + { + tileImage.AlternateText = imageAltText; + } + + if (addImageQuery != default(bool?)) + { + tileImage.AddImageQuery = addImageQuery; + } + + return tileImage; + } + } +#pragma warning restore SA1008 +#pragma warning restore SA1009 +#endif +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/Builder/TileContentBuilder.cs b/components/Notifications/src/Tiles/Builder/TileContentBuilder.cs new file mode 100644 index 000000000..f6c932c3d --- /dev/null +++ b/components/Notifications/src/Tiles/Builder/TileContentBuilder.cs @@ -0,0 +1,531 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ +#if !WINRT + + /// + /// Builder class used to create + /// + public partial class TileContentBuilder + { + /// + /// Flag used to create all tile size (Small , Medium, Large and Wide) + /// + public const TileSize AllSize = TileSize.Small | TileSize.Medium | TileSize.Large | TileSize.Wide; + + /// + /// Gets internal instance of . This is equivalent to the call to . + /// + public TileContent Content + { + get; private set; + } + + private TileVisual Visual + { + get + { + if (Content.Visual == null) + { + Content.Visual = new TileVisual(); + } + + return Content.Visual; + } + } + + private TileBinding SmallTile + { + get + { + return Visual.TileSmall; + } + + set + { + Visual.TileSmall = value; + } + } + + private TileBinding MediumTile + { + get + { + return Visual.TileMedium; + } + + set + { + Visual.TileMedium = value; + } + } + + private TileBinding WideTile + { + get + { + return Visual.TileWide; + } + + set + { + Visual.TileWide = value; + } + } + + private TileBinding LargeTile + { + get + { + return Visual.TileLarge; + } + + set + { + Visual.TileLarge = value; + } + } + + /// + /// Initializes a new instance of the class. + /// + public TileContentBuilder() + { + Content = new TileContent(); + } + + /// + /// Add a tile layout size that the notification will be displayed on. + /// + /// The size of tile that the notification will be displayed on. + /// Specialized tile content. Use for special tile template. Default to NULL. + /// The current instance of + public TileContentBuilder AddTile(TileSize size, ITileBindingContent tileContent = null) + { + if (size.HasFlag(TileSize.Small)) + { + SmallTile = new TileBinding(); + SmallTile.Content = tileContent ?? new TileBindingContentAdaptive(); + } + + if (size.HasFlag(TileSize.Medium)) + { + MediumTile = new TileBinding(); + MediumTile.Content = tileContent ?? new TileBindingContentAdaptive(); + } + + if (size.HasFlag(TileSize.Wide)) + { + WideTile = new TileBinding(); + WideTile.Content = tileContent ?? new TileBindingContentAdaptive(); + } + + if (size.HasFlag(TileSize.Large)) + { + LargeTile = new TileBinding(); + LargeTile.Content = tileContent ?? new TileBindingContentAdaptive(); + } + + return this; + } + + /// + /// Set how the tile notification should display the application branding. + /// + /// How branding should appear on the tile + /// The tile size that the parameter should be applied to. Default to all currently supported tile size. + /// The current instance of + public TileContentBuilder SetBranding(TileBranding branding, TileSize size = AllSize) + { + if (size == AllSize) + { + // Set on visual. + Visual.Branding = branding; + } + else + { + if (size.HasFlag(TileSize.Small) && SmallTile != null) + { + SmallTile.Branding = branding; + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null) + { + MediumTile.Branding = branding; + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null) + { + WideTile.Branding = branding; + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null) + { + LargeTile.Branding = branding; + } + } + + return this; + } + + /// + /// Set the name that will be used to override the application's name on the tile notification. + /// + /// Custom name to display on the tile in place of the application's name + /// The tile size that parameter should be applied to. Default to all currently supported tile size. + /// The current instance of + public TileContentBuilder SetDisplayName(string displayName, TileSize size = AllSize) + { + if (size == AllSize) + { + // Set on visual. + Visual.DisplayName = displayName; + } + else + { + if (size.HasFlag(TileSize.Small) && SmallTile != null) + { + SmallTile.DisplayName = displayName; + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null) + { + MediumTile.DisplayName = displayName; + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null) + { + WideTile.DisplayName = displayName; + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null) + { + LargeTile.DisplayName = displayName; + } + } + + return this; + } + + /// + /// Set the optional background image that stays behind the tile notification. + /// + /// Source of the background image + /// The tile size that the background image should be applied to. Default to all currently supported tile size. + /// Description of the background image, for user of assistance technology + /// + /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification. + /// Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. + /// This query string specifies scale, contrast setting, and language. + /// + /// The opacity of the black overlay on the background image. + /// Desired cropping of the image. + /// The current instance of + public TileContentBuilder SetBackgroundImage(Uri imageUri, TileSize size = AllSize, string alternateText = default(string), bool? addImageQuery = default(bool?), int? hintOverlay = default(int?), TileBackgroundImageCrop hintCrop = TileBackgroundImageCrop.Default) + { + TileBackgroundImage backgroundImage = new TileBackgroundImage(); + backgroundImage.Source = imageUri.OriginalString; + backgroundImage.HintCrop = hintCrop; + + if (alternateText != default(string)) + { + backgroundImage.AlternateText = alternateText; + } + + if (addImageQuery != default(bool?)) + { + backgroundImage.AddImageQuery = addImageQuery; + } + + if (hintOverlay != default(int?)) + { + backgroundImage.HintOverlay = hintOverlay; + } + + return SetBackgroundImage(backgroundImage, size); + } + + /// + /// Set the optional background image that stays behind the tile notification. + /// + /// An instance of as the background image for the tile. + /// The tile size that the background image should be applied to. Default to all currently supported tile size. + /// The current instance of + public TileContentBuilder SetBackgroundImage(TileBackgroundImage backgroundImage, TileSize size = AllSize) + { + // Set to any available tile at the moment of calling. + if (size.HasFlag(TileSize.Small) && SmallTile != null) + { + GetAdaptiveTileContent(SmallTile).BackgroundImage = backgroundImage; + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null) + { + GetAdaptiveTileContent(MediumTile).BackgroundImage = backgroundImage; + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null) + { + GetAdaptiveTileContent(WideTile).BackgroundImage = backgroundImage; + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null) + { + GetAdaptiveTileContent(LargeTile).BackgroundImage = backgroundImage; + } + + return this; + } + + /// + /// Set the Tile's Peek Image that animate from the top of the tile notification. + /// + /// Source of the peek image + /// The tile size that the peek image should be applied to. Default to all currently supported tile size. + /// Description of the peek image, for user of assistance technology + /// + /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification. + /// Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. + /// This query string specifies scale, contrast setting, and language. + /// + /// The opacity of the black overlay on the peek image. + /// Desired cropping of the image. + /// The current instance of + public TileContentBuilder SetPeekImage(Uri imageUri, TileSize size = AllSize, string alternateText = default(string), bool? addImageQuery = default(bool?), int? hintOverlay = default(int?), TilePeekImageCrop hintCrop = TilePeekImageCrop.Default) + { + TilePeekImage peekImage = new TilePeekImage(); + peekImage.Source = imageUri.OriginalString; + peekImage.HintCrop = hintCrop; + + if (alternateText != default(string)) + { + peekImage.AlternateText = alternateText; + } + + if (addImageQuery != default(bool?)) + { + peekImage.AddImageQuery = addImageQuery; + } + + if (hintOverlay != default(int?)) + { + peekImage.HintOverlay = hintOverlay; + } + + return SetPeekImage(peekImage, size); + } + + /// + /// Set the Tile's Peek Image that animate from the top of the tile notification. + /// + /// An instance of for the Tile's peek image + /// The tile size that the peek image should be applied to. Default to all currently supported tile size. + /// The current instance of + public TileContentBuilder SetPeekImage(TilePeekImage peekImage, TileSize size = AllSize) + { + // Set to any available tile at the moment of calling. + if (size.HasFlag(TileSize.Small) && SmallTile != null) + { + GetAdaptiveTileContent(SmallTile).PeekImage = peekImage; + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null) + { + GetAdaptiveTileContent(MediumTile).PeekImage = peekImage; + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null) + { + GetAdaptiveTileContent(WideTile).PeekImage = peekImage; + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null) + { + GetAdaptiveTileContent(LargeTile).PeekImage = peekImage; + } + + return this; + } + + /// + /// Set the text stacking (vertical alignment) of the entire binding element. + /// + /// Text Stacking Option + /// The tile size that the peek image should be applied to. Default to all currently supported tile size. + /// The current instance of + public TileContentBuilder SetTextStacking(TileTextStacking textStacking, TileSize size = AllSize) + { + // Set to any available tile at the moment of calling. + if (size.HasFlag(TileSize.Small) && SmallTile != null) + { + GetAdaptiveTileContent(SmallTile).TextStacking = textStacking; + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null) + { + GetAdaptiveTileContent(MediumTile).TextStacking = textStacking; + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null) + { + GetAdaptiveTileContent(WideTile).TextStacking = textStacking; + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null) + { + GetAdaptiveTileContent(LargeTile).TextStacking = textStacking; + } + + return this; + } + + /// + /// Set the tile's activation arguments for tile notification. + /// + /// App-Defined custom arguments that will be passed in when the user click on the tile when this tile notification is being displayed. + /// The tile size that the custom argument should be applied to. Default to all currently supported tile size. + /// The current instance of + public TileContentBuilder SetActivationArgument(string args, TileSize size = AllSize) + { + if (size == AllSize) + { + Visual.Arguments = args; + } + else + { + if (size.HasFlag(TileSize.Small) && SmallTile != null) + { + SmallTile.Arguments = args; + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null) + { + MediumTile.Arguments = args; + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null) + { + WideTile.Arguments = args; + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null) + { + LargeTile.Arguments = args; + } + } + + return this; + } + + /// + /// Add a custom text that will appear on the tile notification. + /// + /// Custom text to display on the tile. + /// The tile size that the custom text would be added to. Default to all currently supported tile size. + /// Style that controls the text's font size, weight, and opacity. + /// Indicating whether text wrapping is enabled. For Tiles, this is false by default. + /// The maximum number of lines the text element is allowed to display. For Tiles, this is infinity by default + /// The minimum number of lines the text element must display. + /// The horizontal alignment of the text + /// + /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. + /// + /// The current instance of + public TileContentBuilder AddText(string text, TileSize size = AllSize, AdaptiveTextStyle? hintStyle = null, bool? hintWrap = default(bool?), int? hintMaxLines = default(int?), int? hintMinLines = default(int?), AdaptiveTextAlign? hintAlign = null, string language = default(string)) + { + // Create the adaptive text. + AdaptiveText adaptive = new AdaptiveText() + { + Text = text + }; + + if (hintStyle != null) + { + adaptive.HintStyle = hintStyle.Value; + } + + if (hintAlign != null) + { + adaptive.HintAlign = hintAlign.Value; + } + + if (hintWrap != default(bool?)) + { + adaptive.HintWrap = hintWrap; + } + + if (hintMaxLines != default(int?)) + { + adaptive.HintMaxLines = hintMaxLines; + } + + if (hintMinLines != default(int?) && hintMinLines > 0) + { + adaptive.HintMinLines = hintMinLines; + } + + if (language != default(string)) + { + adaptive.Language = language; + } + + // Add to the tile content + return AddAdaptiveTileVisualChild(adaptive, size); + } + + /// + /// Add an adaptive child to the tile notification. + /// + /// An adaptive child to add + /// Tile size that the adaptive child should be added to. Default to all currently supported tile size. + /// The current instance of + /// + /// This can be used to add Group and Subgroup to the tile. + /// + public TileContentBuilder AddAdaptiveTileVisualChild(ITileBindingContentAdaptiveChild child, TileSize size = AllSize) + { + if (size.HasFlag(TileSize.Small) && SmallTile != null && GetAdaptiveTileContent(SmallTile) != null) + { + GetAdaptiveTileContent(SmallTile).Children.Add(child); + } + + if (size.HasFlag(TileSize.Medium) && MediumTile != null && GetAdaptiveTileContent(MediumTile) != null) + { + GetAdaptiveTileContent(MediumTile).Children.Add(child); + } + + if (size.HasFlag(TileSize.Wide) && WideTile != null && GetAdaptiveTileContent(WideTile) != null) + { + GetAdaptiveTileContent(WideTile).Children.Add(child); + } + + if (size.HasFlag(TileSize.Large) && LargeTile != null && GetAdaptiveTileContent(LargeTile) != null) + { + GetAdaptiveTileContent(LargeTile).Children.Add(child); + } + + return this; + } + + /// + /// Get the instance of that has been built by the builder with specified configuration so far. + /// + /// An instance of that can be used to create tile notification. + public TileContent GetTileContent() + { + return Content; + } + + private TileBindingContentAdaptive GetAdaptiveTileContent(TileBinding binding) + { + return binding.Content as TileBindingContentAdaptive; + } + } + +#endif +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/Elements/Element_Tile.cs b/components/Notifications/src/Tiles/Elements/Element_Tile.cs new file mode 100644 index 000000000..de1db439c --- /dev/null +++ b/components/Notifications/src/Tiles/Elements/Element_Tile.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_Tile : BaseElement, IHaveXmlName, IHaveXmlChildren + { + public Element_TileVisual Visual { get; set; } + + /// + string IHaveXmlName.Name => "tile"; + + /// + IEnumerable IHaveXmlChildren.Children => new[] { Visual }; + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/Elements/Element_TileBinding.cs b/components/Notifications/src/Tiles/Elements/Element_TileBinding.cs new file mode 100644 index 000000000..1125354e7 --- /dev/null +++ b/components/Notifications/src/Tiles/Elements/Element_TileBinding.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_TileBinding : IElementWithDescendants, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + internal const TileBranding DEFAULT_BRANDING = TileBranding.Auto; + internal const TileTextStacking DEFAULT_TEXT_STACKING = TileTextStacking.Top; + internal const int DEFAULT_OVERLAY = 20; + + public Element_TileBinding(TileTemplateNameV3 template) + { + Template = template; + } + + public TileTemplateNameV3 Template { get; private set; } + + /// + /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of + /// + /// "www.website.com/images/hello.png" + /// + /// included in the notification becomes + /// + /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us" + /// + public bool? AddImageQuery { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets the form that the Tile should use to display the app's brand. + /// + public TileBranding Branding { get; set; } = DEFAULT_BRANDING; + + /// + /// Gets or sets a sender-defined string that uniquely identifies the content of the notification. This prevents duplicates in the situation where a large Tile template is displaying the last three wide Tile notifications. + /// + /// Required: NO + /// + public string ContentId { get; set; } + + /// + /// Gets or sets an optional string to override the Tile's display name while showing this notification. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides that in visual, but can be overridden by that in text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. See Remarks for when this value isn't specified. + /// + public string Language { get; set; } + + public string LockDetailedStatus1 { get; set; } + + public string LockDetailedStatus2 { get; set; } + + public string LockDetailedStatus3 { get; set; } + + public string Arguments { get; set; } + + /// + /// Throws exception if value is invalid + /// + /// Overlay value (0-100) + internal static void CheckOverlayValue(int value) + { + if (value < 0 || value > 100) + { + throw new ArgumentOutOfRangeException("Overlay must be between 0 and 100, inclusive."); + } + } + + public TilePresentation? Presentation { get; set; } + + public TileTextStacking TextStacking { get; set; } = DEFAULT_TEXT_STACKING; + + public IList Children { get; private set; } = new List(); + + /// + /// Generates an enumerable collection of children and all those children's children + /// + /// Enumerable collection of children and all those children's children. + public IEnumerable Descendants() + { + foreach (IElement_TileBindingChild child in Children) + { + // Return the child + yield return child; + + // And if it has descendants, return the descendants + if (child is IElementWithDescendants) + { + foreach (object descendant in (child as IElementWithDescendants).Descendants()) + { + yield return descendant; + } + } + } + } + + /// + string IHaveXmlName.Name => "binding"; + + /// + IEnumerable IHaveXmlChildren.Children => Children; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("template", Template); + yield return new("addImageQuery", AddImageQuery); + yield return new("baseUri", BaseUri); + + if (Branding != DEFAULT_BRANDING) + { + yield return new("branding", Branding); + } + + yield return new("contentId", ContentId); + yield return new("displayName", DisplayName); + yield return new("lang", Language); + yield return new("hint-lockDetailedStatus1", LockDetailedStatus1); + yield return new("hint-lockDetailedStatus2", LockDetailedStatus2); + yield return new("hint-lockDetailedStatus3", LockDetailedStatus3); + yield return new("arguments", Arguments); + yield return new("hint-presentation", Presentation.ToPascalCaseString()); + + if (TextStacking != DEFAULT_TEXT_STACKING) + { + yield return new("hint-textStacking", TextStacking.ToPascalCaseString()); + } + } + } + + internal interface IElement_TileBindingChild + { + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/Elements/Element_TileVisual.cs b/components/Notifications/src/Tiles/Elements/Element_TileVisual.cs new file mode 100644 index 000000000..d4a59a30f --- /dev/null +++ b/components/Notifications/src/Tiles/Elements/Element_TileVisual.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_TileVisual : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + internal const TileBranding DEFAULT_BRANDING = TileBranding.Auto; + internal const bool DEFAULT_ADD_IMAGE_QUERY = false; + + public bool? AddImageQuery { get; set; } + + public Uri BaseUri { get; set; } + + public TileBranding Branding { get; set; } = DEFAULT_BRANDING; + + public string ContentId { get; set; } + + public string DisplayName { get; set; } + + public string Language { get; set; } + + public string Arguments { get; set; } + + public IList Bindings { get; private set; } = new List(); + + /// + string IHaveXmlName.Name => "visual"; + + /// + IEnumerable IHaveXmlChildren.Children => Bindings; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("addImageQuery", AddImageQuery); + yield return new("baseUri", BaseUri); + + if (Branding != DEFAULT_BRANDING) + { + yield return new("branding", Branding.ToPascalCaseString()); + } + + yield return new("contentId", ContentId); + yield return new("displayName", DisplayName); + yield return new("lang", Language); + yield return new("arguments", Arguments); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/Elements/TileElementsCommon.cs b/components/Notifications/src/Tiles/Elements/TileElementsCommon.cs new file mode 100644 index 000000000..f5b808fdd --- /dev/null +++ b/components/Notifications/src/Tiles/Elements/TileElementsCommon.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + internal enum TilePresentation + { + People, + Photos, + Contact + } + + internal enum TileImagePlacement + { + Inline, + Background, + Peek + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs b/components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs new file mode 100644 index 000000000..5c9ad93e8 --- /dev/null +++ b/components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Elements that can be direct children of , including (, , and ). + /// + public interface ITileBindingContentAdaptiveChild + { + // Blank interface simply for compile-enforcing the child types in the list. + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs new file mode 100644 index 000000000..865bb849e --- /dev/null +++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Phone-only. Supported on Small, Medium, and Wide. + /// + public sealed class TileBindingContentContact : ITileBindingContent + { + /// + /// Gets or sets the image to display. + /// + public TileBasicImage Image { get; set; } + + /// + /// Gets or sets a line of text that is displayed. Not displayed on Small Tile. + /// + public TileBasicText Text { get; set; } + + internal TileTemplateNameV3 GetTemplateName(TileSize size) + { + return TileSizeToAdaptiveTemplateConverter.Convert(size); + } + + internal void PopulateElement(Element_TileBinding binding, TileSize size) + { + binding.Presentation = TilePresentation.Contact; + + // Small size doesn't display the text, so no reason to include it in the payload + if (Text != null && size != TileSize.Small) + { + binding.Children.Add(Text.ConvertToElement()); + } + + if (Image != null) + { + binding.Children.Add(Image.ConvertToElement()); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs new file mode 100644 index 000000000..686278736 --- /dev/null +++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Supported on Small and Medium. Enables an iconic Tile template, where you can have an icon and badge display next to each other on the Tile, in true classic Windows Phone style. The number next to the icon is achieved through a separate badge notification. + /// + public sealed class TileBindingContentIconic : ITileBindingContent + { + /// + /// Gets or sets, at minimum, to support both Desktop and Phone, Small and Medium tiles, a square aspect ratio image with a resolution of 200x200, PNG format, with transparency and no color other than white. For more info see: http://blogs.msdn.com/b/tiles_and_toasts/archive/2015/07/31/iconic-tile-template-for-windows-10.aspx + /// + public TileBasicImage Icon { get; set; } + + internal TileTemplateNameV3 GetTemplateName(TileSize size) + { + switch (size) + { + case TileSize.Small: + return TileTemplateNameV3.TileSquare71x71IconWithBadge; + + case TileSize.Medium: + return TileTemplateNameV3.TileSquare150x150IconWithBadge; + + default: + throw new ArgumentException("The Iconic template is only supported on Small and Medium tiles."); + } + } + + internal void PopulateElement(Element_TileBinding binding, TileSize size) + { + if (Icon != null) + { + var element = Icon.ConvertToElement(); + element.Id = 1; + binding.Children.Add(element); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs new file mode 100644 index 000000000..4f5fdcd42 --- /dev/null +++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// New in 1511: Supported on Medium, Wide, and Large (Desktop and Mobile). + /// Previously for RTM: Phone-only. Supported on Medium and Wide. + /// + public sealed class TileBindingContentPeople : ITileBindingContent + { + /// + /// Gets images that will roll around as circles. + /// + public IList Images { get; private set; } = new List(); + + internal TileTemplateNameV3 GetTemplateName(TileSize size) + { + return TileSizeToAdaptiveTemplateConverter.Convert(size); + } + + internal void PopulateElement(Element_TileBinding binding, TileSize size) + { + binding.Presentation = TilePresentation.People; + + foreach (var img in Images) + { + binding.Children.Add(img.ConvertToElement()); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs new file mode 100644 index 000000000..baa5de892 --- /dev/null +++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// Animates through a slide show of photos. Supported on all sizes. + /// + public sealed class TileBindingContentPhotos : ITileBindingContent + { + /// + /// Gets the collection of slide show images. Up to 12 images can be provided (Mobile will only display up to 9), which will be used for the slide show. Adding more than 12 will throw an exception. + /// + public IList Images { get; private set; } = new LimitedList(12); + + internal TileTemplateNameV3 GetTemplateName(TileSize size) + { + return TileSizeToAdaptiveTemplateConverter.Convert(size); + } + + internal void PopulateElement(Element_TileBinding binding, TileSize size) + { + binding.Presentation = TilePresentation.Photos; + + foreach (var img in Images) + { + binding.Children.Add(img.ConvertToElement()); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileBackgroundImage.cs b/components/Notifications/src/Tiles/TileBackgroundImage.cs new file mode 100644 index 000000000..b326d2825 --- /dev/null +++ b/components/Notifications/src/Tiles/TileBackgroundImage.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// A full-bleed background image that appears beneath the Tile content. + /// + public sealed class TileBackgroundImage : IBaseImage + { + private string _source; + + /// + /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + private int? _hintOverlay; + + /// + /// Gets or sets a black overlay on the background image. This value controls the opacity of the black overlay, with 0 being no overlay and 100 being completely black. Defaults to 20. + /// + public int? HintOverlay + { + get + { + return _hintOverlay; + } + + set + { + if (value != null) + { + Element_TileBinding.CheckOverlayValue(value.Value); + } + + _hintOverlay = value; + } + } + + /// + /// Gets or sets the desired cropping of the image. + /// Previously for RTM: Did not exist, value will be ignored and background image will be displayed without any cropping. + /// + public TileBackgroundImageCrop HintCrop { get; set; } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this); + + image.Placement = AdaptiveImagePlacement.Background; + image.Crop = GetAdaptiveImageCrop(); + image.Overlay = HintOverlay; + + return image; + } + + private AdaptiveImageCrop GetAdaptiveImageCrop() + { + switch (HintCrop) + { + case TileBackgroundImageCrop.Circle: + return AdaptiveImageCrop.Circle; + + case TileBackgroundImageCrop.None: + return AdaptiveImageCrop.None; + + default: + return AdaptiveImageCrop.Default; + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileBasicImage.cs b/components/Notifications/src/Tiles/TileBasicImage.cs new file mode 100644 index 000000000..c3b57b815 --- /dev/null +++ b/components/Notifications/src/Tiles/TileBasicImage.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// An image used on various special templates for the Tile. + /// + public sealed class TileBasicImage : IBaseImage + { + private string _source; + + /// + /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this); + + return image; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileBasicText.cs b/components/Notifications/src/Tiles/TileBasicText.cs new file mode 100644 index 000000000..465d1dd44 --- /dev/null +++ b/components/Notifications/src/Tiles/TileBasicText.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// A text element on the Tile. + /// + public sealed class TileBasicText + { + /// + /// Gets or sets the text value that will be shown in the text field. + /// + public string Text { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Lang { get; set; } + + internal Element_AdaptiveText ConvertToElement() + { + return new Element_AdaptiveText() + { + Text = Text, + Lang = Lang + }; + } + + /// + /// Returns the Text property's value. + /// + /// The Text property's value. + public override string ToString() + { + return Text; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileBinding.cs b/components/Notifications/src/Tiles/TileBinding.cs new file mode 100644 index 000000000..1290f47b2 --- /dev/null +++ b/components/Notifications/src/Tiles/TileBinding.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// The binding element contains the visual content for a specific Tile size. + /// + public sealed class TileBinding + { + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides that in visual, but can be overridden by that in text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. See Remarks for when this value isn't specified. + /// + public string Language { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. Defaults to null. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets the form that the Tile should use to display the app's brand.. + /// + public TileBranding Branding { get; set; } = Element_TileBinding.DEFAULT_BRANDING; + + /// + /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of + /// + /// "www.website.com/images/hello.png" + /// + /// included in the notification becomes + /// + /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us" + /// + public bool? AddImageQuery { get; set; } + + /// + /// Gets or sets a sender-defined string that uniquely identifies the content of the notification. This prevents duplicates in the situation where a large Tile template is displaying the last three wide Tile notifications. + /// + public string ContentId { get; set; } + + /// + /// Gets or sets an optional string to override the Tile's display name while showing this notification. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets an app-defined data that is passed back to your app via the TileActivatedInfo property on + /// LaunchActivatedEventArgs when the user launches your app from the Live Tile. This allows you to know + /// which Tile notifications your user saw when they tapped your Live Tile. On devices without the Anniversary Update, + /// this will simply be ignored. + /// + public string Arguments { get; set; } + + /// + /// Gets or sets the actual content to be displayed. One of , , , , or + /// + public ITileBindingContent Content { get; set; } + + internal Element_TileBinding ConvertToElement(TileSize size) + { + TileTemplateNameV3 templateName = GetTemplateName(Content, size); + + Element_TileBinding binding = new Element_TileBinding(templateName) + { + Language = Language, + BaseUri = BaseUri, + Branding = Branding, + AddImageQuery = AddImageQuery, + DisplayName = DisplayName, + ContentId = ContentId, + Arguments = Arguments + + // LockDetailedStatus gets populated by TileVisual + }; + + PopulateElement(Content, binding, size); + + return binding; + } + + private static void PopulateElement(ITileBindingContent bindingContent, Element_TileBinding binding, TileSize size) + { + if (bindingContent == null) + { + return; + } + + if (bindingContent is TileBindingContentAdaptive) + { + (bindingContent as TileBindingContentAdaptive).PopulateElement(binding, size); + } + else if (bindingContent is TileBindingContentContact) + { + (bindingContent as TileBindingContentContact).PopulateElement(binding, size); + } + else if (bindingContent is TileBindingContentIconic) + { + (bindingContent as TileBindingContentIconic).PopulateElement(binding, size); + } + else if (bindingContent is TileBindingContentPeople) + { + (bindingContent as TileBindingContentPeople).PopulateElement(binding, size); + } + else if (bindingContent is TileBindingContentPhotos) + { + (bindingContent as TileBindingContentPhotos).PopulateElement(binding, size); + } + else + { + throw new NotImplementedException("Unknown binding content type: " + bindingContent.GetType()); + } + } + + private static TileTemplateNameV3 GetTemplateName(ITileBindingContent bindingContent, TileSize size) + { + if (bindingContent == null) + { + return TileSizeToAdaptiveTemplateConverter.Convert(size); + } + + if (bindingContent is TileBindingContentAdaptive) + { + return (bindingContent as TileBindingContentAdaptive).GetTemplateName(size); + } + + if (bindingContent is TileBindingContentContact) + { + return (bindingContent as TileBindingContentContact).GetTemplateName(size); + } + + if (bindingContent is TileBindingContentIconic) + { + return (bindingContent as TileBindingContentIconic).GetTemplateName(size); + } + + if (bindingContent is TileBindingContentPeople) + { + return (bindingContent as TileBindingContentPeople).GetTemplateName(size); + } + + if (bindingContent is TileBindingContentPhotos) + { + return (bindingContent as TileBindingContentPhotos).GetTemplateName(size); + } + + throw new NotImplementedException("Unknown binding content type: " + bindingContent.GetType()); + } + } + + /// + /// Visual Tile content. One of , , , , or . + /// + public interface ITileBindingContent + { + } + + internal enum TileTemplate + { + TileSmall, + TileMedium, + TileWide, + TileLarge + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileBindingContentAdaptive.cs b/components/Notifications/src/Tiles/TileBindingContentAdaptive.cs new file mode 100644 index 000000000..3b970165e --- /dev/null +++ b/components/Notifications/src/Tiles/TileBindingContentAdaptive.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using CommunityToolkit.Notifications.Adaptive; + +namespace CommunityToolkit.Notifications +{ + /// + /// Supported on all sizes. This is the recommended way of specifying your Tile content. Adaptive Tile templates are the de-facto choice for Windows 10, and you can create a wide variety of custom Tiles through adaptive. + /// + public sealed class TileBindingContentAdaptive : ITileBindingContent + { + /// + /// Gets , , and objects that can be added as children. The children are displayed in a vertical StackPanel fashion. + /// + public IList Children { get; private set; } = new List(); + + /// + /// Gets or sets an optional background image that gets displayed behind all the Tile content, full bleed. + /// + public TileBackgroundImage BackgroundImage { get; set; } + + /// + /// Gets or sets an optional peek image that animates in from the top of the Tile. + /// + public TilePeekImage PeekImage { get; set; } + + /// + /// Gets or sets the text stacking (vertical alignment) of the entire binding element. + /// + public TileTextStacking TextStacking { get; set; } = Element_TileBinding.DEFAULT_TEXT_STACKING; + + internal TileTemplateNameV3 GetTemplateName(TileSize size) + { + return TileSizeToAdaptiveTemplateConverter.Convert(size); + } + + internal void PopulateElement(Element_TileBinding binding, TileSize size) + { + // Assign properties + binding.TextStacking = TextStacking; + + // Add the background image if there's one + if (BackgroundImage != null) + { + // And add it as a child + binding.Children.Add(BackgroundImage.ConvertToElement()); + } + + // Add the peek image if there's one + if (PeekImage != null) + { + var el = PeekImage.ConvertToElement(); + + binding.Children.Add(el); + } + + // And then add all the children + foreach (var child in Children) + { + binding.Children.Add(ConvertToBindingChildElement(child)); + } + } + + private static IElement_TileBindingChild ConvertToBindingChildElement(ITileBindingContentAdaptiveChild child) + { + return (IElement_TileBindingChild)AdaptiveHelper.ConvertToElement(child); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileBranding.cs b/components/Notifications/src/Tiles/TileBranding.cs new file mode 100644 index 000000000..c70a50c0b --- /dev/null +++ b/components/Notifications/src/Tiles/TileBranding.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// The form that the Tile should use to display the app's brand. + /// + public enum TileBranding + { + /// + /// The default choice. If ShowNameOn___ is true for the Tile size being displayed, then branding will be "Name". Otherwise it will be "None". + /// + Auto, + + /// + /// No branding will be displayed. + /// + None, + + /// + /// The DisplayName will be shown. + /// + Name, + + /// + /// Desktop-only. The Square44x44Logo will be shown. On Mobile, this will fallback to Name. + /// + Logo, + + /// + /// Desktop-only. Both the DisplayName and Square44x44Logo will be shown. On Mobile, this will fallback to Name. + /// + NameAndLogo + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileCommon.cs b/components/Notifications/src/Tiles/TileCommon.cs new file mode 100644 index 000000000..96544d3c9 --- /dev/null +++ b/components/Notifications/src/Tiles/TileCommon.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Represent the all tile sizes that are available. + /// + [Flags] + public enum TileSize + { + /// + /// Small Square Tile + /// + Small = 1, + + /// + /// Medium Square Tile + /// + Medium = 2, + + /// + /// Wide Rectangle Tile + /// + Wide = 4, + + /// + /// Large Square Tile + /// + Large = 8 + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileContent.cs b/components/Notifications/src/Tiles/TileContent.cs new file mode 100644 index 000000000..b05d66f2d --- /dev/null +++ b/components/Notifications/src/Tiles/TileContent.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP +using Windows.Data.Xml.Dom; +using Windows.UI.Notifications; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Base Tile element, which contains a single visual element. + /// + public sealed class TileContent + { + /// + /// Gets or sets the visual element. Required. + /// + public TileVisual Visual { get; set; } + + /// + /// Retrieves the notification XML content as a string, so that it can be sent with a HTTP POST in a push notification. + /// + /// The notification XML content as a string. + public string GetContent() + { + return ConvertToElement().GetContent(); + } + +#if WINDOWS_UWP + /// + /// Retrieves the notification XML content as a WinRT XmlDocument, so that it can be used with a local Tile notification's constructor on either or . + /// + /// The notification XML content as a WinRT XmlDocument. + public XmlDocument GetXml() + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(GetContent()); + + return doc; + } +#endif + + internal Element_Tile ConvertToElement() + { + var tile = new Element_Tile(); + + if (Visual != null) + { + tile.Visual = Visual.ConvertToElement(); + } + + return tile; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileImages.cs b/components/Notifications/src/Tiles/TileImages.cs new file mode 100644 index 000000000..6ac9407f1 --- /dev/null +++ b/components/Notifications/src/Tiles/TileImages.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Specify the desired cropping of the image. + /// + public enum TileBackgroundImageCrop + { + /// + /// Cropping style automatically determined by renderer. + /// + Default, + + /// + /// Default value. Image is not cropped. + /// + None, + + /// + /// Image is cropped to a circle shape. + /// + Circle + } + + /// + /// Specify the desired cropping of the image. + /// + public enum TilePeekImageCrop + { + /// + /// Cropping style automatically determined by renderer. + /// + Default, + + /// + /// Default value. Image is not cropped. + /// + None, + + /// + /// Image is cropped to a circle shape. + /// + Circle + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TilePeekImage.cs b/components/Notifications/src/Tiles/TilePeekImage.cs new file mode 100644 index 000000000..e8e75f309 --- /dev/null +++ b/components/Notifications/src/Tiles/TilePeekImage.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// A peek image that animates in from the top of the Tile. + /// + public sealed class TilePeekImage : IBaseImage + { + private string _source; + + /// + /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets set a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + private int? _hintOverlay; + + /// + /// Gets or sets a black overlay on the peek image. This value controls the opacity of the black overlay, with 0 being no overlay and 100 being completely black. Defaults to 0. + /// Previously for RTM: Did not exist, value will be ignored and peek image will be displayed with 0 overlay. + /// + public int? HintOverlay + { + get + { + return _hintOverlay; + } + + set + { + if (value != null) + { + Element_TileBinding.CheckOverlayValue(value.Value); + } + + _hintOverlay = value; + } + } + + /// + /// Gets or sets the desired cropping of the image. + /// Previously for RTM: Did not exist, value will be ignored and peek image will be displayed without any cropping. + /// + public TilePeekImageCrop HintCrop { get; set; } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this); + + image.Placement = AdaptiveImagePlacement.Peek; + image.Crop = GetAdaptiveImageCrop(); + image.Overlay = HintOverlay; + + return image; + } + + private AdaptiveImageCrop GetAdaptiveImageCrop() + { + switch (HintCrop) + { + case TilePeekImageCrop.Circle: + return AdaptiveImageCrop.Circle; + + case TilePeekImageCrop.None: + return AdaptiveImageCrop.None; + + default: + return AdaptiveImageCrop.Default; + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs b/components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs new file mode 100644 index 000000000..86b19981d --- /dev/null +++ b/components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + internal static class TileSizeToAdaptiveTemplateConverter + { + public static TileTemplateNameV3 Convert(TileSize size) + { + switch (size) + { + case TileSize.Small: + return TileTemplateNameV3.TileSmall; + + case TileSize.Medium: + return TileTemplateNameV3.TileMedium; + + case TileSize.Wide: + return TileTemplateNameV3.TileWide; + + case TileSize.Large: + return TileTemplateNameV3.TileLarge; + + default: + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileTemplateNameV3.cs b/components/Notifications/src/Tiles/TileTemplateNameV3.cs new file mode 100644 index 000000000..1cdd685aa --- /dev/null +++ b/components/Notifications/src/Tiles/TileTemplateNameV3.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + internal enum TileTemplateNameV3 + { + TileMedium, + TileSmall, + TileWide, + TileLarge, + + TileSquare150x150Block, + TileSquare150x150Image, + TileSquare150x150PeekImageAndText01, + TileSquare150x150PeekImageAndText02, + TileSquare150x150PeekImageAndText03, + TileSquare150x150PeekImageAndText04, + TileSquare150x150Text01, + TileSquare150x150Text02, + TileSquare150x150Text03, + TileSquare150x150Text04, + TileSquare310x310BlockAndText01, + TileSquare310x310BlockAndText02, + TileSquare310x310Image, + TileSquare310x310ImageAndText01, + TileSquare310x310ImageAndText02, + TileSquare310x310ImageAndTextOverlay01, + TileSquare310x310ImageAndTextOverlay02, + TileSquare310x310ImageAndTextOverlay03, + TileSquare310x310ImageCollection, + TileSquare310x310ImageCollectionAndText01, + TileSquare310x310ImageCollectionAndText02, + TileSquare310x310SmallImagesAndTextList01, + TileSquare310x310SmallImagesAndTextList02, + TileSquare310x310SmallImagesAndTextList03, + TileSquare310x310SmallImagesAndTextList04, + TileSquare310x310Text01, + TileSquare310x310Text02, + TileSquare310x310Text03, + TileSquare310x310Text04, + TileSquare310x310Text05, + TileSquare310x310Text06, + TileSquare310x310Text07, + TileSquare310x310Text08, + TileSquare310x310TextList01, + TileSquare310x310TextList02, + TileSquare310x310TextList03, + TileWide310x150BlockAndText01, + TileWide310x150BlockAndText02, + TileWide310x150Image, + TileWide310x150ImageAndText01, + TileWide310x150ImageAndText02, + TileWide310x150ImageCollection, + TileWide310x150PeekImage01, + TileWide310x150PeekImage02, + TileWide310x150PeekImage03, + TileWide310x150PeekImage04, + TileWide310x150PeekImage05, + TileWide310x150PeekImage06, + TileWide310x150PeekImageAndText01, + TileWide310x150PeekImageAndText02, + TileWide310x150PeekImageCollection01, + TileWide310x150PeekImageCollection02, + TileWide310x150PeekImageCollection03, + TileWide310x150PeekImageCollection04, + TileWide310x150PeekImageCollection05, + TileWide310x150PeekImageCollection06, + TileWide310x150SmallImageAndText01, + TileWide310x150SmallImageAndText02, + TileWide310x150SmallImageAndText03, + TileWide310x150SmallImageAndText04, + TileWide310x150SmallImageAndText05, + TileWide310x150Text01, + TileWide310x150Text02, + TileWide310x150Text03, + TileWide310x150Text04, + TileWide310x150Text05, + TileWide310x150Text06, + TileWide310x150Text07, + TileWide310x150Text08, + TileWide310x150Text09, + TileWide310x150Text10, + TileWide310x150Text11, + + TileSquare71x71Image, + TileSquare71x71IconWithBadge, + TileSquare150x150IconWithBadge, + TileWide310x150IconWithBadgeAndText + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileTextStacking.cs b/components/Notifications/src/Tiles/TileTextStacking.cs new file mode 100644 index 000000000..4b4e352dd --- /dev/null +++ b/components/Notifications/src/Tiles/TileTextStacking.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// TextStacking specifies the vertical alignment of content. + /// + public enum TileTextStacking + { + /// + /// Vertical align to the top. + /// + Top, + + /// + /// Vertical align to the center. + /// + Center, + + /// + /// Vertical align to the bottom. + /// + Bottom + } +} \ No newline at end of file diff --git a/components/Notifications/src/Tiles/TileVisual.cs b/components/Notifications/src/Tiles/TileVisual.cs new file mode 100644 index 000000000..123a785c3 --- /dev/null +++ b/components/Notifications/src/Tiles/TileVisual.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// Contains multiple binding child elements, each of which defines a Tile. + /// + public sealed class TileVisual + { + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Language { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets the form that the Tile should use to display the app's brand. + /// + public TileBranding Branding { get; set; } = Element_TileVisual.DEFAULT_BRANDING; + + /// + /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of + /// + /// "www.website.com/images/hello.png" + /// + /// included in the notification becomes + /// + /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us" + /// + public bool? AddImageQuery { get; set; } + + /// + /// Gets or sets a sender-defined string that uniquely identifies the content of the notification. This prevents duplicates in the situation where a large Tile template is displaying the last three wide Tile notifications. + /// + public string ContentId { get; set; } + + /// + /// Gets or sets an optional string to override the Tile's display name while showing this notification. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets the first line of text that will be displayed on the lock screen if the user has selected + /// your Tile as their detailed status app. Ff you specify this, you must also provide a Wide Tile binding. + /// + public string LockDetailedStatus1 { get; set; } + + /// + /// Gets or sets the second line of text that will be displayed on the lock screen if the user has selected + /// your Tile as their detailed status app. If you specify this, you must also provide a Wide Tile binding. + /// + public string LockDetailedStatus2 { get; set; } + + /// + /// Gets or sets the third line of text that will be displayed on the lock screen if the user has selected your + /// Tile as their detailed status app. If you specify this, you must also provide a Wide Tile binding. + /// + public string LockDetailedStatus3 { get; set; } + + /// + /// Gets or sets app-defined data that is passed back to your app via the TileActivatedInfo property on LaunchActivatedEventArgs when the user launches your app from the Live Tile. This allows you to know which Tile notifications your user saw when they tapped your Live Tile. On devices without the Anniversary Update, this will simply be ignored. + /// + public string Arguments { get; set; } + + /// + /// Gets or sets an optional small binding to specify content for the small Tile size. + /// + public TileBinding TileSmall { get; set; } + + /// + /// Gets or sets an optional medium binding to specify content for the medium Tile size. + /// + public TileBinding TileMedium { get; set; } + + /// + /// Gets or sets an optional wide binding to specify content for the wide Tile size. + /// + public TileBinding TileWide { get; set; } + + /// + /// Gets or sets an optional large binding to specify content for the large Tile size. Desktop-only + /// + public TileBinding TileLarge { get; set; } + + /// + /// Attempts to find and re-use an existing text element inside the binding. Returns true if it could. Otherwise returns false, and the caller will have to specify the detailed status using the lock hint attribute. + /// + /// The lock screen line number. + /// The lock screen line text. + /// The binding to look in for matches. + /// True if could re-use existing text element, otherwise false. + private static bool TryReuseTextElementForLockDetailedText(int lineNumber, string lockText, Element_TileBinding binding) + { + if (lockText == null) + { + throw new ArgumentNullException("lockText cannot be null"); + } + + if (binding == null) + { + throw new ArgumentNullException("binding cannot be null"); + } + + // If a text element already has an id with the line number (only look at immediate children, since the lock screen will ignore things under groups/subgroups) + Element_AdaptiveText matchingIdTextElement = binding.Children.OfType().FirstOrDefault(i => i.Id != null && i.Id.Equals(lineNumber.ToString())); + + if (matchingIdTextElement != null) + { + // If the text in the element matches the lock text, then we're good, don't need to assign anything else! + if (matchingIdTextElement.Text != null && matchingIdTextElement.Text.Equals(lockText)) + { + return true; + } + + // Otherwise, we need to specify the lock text in the hint attribute, so we return false + return false; + } + + // Otherwise no text elements use that ID, so we could assign one if we find a text element that doesn't have an ID assigned and matches the lock text + Element_AdaptiveText matchingTextTextElement = binding.Children.OfType().FirstOrDefault(i => i.Id == null && i.Text != null && i.Text.Equals(lockText)); + + // If we found text that matched, we'll assign the id so it gets re-used for lock! + if (matchingTextTextElement != null) + { + matchingTextTextElement.Id = lineNumber; + return true; + } + + // Otherwise we'll need to specify lock text in hint attribute, so return false + return false; + } + + internal Element_TileVisual ConvertToElement() + { + var visual = new Element_TileVisual() + { + Language = Language, + BaseUri = BaseUri, + Branding = Branding, + AddImageQuery = AddImageQuery, + ContentId = ContentId, + DisplayName = DisplayName, + Arguments = Arguments + }; + + if (TileSmall != null) + { + visual.Bindings.Add(TileSmall.ConvertToElement(TileSize.Small)); + } + + if (TileMedium != null) + { + visual.Bindings.Add(TileMedium.ConvertToElement(TileSize.Medium)); + } + + if (TileWide != null) + { + Element_TileBinding wideBindingElement = TileWide.ConvertToElement(TileSize.Wide); + + // If lock detailed status was specified, add them + if (LockDetailedStatus1 != null) + { + // If we can't reuse existing text element, we'll have to use the hints + if (!TryReuseTextElementForLockDetailedText(1, LockDetailedStatus1, wideBindingElement)) + { + wideBindingElement.LockDetailedStatus1 = LockDetailedStatus1; + } + } + + if (LockDetailedStatus2 != null) + { + if (!TryReuseTextElementForLockDetailedText(2, LockDetailedStatus2, wideBindingElement)) + { + wideBindingElement.LockDetailedStatus2 = LockDetailedStatus2; + } + } + + if (LockDetailedStatus3 != null) + { + if (!TryReuseTextElementForLockDetailedText(3, LockDetailedStatus3, wideBindingElement)) + { + wideBindingElement.LockDetailedStatus3 = LockDetailedStatus3; + } + } + + visual.Bindings.Add(wideBindingElement); + } + + // Otherwise if they specified lock values, throw an exception since lock values require wide + else if (HasLockDetailedStatusValues()) + { + throw new Exception("To provide lock detailed status text strings, you must also provide a TileWide binding. Either provide a TileWide binding, or leave the detailed status values null."); + } + + if (TileLarge != null) + { + visual.Bindings.Add(TileLarge.ConvertToElement(TileSize.Large)); + } + + // TODO: If a BaseUri wasn't provided, we can potentially optimize the payload size by calculating the best BaseUri + return visual; + } + + private bool HasLockDetailedStatusValues() + { + return LockDetailedStatus1 != null && LockDetailedStatus2 != null && LockDetailedStatus3 != null; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Builder/CustomizeToast.cs b/components/Notifications/src/Toasts/Builder/CustomizeToast.cs new file mode 100644 index 000000000..9623eb617 --- /dev/null +++ b/components/Notifications/src/Toasts/Builder/CustomizeToast.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP + +using Windows.Foundation; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Allows you to set additional properties on the object before the toast is displayed. + /// + /// The toast to modify that will be displayed. + public delegate void CustomizeToast(ToastNotification toast); + + /// + /// Allows you to set additional properties on the object before the toast is displayed. + /// + /// The toast to modify that will be displayed. + /// An operation. + public delegate IAsyncAction CustomizeToastAsync(ToastNotification toast); + + /// + /// Allows you to set additional properties on the object before the toast is scheduled. + /// + /// The scheduled toast to modify that will be scheduled. + public delegate void CustomizeScheduledToast(ScheduledToastNotification toast); + + /// + /// Allows you to set additional properties on the object before the toast is scheduled. + /// + /// The scheduled toast to modify that will be scheduled. + /// An operation. + public delegate IAsyncAction CustomizeScheduledToastAsync(ScheduledToastNotification toast); +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs new file mode 100644 index 000000000..8575f8155 --- /dev/null +++ b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs @@ -0,0 +1,319 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CommunityToolkit.Notifications +{ +#pragma warning disable SA1008 +#pragma warning disable SA1009 + /// + /// Builder class used to create + /// + public +#if WINRT + sealed +#endif + partial class ToastContentBuilder + { + private IToastActions Actions + { + get + { + if (Content.Actions == null) + { + Content.Actions = new ToastActionsCustom(); + } + + return Content.Actions; + } + } + + private IList ButtonList + { + get + { + return ((ToastActionsCustom)Actions).Buttons; + } + } + + private IList InputList + { + get + { + return ((ToastActionsCustom)Actions).Inputs; + } + } + + private string SerializeArgumentsIncludingGeneric(ToastArguments arguments) + { + if (_genericArguments.Count == 0) + { + return arguments.ToString(); + } + + foreach (var genericArg in _genericArguments) + { + if (!arguments.Contains(genericArg.Key)) + { + arguments.Add(genericArg.Key, genericArg.Value); + } + } + + return arguments.ToString(); + } + + /// + /// Add a button to the current toast. + /// + /// Text to display on the button. + /// Type of activation this button will use when clicked. Defaults to Foreground. + /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button. + /// The current instance of + public ToastContentBuilder AddButton(string content, ToastActivationType activationType, string arguments) + { + return AddButton(content, activationType, arguments, default); + } + + /// + /// Add a button to the current toast. + /// + /// Text to display on the button. + /// Type of activation this button will use when clicked. Defaults to Foreground. + /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button. + /// Optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). + /// The current instance of +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + public ToastContentBuilder AddButton(string content, ToastActivationType activationType, string arguments, Uri imageUri) + { + // Add new button + ToastButton button = new ToastButton(content, arguments) + { + ActivationType = activationType + }; + + if (imageUri != default) + { + button.ImageUri = imageUri.OriginalString; + } + + return AddButton(button); + } + + /// + /// Add a button to the current toast. + /// + /// An instance of class that implement for the button that will be used on the toast. + /// The current instance of + public ToastContentBuilder AddButton(IToastButton button) + { + if (button is ToastButton toastButton && toastButton.Content == null && toastButton.NeedsContent()) + { + throw new InvalidOperationException("Content is required on button."); + } + + // List has max 5 buttons + if (ButtonList.Count == 5) + { + throw new InvalidOperationException("A toast can't have more than 5 buttons"); + } + + if (button is ToastButton b && b.CanAddArguments()) + { + foreach (var arg in _genericArguments) + { + if (!b.ContainsArgument(arg.Key)) + { + b.AddArgument(arg.Key, arg.Value); + } + } + } + + ButtonList.Add(button); + + return this; + } + + /// + /// Add an button to the toast that will be display to the right of the input text box, achieving a quick reply scenario. + /// + /// ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. + /// Text to display on the button. + /// Type of activation this button will use when clicked. Defaults to Foreground. + /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button. + /// The current instance of + public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, string arguments) + { + return AddButton(textBoxId, content, activationType, arguments, default); + } + + /// + /// Add an button to the toast that will be display to the right of the input text box, achieving a quick reply scenario. + /// + /// ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. + /// Text to display on the button. + /// Type of activation this button will use when clicked. Defaults to Foreground. + /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button. + /// An optional image icon for the button to display (required for buttons adjacent to inputs like quick reply) + /// The current instance of + public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, string arguments, Uri imageUri) + { + // Add new button + ToastButton button = new ToastButton(content, arguments) + { + ActivationType = activationType, + TextBoxId = textBoxId + }; + + if (imageUri != default) + { + button.ImageUri = imageUri.OriginalString; + } + + return AddButton(button); + } + +#if WINRT + /// + /// Add an input text box that the user can type into. + /// + /// Required ID property so that developers can retrieve user input once the app is activated. + /// The current instance of + public ToastContentBuilder AddInputTextBox(string id) + { + return AddInputTextBox(id, default, default); + } + + /// + /// Add an input text box that the user can type into. + /// + /// Required ID property so that developers can retrieve user input once the app is activated. + /// Placeholder text to be displayed on the text box when the user hasn't typed any text yet. + /// The current instance of + public ToastContentBuilder AddInputTextBox(string id, string placeHolderContent) + { + return AddInputTextBox(id, placeHolderContent, default); + } +#endif + + /// + /// Add an input text box that the user can type into. + /// + /// Required ID property so that developers can retrieve user input once the app is activated. + /// Placeholder text to be displayed on the text box when the user hasn't typed any text yet. + /// Title text to display above the text box. + /// The current instance of + public ToastContentBuilder AddInputTextBox( + string id, +#if WINRT + string placeHolderContent, + string title) +#else + string placeHolderContent = default, + string title = default) +#endif + { + var inputTextBox = new ToastTextBox(id); + + if (placeHolderContent != default) + { + inputTextBox.PlaceholderContent = placeHolderContent; + } + + if (title != default) + { + inputTextBox.Title = title; + } + + return AddToastInput(inputTextBox); + } + +#if !WINRT + /// + /// Add a combo box / drop-down menu that contain options for user to select. + /// + /// Required ID property used so that developers can retrieve user input once the app is activated. + /// List of choices that will be available for user to select. + /// The current instance of + public ToastContentBuilder AddComboBox(string id, params (string comboBoxItemId, string comboBoxItemContent)[] choices) + { + return AddComboBox(id, default, choices); + } + + /// + /// Add a combo box / drop-down menu that contain options for user to select. + /// + /// Required ID property used so that developers can retrieve user input once the app is activated. + /// Sets which item is selected by default, and refers to the Id property of . If you do not provide this or null, the default selection will be empty (user sees nothing). + /// List of choices that will be available for user to select. + /// The current instance of + public ToastContentBuilder AddComboBox(string id, string defaultSelectionBoxItemId, params (string comboBoxItemId, string comboBoxItemContent)[] choices) + { + return AddComboBox(id, default, defaultSelectionBoxItemId, choices); + } + + /// + /// Add a combo box / drop-down menu that contain options for user to select. + /// + /// Required ID property used so that developers can retrieve user input once the app is activated. + /// Title text to display above the Combo Box. + /// Sets which item is selected by default, and refers to the Id property of . If you do not provide this or null, the default selection will be empty (user sees nothing). + /// List of choices that will be available for user to select. + /// The current instance of + public ToastContentBuilder AddComboBox(string id, string title, string defaultSelectionBoxItemId, params (string comboBoxItemId, string comboBoxItemContent)[] choices) + { + return AddComboBox(id, title, defaultSelectionBoxItemId, choices as IEnumerable<(string, string)>); + } + + /// + /// Add a combo box / drop-down menu that contain options for user to select. + /// + /// Required ID property used so that developers can retrieve user input once the app is activated. + /// Title text to display above the Combo Box. + /// Sets which item is selected by default, and refers to the Id property of . If you do not provide this or null, the default selection will be empty (user sees nothing). + /// List of choices that will be available for user to select. + /// The current instance of + public ToastContentBuilder AddComboBox(string id, string title, string defaultSelectionBoxItemId, IEnumerable<(string comboBoxItemId, string comboBoxItemContent)> choices) + { + var box = new ToastSelectionBox(id); + + if (defaultSelectionBoxItemId != default) + { + box.DefaultSelectionBoxItemId = defaultSelectionBoxItemId; + } + + if (title != default) + { + box.Title = title; + } + + for (int i = 0; i < choices.Count(); i++) + { + var (comboBoxItemId, comboBoxItemContent) = choices.ElementAt(i); + box.Items.Add(new ToastSelectionBoxItem(comboBoxItemId, comboBoxItemContent)); + } + + return AddToastInput(box); + } +#endif + + /// + /// Add an input option to the Toast. + /// + /// An instance of a class that implement that will be used on the toast. + /// The current instance of + public ToastContentBuilder AddToastInput(IToastInput input) + { + InputList.Add(input); + + return this; + } + } +#pragma warning restore SA1008 +#pragma warning restore SA1009 +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs new file mode 100644 index 000000000..b4d67b375 --- /dev/null +++ b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs @@ -0,0 +1,594 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +#if WINDOWS_UWP +using Windows.UI.Notifications; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Builder class used to create + /// + public partial class ToastContentBuilder + { + private ToastVisual Visual + { + get + { + if (Content.Visual == null) + { + Content.Visual = new ToastVisual(); + Content.Visual.BindingGeneric = new ToastBindingGeneric(); + } + + return Content.Visual; + } + } + + private ToastGenericAppLogo AppLogoOverrideUri + { + get + { + return Visual.BindingGeneric.AppLogoOverride; + } + + set + { + Visual.BindingGeneric.AppLogoOverride = value; + } + } + + private ToastGenericAttributionText AttributionText + { + get + { + return Visual.BindingGeneric.Attribution; + } + + set + { + Visual.BindingGeneric.Attribution = value; + } + } + + private ToastGenericHeroImage HeroImage + { + get + { + return Visual.BindingGeneric.HeroImage; + } + + set + { + Visual.BindingGeneric.HeroImage = value; + } + } + + private IList VisualChildren + { + get + { + return Visual.BindingGeneric.Children; + } + } + +#if WINDOWS_UWP +#if !WINRT + /// + /// Create an instance of NotificationData that can be used to update toast that has a progress bar. + /// + /// Instance of ToastContent that contain progress bars that need to be updated + /// Index of the progress bar (0-based) that this notification data is updating in the case that toast has multiple progress bars. Default to 0. + /// Title of the progress bar. + /// Value of the progress bar. + /// An optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed. + /// A status string, which is displayed underneath the progress bar on the left. Default to empty. + /// A sequence number to prevent out-of-order updates, or assign 0 to indicate "always update". + /// An instance of NotificationData that can be used to update the toast. + public static NotificationData CreateProgressBarData(ToastContent toast, int index = 0, string title = default, double? value = default, string valueStringOverride = default, string status = default, uint sequence = 0) + { + var progressBar = toast.Visual.BindingGeneric.Children.Where(c => c is AdaptiveProgressBar).ElementAt(index) as AdaptiveProgressBar; + if (progressBar == null) + { + throw new ArgumentException(nameof(toast), "Given toast does not have any progress bar"); + } + + NotificationData data = new NotificationData(); + data.SequenceNumber = sequence; + + // Native C++ doesn't support BindableString + if (progressBar.Title is BindableString bindableTitle && title != default) + { + data.Values[bindableTitle.BindingName] = title; + } + + if (progressBar.Value is BindableProgressBarValue bindableProgressValue && value != default) + { + data.Values[bindableProgressValue.BindingName] = value.ToString(); + } + + if (progressBar.ValueStringOverride is BindableString bindableValueStringOverride && valueStringOverride != default) + { + data.Values[bindableValueStringOverride.BindingName] = valueStringOverride; + } + + if (progressBar.Status is BindableString bindableStatus && status != default) + { + data.Values[bindableStatus.BindingName] = status; + } + + return data; + } +#endif +#endif + + /// + /// Add an Attribution Text to be displayed on the toast. + /// + /// Text to be displayed as Attribution Text + /// The current instance of + public ToastContentBuilder AddAttributionText(string text) + { + return AddAttributionText(text, default); + } + + /// + /// Add an Attribution Text to be displayed on the toast. + /// + /// Text to be displayed as Attribution Text + /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". + /// The current instance of + public ToastContentBuilder AddAttributionText(string text, string language) + { + AttributionText = new ToastGenericAttributionText() + { + Text = text + }; + + if (language != default) + { + AttributionText.Language = language; + } + + return this; + } + +#if WINRT + /// + /// Override the app logo with custom image of choice that will be displayed on the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// The current instance of + public ToastContentBuilder AddAppLogoOverride(Uri uri) + { + return AddAppLogoOverride(uri, default); + } + + /// + /// Override the app logo with custom image of choice that will be displayed on the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// Specify how the image should be cropped. + /// The current instance of + public ToastContentBuilder AddAppLogoOverride(Uri uri, ToastGenericAppLogoCrop? hintCrop) + { + return AddAppLogoOverride(uri, hintCrop, default); + } + + /// + /// Override the app logo with custom image of choice that will be displayed on the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// Specify how the image should be cropped. + /// A description of the image, for users of assistive technologies. + /// The current instance of + public ToastContentBuilder AddAppLogoOverride(Uri uri, ToastGenericAppLogoCrop? hintCrop, string alternateText) + { + return AddAppLogoOverride(uri, hintCrop, alternateText, default); + } +#endif + + /// + /// Override the app logo with custom image of choice that will be displayed on the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// Specify how the image should be cropped. + /// A description of the image, for users of assistive technologies. + /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. + /// The current instance of + public ToastContentBuilder AddAppLogoOverride( + Uri uri, +#if WINRT + ToastGenericAppLogoCrop? hintCrop, + string alternateText, + bool? addImageQuery) +#else + ToastGenericAppLogoCrop? hintCrop = default, + string alternateText = default, + bool? addImageQuery = default) +#endif + { + AppLogoOverrideUri = new ToastGenericAppLogo() + { + Source = uri.OriginalString + }; + + if (hintCrop != default) + { + AppLogoOverrideUri.HintCrop = hintCrop.Value; + } + + if (alternateText != default) + { + AppLogoOverrideUri.AlternateText = alternateText; + } + + if (addImageQuery != default) + { + AppLogoOverrideUri.AddImageQuery = addImageQuery; + } + + return this; + } + +#if WINRT + /// + /// Add a hero image to the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// The current instance of + public ToastContentBuilder AddHeroImage(Uri uri) + { + return AddHeroImage(uri, default); + } + + /// + /// Add a hero image to the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// A description of the image, for users of assistive technologies. + /// The current instance of + public ToastContentBuilder AddHeroImage(Uri uri, string alternateText) + { + return AddHeroImage(uri, alternateText, default); + } +#endif + + /// + /// Add a hero image to the toast. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// A description of the image, for users of assistive technologies. + /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. + /// The current instance of + public ToastContentBuilder AddHeroImage( + Uri uri, +#if WINRT + string alternateText, + bool? addImageQuery) +#else + string alternateText = default, + bool? addImageQuery = default) +#endif + { + HeroImage = new ToastGenericHeroImage() + { + Source = uri.OriginalString + }; + + if (alternateText != default) + { + HeroImage.AlternateText = alternateText; + } + + if (addImageQuery != default) + { + HeroImage.AddImageQuery = addImageQuery; + } + + return this; + } + +#if WINRT + /// + /// Add an image inline with other toast content. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// The current instance of + public ToastContentBuilder AddInlineImage(Uri uri) + { + return AddInlineImage(uri, default); + } + + /// + /// Add an image inline with other toast content. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// A description of the image, for users of assistive technologies. + /// The current instance of + public ToastContentBuilder AddInlineImage(Uri uri, string alternateText) + { + return AddInlineImage(uri, alternateText, default); + } + + /// + /// Add an image inline with other toast content. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// A description of the image, for users of assistive technologies. + /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. + /// The current instance of + public ToastContentBuilder AddInlineImage(Uri uri, string alternateText, bool? addImageQuery) + { + return AddInlineImage(uri, alternateText, addImageQuery, default); + } +#endif + +#if WINRT + /// + /// Add an image inline with other toast content. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// A description of the image, for users of assistive technologies. + /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. + /// A value whether a margin is removed. images have an 8px margin around them. + /// The current instance of + public ToastContentBuilder AddInlineImage( + Uri uri, + string alternateText, + bool? addImageQuery, + AdaptiveImageCrop? hintCrop) +#else + /// + /// Add an image inline with other toast content. + /// + /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// A description of the image, for users of assistive technologies. + /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. + /// A value whether a margin is removed. images have an 8px margin around them. + /// This property is not used. Setting this has no impact. + /// The current instance of + public ToastContentBuilder AddInlineImage( + Uri uri, + string alternateText = default, + bool? addImageQuery = default, + AdaptiveImageCrop? hintCrop = default, + bool? hintRemoveMargin = default) +#endif + { + var inlineImage = new AdaptiveImage() + { + Source = uri.OriginalString + }; + + if (hintCrop != null) + { + inlineImage.HintCrop = hintCrop.Value; + } + + if (alternateText != default) + { + inlineImage.AlternateText = alternateText; + } + + if (addImageQuery != default) + { + inlineImage.AddImageQuery = addImageQuery; + } + + return AddVisualChild(inlineImage); + } + +#if !WINRT + /// + /// Add a progress bar to the toast. + /// + /// Title of the progress bar. + /// Value of the progress bar. Default is 0 + /// Determine if the progress bar value should be indeterminate. Default to false. + /// An optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed. + /// A status string which is displayed underneath the progress bar. This string should reflect the status of the operation, like "Downloading..." or "Installing...". Default to empty. + /// The current instance of + /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-progress-bar + public ToastContentBuilder AddProgressBar(string title = default, double? value = null, bool isIndeterminate = false, string valueStringOverride = default, string status = default) + { + int index = VisualChildren.Count(c => c is AdaptiveProgressBar); + + var progressBar = new AdaptiveProgressBar() + { + }; + + if (title == default) + { + progressBar.Title = new BindableString($"progressBarTitle_{index}"); + } + else + { + progressBar.Title = title; + } + + if (isIndeterminate) + { + progressBar.Value = AdaptiveProgressBarValue.Indeterminate; + } + else if (value == null) + { + progressBar.Value = new BindableProgressBarValue($"progressValue_{index}"); + } + else + { + progressBar.Value = value.Value; + } + + if (valueStringOverride == default) + { + progressBar.ValueStringOverride = new BindableString($"progressValueString_{index}"); + } + else + { + progressBar.ValueStringOverride = valueStringOverride; + } + + if (status == default) + { + progressBar.Status = new BindableString($"progressStatus_{index}"); + } + else + { + progressBar.Status = status; + } + + return AddVisualChild(progressBar); + } +#endif + +#if WINRT + /// + /// Add text to the toast. + /// + /// Custom text to display on the tile. + /// The current instance of + /// Throws when attempting to add/reserve more than 4 lines on a single toast. + /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements + public ToastContentBuilder AddText(string text) + { + return AddText(text, default, default); + } + + /// + /// Add text to the toast. + /// + /// Custom text to display on the tile. + /// The maximum number of lines the text element is allowed to display. + /// The current instance of + /// Throws when attempting to add/reserve more than 4 lines on a single toast. + /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements + public ToastContentBuilder AddText(string text, int? hintMaxLines) + { + return AddText(text, hintMaxLines, default); + } +#endif + +#if WINRT + /// + /// Add text to the toast. + /// + /// Custom text to display on the tile. + /// The maximum number of lines the text element is allowed to display. + /// + /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. + /// + /// The current instance of + /// Throws when attempting to add/reserve more than 4 lines on a single toast. + /// Throws when value is larger than 2. + /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements + public ToastContentBuilder AddText( + string text, + int? hintMaxLines, + string language) +#else + /// + /// Add text to the toast. + /// + /// Custom text to display on the tile. + /// This property is not used. Setting this has no effect. + /// This property is not used. Setting this has no effect. If you need to disable wrapping, set hintMaxLines to 1. + /// The maximum number of lines the text element is allowed to display. + /// hintMinLines is not used. Setting this has no effect. + /// hintAlign is not used. Setting this has no effect. + /// + /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. + /// + /// The current instance of + /// Throws when attempting to add/reserve more than 4 lines on a single toast. + /// Throws when value is larger than 2. + /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements + public ToastContentBuilder AddText( + string text, + AdaptiveTextStyle? hintStyle = null, + bool? hintWrap = default, + int? hintMaxLines = default, + int? hintMinLines = default, + AdaptiveTextAlign? hintAlign = null, + string language = default) +#endif + { + int lineCount = GetCurrentTextLineCount(); + if (GetCurrentTextLineCount() == 4) + { + // Reached maximum, we can't go further. + throw new InvalidOperationException("We have reached max lines allowed (4) per toast"); + } + + AdaptiveText adaptive = new AdaptiveText() + { + Text = text + }; + + if (hintMaxLines != default) + { + if (hintMaxLines > 2) + { + throw new ArgumentOutOfRangeException(nameof(hintMaxLines), "max line can't go more than 2 lines."); + } + else if ((lineCount + hintMaxLines) > 4) + { + throw new InvalidOperationException($"Can't exceed more than 4 lines of text per toast. Current line count : {lineCount} | Requesting line count: {lineCount + hintMaxLines}"); + } + + adaptive.HintMaxLines = hintMaxLines; + } + + if (language != default) + { + adaptive.Language = language; + } + + return AddVisualChild(adaptive); + } + + /// + /// Add a visual element to the toast. + /// + /// An instance of a class that implement . + /// The current instance of + public ToastContentBuilder AddVisualChild(IToastBindingGenericChild child) + { + VisualChildren.Add(child); + + return this; + } + + private int GetCurrentTextLineCount() + { + if (!VisualChildren.Any(c => c is AdaptiveText)) + { + return 0; + } + + var textList = VisualChildren.Where(c => c is AdaptiveText).Select(c => c as AdaptiveText).ToList(); + + // First one is already the header. + // https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements + // The default (and maximum) is up to 2 lines of text for the title, and up to 4 lines (combined) for the two additional description elements (the second and third AdaptiveText). + AdaptiveText text = textList.First(); + int count = 0; + count += text.HintMaxLines ?? 2; + + for (int i = 1; i < textList.Count; i++) + { + text = textList[i]; + count += text.HintMaxLines ?? 1; + } + + return count; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs new file mode 100644 index 000000000..9e9fe17f8 --- /dev/null +++ b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs @@ -0,0 +1,505 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// Builder class used to create + /// + public partial class ToastContentBuilder +#if !WINRT + : IToastActivateableBuilder +#endif + { + private Dictionary _genericArguments = new Dictionary(); + + private bool _customArgumentsUsedOnToastItself; + + /// + /// Gets internal instance of . This is equivalent to the call to . + /// + public ToastContent Content + { + get; private set; + } + + /// + /// Initializes a new instance of the class. + /// + public ToastContentBuilder() + { + Content = new ToastContent(); + } + + /// + /// Add custom time stamp on the toast to override the time display on the toast. + /// + /// Custom Time to be displayed on the toast + /// The current instance of + public ToastContentBuilder AddCustomTimeStamp( +#if WINRT + DateTimeOffset dateTime) +#else + DateTime dateTime) +#endif + { + Content.DisplayTimestamp = dateTime; + + return this; + } + + /// + /// Add a header to a toast. + /// + /// A developer-created identifier that uniquely identifies this header. If two notifications have the same header id, they will be displayed underneath the same header in Action Center. + /// A title for the header. + /// Developer-defined arguments that are returned to the app when the user clicks this header. + /// The current instance of + /// More info about toast header: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-headers +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + public ToastContentBuilder AddHeader(string id, string title, ToastArguments arguments) + { + return AddHeader(id, title, arguments.ToString()); + } + + /// + /// Add a header to a toast. + /// + /// A developer-created identifier that uniquely identifies this header. If two notifications have the same header id, they will be displayed underneath the same header in Action Center. + /// A title for the header. + /// A developer-defined string of arguments that is returned to the app when the user clicks this header. + /// The current instance of + /// More info about toast header: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-headers + public ToastContentBuilder AddHeader(string id, string title, string arguments) + { + Content.Header = new ToastHeader(id, title, arguments); + + return this; + } + + /// + /// Adds a key (without value) to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key. + /// The current instance of + public ToastContentBuilder AddArgument(string key) + { + return AddArgumentHelper(key, null); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, string value) + { + return AddArgumentHelper(key, value); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, int value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, double value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, float value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, bool value) + { + return AddArgumentHelper(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + } + +#if !WINRT + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current instance of + public ToastContentBuilder AddArgument(string key, Enum value) + { + return AddArgumentHelper(key, ((int)(object)value).ToString()); + } +#endif + + private ToastContentBuilder AddArgumentHelper(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + bool alreadyExists = _genericArguments.ContainsKey(key); + + _genericArguments[key] = value; + + if (Content.ActivationType != ToastActivationType.Protocol && !_customArgumentsUsedOnToastItself) + { + Content.Launch = alreadyExists ? SerializeArgumentsHelper(_genericArguments) : AddArgumentHelper(Content.Launch, key, value); + } + + if (Content.Actions is ToastActionsCustom actions) + { + foreach (var button in actions.Buttons) + { + if (button is ToastButton b && b.CanAddArguments() && !b.ContainsArgument(key)) + { + b.AddArgument(key, value); + } + } + } + + return this; + } + + private string SerializeArgumentsHelper(IDictionary arguments) + { + var args = new ToastArguments(); + + foreach (var a in arguments) + { + args.Add(a.Key, a.Value); + } + + return args.ToString(); + } + + private string AddArgumentHelper(string existing, string key, string value) + { + string pair = ToastArguments.EncodePair(key, value); + + if (existing == null) + { + return pair; + } + else + { + return existing + ToastArguments.Separator + pair; + } + } + + /// + /// Configures the toast notification to launch the specified url when the toast body is clicked. + /// + /// The protocol to launch. + /// The current instance of + public ToastContentBuilder SetProtocolActivation(Uri protocol) + { + return SetProtocolActivation(protocol, default); + } + + /// + /// Configures the toast notification to launch the specified url when the toast body is clicked. + /// + /// The protocol to launch. + /// New in Creators Update: The target PFN, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// The current instance of + public ToastContentBuilder SetProtocolActivation(Uri protocol, string targetApplicationPfn) + { + Content.Launch = protocol.ToString(); + Content.ActivationType = ToastActivationType.Protocol; + + if (targetApplicationPfn != null) + { + if (Content.ActivationOptions == null) + { + Content.ActivationOptions = new ToastActivationOptions(); + } + + Content.ActivationOptions.ProtocolActivationTargetApplicationPfn = targetApplicationPfn; + } + + return this; + } + + /// + /// Configures the toast notification to use background activation when the toast body is clicked. + /// + /// The current instance of + public ToastContentBuilder SetBackgroundActivation() + { + Content.ActivationType = ToastActivationType.Background; + return this; + } + + /// + /// Instead of this method, for foreground/background activation, it is suggested to use and optionally . For protocol activation, you should use . Add info that can be used by the application when the app was activated/launched by the toast. + /// + /// Custom app-defined launch arguments to be passed along on toast activation + /// Set the activation type that will be used when the user click on this toast + /// The current instance of + public ToastContentBuilder AddToastActivationInfo(string launchArgs, ToastActivationType activationType) + { + Content.Launch = launchArgs; + Content.ActivationType = activationType; + _customArgumentsUsedOnToastItself = true; + return this; + } + + /// + /// Sets the amount of time the Toast should display. You typically should use the + /// Scenario attribute instead, which impacts how long a Toast stays on screen. + /// + /// Duration of the toast + /// The current instance of + public ToastContentBuilder SetToastDuration(ToastDuration duration) + { + Content.Duration = duration; + return this; + } + + /// + /// Sets the scenario, to make the Toast behave like an alarm, reminder, or more. + /// + /// Scenario to be used for the toast's behavior + /// The current instance of + public ToastContentBuilder SetToastScenario(ToastScenario scenario) + { + Content.Scenario = scenario; + return this; + } + +#if WINRT + /// + /// Set custom audio to go along with the toast. + /// + /// Source to the media that will be played when the toast is pop + /// The current instance of + [Windows.Foundation.Metadata.DefaultOverload] + public ToastContentBuilder AddAudio(Uri src) + { + return AddAudio(src, default, default); + } + + /// + /// Set custom audio to go along with the toast. + /// + /// Source to the media that will be played when the toast is pop + /// Indicating whether sound should repeat as long as the Toast is shown; false to play only once (default). + /// The current instance of + public ToastContentBuilder AddAudio(Uri src, bool? loop) + { + return AddAudio(src, loop, default); + } +#endif + + /// + /// Set custom audio to go along with the toast. + /// + /// Source to the media that will be played when the toast is pop + /// Indicating whether sound should repeat as long as the Toast is shown; false to play only once (default). + /// Indicating whether sound is muted; false to allow the Toast notification sound to play (default). + /// The current instance of + public ToastContentBuilder AddAudio( + Uri src, +#if WINRT + bool? loop, + bool? silent) +#else + bool? loop = default, + bool? silent = default) +#endif + { + var audio = new ToastAudio(); + audio.Src = src; + + if (loop != default) + { + audio.Loop = loop.Value; + } + + if (silent != default) + { + audio.Silent = silent.Value; + } + + return AddAudio(audio); + } + + /// + /// Set custom audio to go along with the toast. + /// + /// The to set. + /// The current instance of + public ToastContentBuilder AddAudio(ToastAudio audio) + { + if (audio.Src != null && !audio.Src.IsFile && audio.Src.Scheme != "ms-appx" && audio.Src.Scheme != "ms-winsoundevent") + { + throw new InvalidOperationException("Audio Source must either be a ms-appx file, absolute file, or ms-winsoundevent."); + } + + Content.Audio = audio; + return this; + } + + /// + /// Get the instance of that has been built by the builder with specified configuration so far. + /// + /// An instance of that can be used to create tile notification. + public ToastContent GetToastContent() + { + return Content; + } + +#if WINDOWS_UWP + /// + /// Retrieves the notification XML content as a WinRT XmlDocument, so that it can be used with a local Toast notification's constructor on either or . + /// + /// The notification XML content as a WinRT XmlDocument. + public Windows.Data.Xml.Dom.XmlDocument GetXml() + { + return GetToastContent().GetXml(); + } + + /// + /// Shows a new toast notification with the current content. + /// + public void Show() + { + CustomizeToast customize = null; + Show(customize); + } + + /// + /// Shows a new toast notification with the current content. + /// + /// Allows you to set additional properties on the object. +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + public void Show(CustomizeToast customize) + { + var notif = new Windows.UI.Notifications.ToastNotification(GetToastContent().GetXml()); + customize?.Invoke(notif); + + ToastNotificationManagerCompat.CreateToastNotifier().Show(notif); + } + + /// + /// Shows a new toast notification with the current content. + /// + /// Allows you to set additional properties on the object. + /// An operation that completes after your async customizations have completed. + public Windows.Foundation.IAsyncAction Show(CustomizeToastAsync customize) + { + return ShowAsyncHelper(customize).AsAsyncAction(); + } + + private async System.Threading.Tasks.Task ShowAsyncHelper(CustomizeToastAsync customize) + { + var notif = new Windows.UI.Notifications.ToastNotification(GetToastContent().GetXml()); + + if (customize != null) + { + await customize.Invoke(notif); + } + + ToastNotificationManagerCompat.CreateToastNotifier().Show(notif); + } + + /// + /// Schedules the notification. + /// + /// The date and time that Windows should display the toast notification. This time must be in the future. + public void Schedule(DateTimeOffset deliveryTime) + { + CustomizeScheduledToast customize = null; + Schedule(deliveryTime, customize); + } + + /// + /// Schedules the notification. + /// + /// The date and time that Windows should display the toast notification. This time must be in the future. + /// Allows you to set additional properties on the object. +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + public void Schedule(DateTimeOffset deliveryTime, CustomizeScheduledToast customize) + { + var notif = new Windows.UI.Notifications.ScheduledToastNotification(GetToastContent().GetXml(), deliveryTime); + customize?.Invoke(notif); + + ToastNotificationManagerCompat.CreateToastNotifier().AddToSchedule(notif); + } + + /// + /// Schedules the notification. + /// + /// The date and time that Windows should display the toast notification. This time must be in the future. + /// Allows you to set additional properties on the object. + /// An operation that completes after your async customizations have completed. + public Windows.Foundation.IAsyncAction Schedule(DateTimeOffset deliveryTime, CustomizeScheduledToastAsync customize) + { + return ScheduleAsyncHelper(deliveryTime, customize).AsAsyncAction(); + } + + private async System.Threading.Tasks.Task ScheduleAsyncHelper(DateTimeOffset deliveryTime, CustomizeScheduledToastAsync customize = null) + { + var notif = new Windows.UI.Notifications.ScheduledToastNotification(GetToastContent().GetXml(), deliveryTime); + + if (customize != null) + { + await customize.Invoke(notif); + } + + ToastNotificationManagerCompat.CreateToastNotifier().AddToSchedule(notif); + } +#endif + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/DesktopBridgeHelpers.cs b/components/Notifications/src/Toasts/Compat/Desktop/DesktopBridgeHelpers.cs new file mode 100644 index 000000000..7fa9eec79 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/DesktopBridgeHelpers.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Windows.ApplicationModel; + +namespace CommunityToolkit.Notifications +{ + /// + /// Code from https://github.com/qmatteoq/DesktopBridgeHelpers/tree/master/DesktopBridge.Helpers/Helpers.cs + /// + internal class DesktopBridgeHelpers + { + private const long APPMODEL_ERROR_NO_PACKAGE = 15700L; + + private static bool? _hasIdentity; + + public static bool HasIdentity() + { + if (_hasIdentity == null) + { + if (IsWindows7OrLower) + { + _hasIdentity = false; + } + else + { + int length = 0; + var sb = new StringBuilder(0); + NativeMethods.GetCurrentPackageFullName(ref length, sb); + + sb = new StringBuilder(length); + int error = NativeMethods.GetCurrentPackageFullName(ref length, sb); + + _hasIdentity = error != APPMODEL_ERROR_NO_PACKAGE; + } + } + + return _hasIdentity.Value; + } + + private static bool? _isContainerized; + + /// + /// Returns true if the app is running in a container (MSIX) or false if not running in a container (sparse or plain Win32) + /// + /// Boolean + public static bool IsContainerized() + { + if (_isContainerized == null) + { + // If MSIX or sparse + if (HasIdentity()) + { + // Sparse is identified if EXE is running outside of installed package location + var packageInstalledLocation = Package.Current.InstalledLocation.Path; + var actualExeFullPath = Process.GetCurrentProcess().MainModule.FileName; + + // If inside package location + if (actualExeFullPath.StartsWith(packageInstalledLocation)) + { + _isContainerized = true; + } + else + { + _isContainerized = false; + } + } + + // Plain Win32 + else + { + _isContainerized = false; + } + } + + return _isContainerized.Value; + } + + private static bool IsWindows7OrLower + { + get + { + int versionMajor = Environment.OSVersion.Version.Major; + int versionMinor = Environment.OSVersion.Version.Minor; + double version = versionMajor + ((double)versionMinor / 10); + return version <= 6.1; + } + } + } +} +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationHistoryCompat.cs b/components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationHistoryCompat.cs new file mode 100644 index 000000000..06cc99d94 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationHistoryCompat.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Collections.Generic; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Manages the toast notifications for an app including the ability the clear all toast history and removing individual toasts. + /// + [Obsolete("We recommend switching to the new ToastNotificationManagerCompat and ToastNotificationHistoryCompat.")] + public sealed class DesktopNotificationHistoryCompat + { + private string _aumid; + private ToastNotificationHistory _history; + + /// + /// Initializes a new instance of the class. + /// Do not call this. Instead, call to obtain an instance. + /// + /// An AUMID that uniquely identifies your application. + internal DesktopNotificationHistoryCompat(string aumid) + { + _aumid = aumid; + _history = ToastNotificationManager.History; + } + + /// + /// Removes all notifications sent by this app from action center. + /// + public void Clear() + { + if (_aumid != null) + { + _history.Clear(_aumid); + } + else + { + _history.Clear(); + } + } + + /// + /// Gets all notifications sent by this app that are currently still in Action Center. + /// + /// A collection of toasts. + public IReadOnlyList GetHistory() + { + return _aumid != null ? _history.GetHistory(_aumid) : _history.GetHistory(); + } + + /// + /// Removes an individual toast, with the specified tag label, from action center. + /// + /// The tag label of the toast notification to be removed. + public void Remove(string tag) + { + if (_aumid != null) + { + _history.Remove(tag, string.Empty, _aumid); + } + else + { + _history.Remove(tag); + } + } + + /// + /// Removes a toast notification from the action using the notification's tag and group labels. + /// + /// The tag label of the toast notification to be removed. + /// The group label of the toast notification to be removed. + public void Remove(string tag, string group) + { + if (_aumid != null) + { + _history.Remove(tag, group, _aumid); + } + else + { + _history.Remove(tag, group); + } + } + + /// + /// Removes a group of toast notifications, identified by the specified group label, from action center. + /// + /// The group label of the toast notifications to be removed. + public void RemoveGroup(string group) + { + if (_aumid != null) + { + _history.RemoveGroup(group, _aumid); + } + else + { + _history.RemoveGroup(group); + } + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationManagerCompat.cs b/components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationManagerCompat.cs new file mode 100644 index 000000000..3f737f8f4 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/DesktopNotificationManagerCompat.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Helper for .NET Framework applications to display toast notifications and respond to toast events + /// + [Obsolete("We recommend switching to the new ToastNotificationManagerCompat. For Win32 apps, it no longer requires a Start menu shortcut, and activation now uses a straightforward event handler (no NotificationActivator class and COM GUIDs needed)!")] + public class DesktopNotificationManagerCompat + { + /// + /// A constant that is used as the launch arg when your EXE is launched from a toast notification. + /// + public const string ToastActivatedLaunchArg = "-ToastActivated"; + + private const int CLASS_E_NOAGGREGATION = -2147221232; + private const int E_NOINTERFACE = -2147467262; + private const int CLSCTX_LOCAL_SERVER = 4; + private const int REGCLS_MULTIPLEUSE = 1; + private const int S_OK = 0; + private static readonly Guid IUnknownGuid = new Guid("00000000-0000-0000-C000-000000000046"); + + private static bool _registeredAumidAndComServer; + private static string _aumid; + private static bool _registeredActivator; + + /// + /// If you're not using MSIX or sparse packages, you must call this method to register your AUMID with the Compat library and to + /// register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will no-op if running + /// under Desktop Bridge. Call this upon application startup, before calling any other APIs. + /// + /// Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class. + /// An AUMID that uniquely identifies your application. + public static void RegisterAumidAndComServer(string aumid) + where T : NotificationActivator + { + if (string.IsNullOrWhiteSpace(aumid)) + { + throw new ArgumentException("You must provide an AUMID.", nameof(aumid)); + } + + // If running as Desktop Bridge + if (DesktopBridgeHelpers.HasIdentity()) + { + // Clear the AUMID since Desktop Bridge doesn't use it, and then we're done. + // Desktop Bridge apps are registered with platform through their manifest. + // Their LocalServer32 key is also registered through their manifest. + _aumid = null; + _registeredAumidAndComServer = true; + return; + } + + _aumid = aumid; + + string exePath = Process.GetCurrentProcess().MainModule.FileName; + RegisterComServer(exePath); + + _registeredAumidAndComServer = true; + } + + private static void RegisterComServer(string exePath) + where T : NotificationActivator + { + // We register the EXE to start up when the notification is activated + string regString = string.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}\\LocalServer32", typeof(T).GUID); + var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(regString); + + // Include a flag so we know this was a toast activation and should wait for COM to process + // We also wrap EXE path in quotes for extra security + key.SetValue(null, '"' + exePath + '"' + " " + ToastActivatedLaunchArg); + } + + /* + * RegisterActivator code and all internal dependencies is from FrecherxDachs. + * See entry #2 in ThirdPartyNotices.txt in root repository directory for full license. */ + + /// + /// Registers the activator type as a COM server client so that Windows can launch your activator. + /// + /// Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class. + public static void RegisterActivator() + where T : NotificationActivator, new() + { + // Big thanks to FrecherxDachs for figuring out the following code which works in .NET Core 3: https://github.com/FrecherxDachs/UwpNotificationNetCoreTest + var uuid = typeof(T).GUID; + uint cookie; + NativeMethods.CoRegisterClassObject( + uuid, + new NotificationActivatorClassFactory(), + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + out cookie); + + _registeredActivator = true; + } + + [ComImport] + [Guid("00000001-0000-0000-C000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IClassFactory + { + [PreserveSig] + int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject); + + [PreserveSig] + int LockServer(bool fLock); + } + + private class NotificationActivatorClassFactory : IClassFactory + where T : NotificationActivator, new() + { + public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject) + { + ppvObject = IntPtr.Zero; + + if (pUnkOuter != IntPtr.Zero) + { + Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION); + } + + if (riid == typeof(T).GUID || riid == IUnknownGuid) + { + // Create the instance of the .NET object + ppvObject = Marshal.GetComInterfaceForObject( + new T(), + typeof(NotificationActivator.INotificationActivationCallback)); + } + else + { + // The object that ppvObject points to does not support the + // interface identified by riid. + Marshal.ThrowExceptionForHR(E_NOINTERFACE); + } + + return S_OK; + } + + public int LockServer(bool fLock) + { + return S_OK; + } + } + + /// + /// Creates a toast notifier. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// + /// + public static ToastNotifier CreateToastNotifier() + { + EnsureRegistered(); + + if (_aumid != null) + { + // Non-Desktop Bridge + return ToastNotificationManager.CreateToastNotifier(_aumid); + } + else + { + // Desktop Bridge + return ToastNotificationManager.CreateToastNotifier(); + } + } + + /// + /// Gets the object. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// + public static DesktopNotificationHistoryCompat History + { + get + { + EnsureRegistered(); + + return new DesktopNotificationHistoryCompat(_aumid); + } + } + + private static void EnsureRegistered() + { + // If not registered AUMID yet + if (!_registeredAumidAndComServer) + { + // Check if Desktop Bridge + if (DesktopBridgeHelpers.HasIdentity()) + { + // Implicitly registered, all good! + _registeredAumidAndComServer = true; + } + else + { + // Otherwise, incorrect usage + throw new Exception("You must call RegisterAumidAndComServer first."); + } + } + + // If not registered activator yet + if (!_registeredActivator) + { + // Incorrect usage + throw new Exception("You must call RegisterActivator first."); + } + } + + /// + /// Gets a value indicating whether http images can be used within toasts. This is true if running with package identity (MSIX or sparse package). + /// + public static bool CanUseHttpImages + { + get { return DesktopBridgeHelpers.HasIdentity(); } + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/InternalNotificationActivator.cs b/components/Notifications/src/Toasts/Compat/Desktop/InternalNotificationActivator.cs new file mode 100644 index 000000000..4c8fe6ce2 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/InternalNotificationActivator.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.Notifications.Internal +{ + /// + /// Do not use this class. It must be public so that reflection can properly activate it, but consider it internal. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public abstract class InternalNotificationActivator : InternalNotificationActivator.INotificationActivationCallback + { + /// + public void Activate(string appUserModelId, string invokedArgs, InternalNotificationActivator.NOTIFICATION_USER_INPUT_DATA[] data, uint dataCount) + { + ToastNotificationManagerCompat.OnActivatedInternal(invokedArgs, data, appUserModelId); + } + + /// + /// A single user input key/value pair. + /// + [StructLayout(LayoutKind.Sequential)] + [Serializable] + public struct NOTIFICATION_USER_INPUT_DATA + { + /// + /// The key of the user input. + /// + [MarshalAs(UnmanagedType.LPWStr)] + public string Key; + + /// + /// The value of the user input. + /// + [MarshalAs(UnmanagedType.LPWStr)] + public string Value; + } + + /// + /// The COM callback that is triggered when your notification is clicked. + /// + [ComImport] + [Guid("53E31837-6600-4A81-9395-75CFFE746F94")] + [ComVisible(true)] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface INotificationActivationCallback + { + /// + /// The method called when your notification is clicked. + /// + /// The app id of the app that sent the toast. + /// The activation arguments from the toast. + /// The user input from the toast. + /// The number of user inputs. + void Activate( + [In, MarshalAs(UnmanagedType.LPWStr)] + string appUserModelId, + [In, MarshalAs(UnmanagedType.LPWStr)] + string invokedArgs, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] NOTIFICATION_USER_INPUT_DATA[] data, + [In, MarshalAs(UnmanagedType.U4)] + uint dataCount); + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/ManifestHelper.cs b/components/Notifications/src/Toasts/Compat/Desktop/ManifestHelper.cs new file mode 100644 index 000000000..30bea256b --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/ManifestHelper.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Diagnostics; +using System.IO; +using System.Xml; +using Windows.ApplicationModel; + +namespace CommunityToolkit.Notifications +{ + internal class ManifestHelper + { + private XmlDocument _doc; + private XmlNamespaceManager _namespaceManager; + + public ManifestHelper() + { + var appxManifestPath = Path.Combine(Package.Current.InstalledLocation.Path, "AppxManifest.xml"); + var doc = new XmlDocument(); + doc.Load(appxManifestPath); + + var namespaceManager = new XmlNamespaceManager(doc.NameTable); + namespaceManager.AddNamespace("default", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); + namespaceManager.AddNamespace("desktop", "http://schemas.microsoft.com/appx/manifest/desktop/windows10"); + namespaceManager.AddNamespace("com", "http://schemas.microsoft.com/appx/manifest/com/windows10"); + + _doc = doc; + _namespaceManager = namespaceManager; + } + + internal string GetAumidFromPackageManifest() + { + var appNode = _doc.SelectSingleNode("/default:Package/default:Applications/default:Application[1]", _namespaceManager); + + if (appNode == null) + { + throw new InvalidOperationException("Your MSIX app manifest must have an entry."); + } + + return Package.Current.Id.FamilyName + "!" + appNode.Attributes["Id"].Value; + } + + internal string GetClsidFromPackageManifest() + { + var activatorClsidNode = _doc.SelectSingleNode("/default:Package/default:Applications/default:Application[1]/default:Extensions/desktop:Extension[@Category='windows.toastNotificationActivation']/desktop:ToastNotificationActivation/@ToastActivatorCLSID", _namespaceManager); + + if (activatorClsidNode == null) + { + throw new InvalidOperationException("Your app manifest must have a toastNotificationActivation extension with a valid ToastActivatorCLSID specified."); + } + + var clsid = activatorClsidNode.Value; + + // Make sure they have a COM class registration matching the CLSID + var comClassNode = _doc.SelectSingleNode($"/default:Package/default:Applications/default:Application[1]/default:Extensions/com:Extension[@Category='windows.comServer']/com:ComServer/com:ExeServer/com:Class[@Id='{clsid}']", _namespaceManager); + + if (comClassNode == null) + { + throw new InvalidOperationException("Your app manifest must have a comServer extension with a class ID matching your toastNotificationActivator's CLSID."); + } + + // Make sure they have a COM class registration matching their current process executable + var comExeServerNode = comClassNode.ParentNode; + + var specifiedExeRelativePath = comExeServerNode.Attributes["Executable"].Value; + var specifiedExeFullPath = Path.Combine(Package.Current.InstalledLocation.Path, specifiedExeRelativePath); + var actualExeFullPath = Process.GetCurrentProcess().MainModule.FileName; + + if (specifiedExeFullPath != actualExeFullPath) + { + var correctExeRelativePath = actualExeFullPath.Substring(Package.Current.InstalledLocation.Path.Length + 1); + throw new InvalidOperationException($"Your app manifest's comServer extension's Executable value is incorrect. It should be \"{correctExeRelativePath}\"."); + } + + // Make sure their arguments are set correctly + var argumentsNode = comExeServerNode.Attributes.GetNamedItem("Arguments"); + if (argumentsNode == null || argumentsNode.Value != "-ToastActivated") + { + throw new InvalidOperationException("Your app manifest's comServer extension for toast activation must have its Arguments set exactly to \"-ToastActivated\""); + } + + return clsid; + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/CAppResolver.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/CAppResolver.cs new file mode 100644 index 000000000..4a3eca450 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/CAppResolver.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.Notifications +{ + [ComImport] + [Guid("660b90c8-73a9-4b58-8cae-355b7f55341b")] + [ClassInterface(ClassInterfaceType.None)] + internal class CAppResolver + { + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/IApplicationResolver.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/IApplicationResolver.cs new file mode 100644 index 000000000..13c82866e --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/IApplicationResolver.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.Notifications +{ + [ComImport] + [Guid("DE25675A-72DE-44b4-9373-05170450C140")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IApplicationResolver + { + void GetAppIDForShortcut( + IntPtr psi, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszAppID); + + void GetAppIDForShortcutObject( + IntPtr psl, + IntPtr psi, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszAppID); + + void GetAppIDForWindow( + int hwnd, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszAppID, + [Out] out bool pfPinningPrevented, + [Out] out bool pfExplicitAppID, + [Out] out bool pfEmbeddedShortcutValid); + + /// + /// Main way to obtain app ID for any process. Calls GetShortcutPathOrAppIdFromPid + /// + void GetAppIDForProcess( + uint dwProcessID, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszAppID, + [Out] out bool pfPinningPrevented, + [Out] out bool pfExplicitAppID, + [Out] out bool pfEmbeddedShortcutValid); + + void GetShortcutForProcess( + uint dwProcessID, + [Out] out IntPtr ppsi); + + void GetBestShortcutForAppID( + string pszAppID, + [Out] out IShellItem ppsi); + + void GetBestShortcutAndAppIDForAppPath( + string pszAppPath, + [Out] out IntPtr ppsi, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszAppID); + + void CanPinApp(IntPtr psi); + + void CanPinAppShortcut( + IntPtr psl, + IntPtr psi); + + void GetRelaunchProperties( + int hwnd, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszAppID, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszCmdLine, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszIconResource, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszDisplayNameResource, + [Out] bool pfPinnable); + + void GenerateShortcutFromWindowProperties( + int hwnd, + [Out] out IntPtr ppsi); + + void GenerateShortcutFromItemProperties( + IntPtr psi2, + [Out] out IntPtr ppsi); + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItem.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItem.cs new file mode 100644 index 000000000..454b0335a --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItem.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.Notifications +{ + [ComImport] + [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IShellItem + { + void BindToHandler( + IntPtr pbc, + IntPtr bhid, + IntPtr riid, + [Out] out IntPtr ppv); + + void GetParent( + [Out] out IShellItem ppsi); + + void GetDisplayName( + int sigdnName, + [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszName); + + void GetAttributes( + int sfgaoMask, + [Out] out int psfgaoAttribs); + + void Compare( + IShellItem psi, + int hint, + [Out] out int piOrder); + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItemImageFactory.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItemImageFactory.cs new file mode 100644 index 000000000..7fdb10c36 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/IShellItemImageFactory.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.Notifications +{ + [ComImport] + [Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IShellItemImageFactory + { + void GetImage( + [In, MarshalAs(UnmanagedType.Struct)] SIZE size, + [In] SIIGBF flags, + [Out] out IntPtr phbm); + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/NativeMethods.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/NativeMethods.cs new file mode 100644 index 000000000..a31b549b7 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/NativeMethods.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace CommunityToolkit.Notifications +{ + internal class NativeMethods + { + [DllImport("gdi32.dll", EntryPoint = "DeleteObject")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DeleteObject([In] IntPtr hObject); + + [DllImport("ole32.dll")] + internal static extern int CoRegisterClassObject( + [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, + [MarshalAs(UnmanagedType.IUnknown)] object pUnk, + uint dwClsContext, + uint flags, + out uint lpdwRegister); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/SIIGBF.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/SIIGBF.cs new file mode 100644 index 000000000..c0036a2e7 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/SIIGBF.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; + +namespace CommunityToolkit.Notifications +{ + [Flags] + internal enum SIIGBF + { + ResizeToFit = 0x00, + BiggerSizeOk = 0x01, + MemoryOnly = 0x02, + IconOnly = 0x04, + ThumbnailOnly = 0x08, + InCacheOnly = 0x10, + ScaleUp = 0x00000100 + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Native/SIZE.cs b/components/Notifications/src/Toasts/Compat/Desktop/Native/SIZE.cs new file mode 100644 index 000000000..902e34a50 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Native/SIZE.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System.Runtime.InteropServices; + +namespace CommunityToolkit.Notifications +{ + [StructLayout(LayoutKind.Sequential)] + internal struct SIZE + { + internal int X; + internal int Y; + + internal SIZE(int x, int y) + { + this.X = x; + this.Y = y; + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/NotificationActivator.cs b/components/Notifications/src/Toasts/Compat/Desktop/NotificationActivator.cs new file mode 100644 index 000000000..b84166e68 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/NotificationActivator.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Apps must implement this activator to handle notification activation. + /// + [Obsolete("You can now subscribe to activation by simpy using the ToastNotificationManagerCompat.OnActivated event. We recommend deleting your NotificationActivator and switching to using the event.")] + public abstract class NotificationActivator : NotificationActivator.INotificationActivationCallback + { + /// + public void Activate(string appUserModelId, string invokedArgs, NOTIFICATION_USER_INPUT_DATA[] data, uint dataCount) + { + OnActivated(invokedArgs, new NotificationUserInput(data), appUserModelId); + } + + /// + /// This method will be called when the user clicks on a foreground or background activation on a toast. Parent app must implement this method. + /// + /// The arguments from the original notification. This is either the launch argument if the user clicked the body of your toast, or the arguments from a button on your toast. + /// Text and selection values that the user entered in your toast. + /// Your AUMID. + public abstract void OnActivated(string arguments, NotificationUserInput userInput, string appUserModelId); + + /// + /// A single user input key/value pair. + /// + [StructLayout(LayoutKind.Sequential)] + [Serializable] + public struct NOTIFICATION_USER_INPUT_DATA + { + /// + /// The key of the user input. + /// + [MarshalAs(UnmanagedType.LPWStr)] + public string Key; + + /// + /// The value of the user input. + /// + [MarshalAs(UnmanagedType.LPWStr)] + public string Value; + } + + /// + /// The COM callback that is triggered when your notification is clicked. + /// + [ComImport] + [Guid("53E31837-6600-4A81-9395-75CFFE746F94")] + [ComVisible(true)] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface INotificationActivationCallback + { + /// + /// The method called when your notification is clicked. + /// + /// The app id of the app that sent the toast. + /// The activation arguments from the toast. + /// The user input from the toast. + /// The number of user inputs. + void Activate( + [In, MarshalAs(UnmanagedType.LPWStr)] + string appUserModelId, + [In, MarshalAs(UnmanagedType.LPWStr)] + string invokedArgs, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] NOTIFICATION_USER_INPUT_DATA[] data, + [In, MarshalAs(UnmanagedType.U4)] + uint dataCount); + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/NotificationUserInput.cs b/components/Notifications/src/Toasts/Compat/Desktop/NotificationUserInput.cs new file mode 100644 index 000000000..173b8eb67 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/NotificationUserInput.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Text and selection values that the user entered on your notification. The Key is the ID of the input, and the Value is what the user entered. + /// + public class NotificationUserInput : IReadOnlyDictionary + { +#pragma warning disable CS0618 // Type or member is obsolete + private NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] _data; +#pragma warning restore CS0618 // Type or member is obsolete + +#pragma warning disable CS0618 // Type or member is obsolete + internal NotificationUserInput(NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] data) +#pragma warning restore CS0618 // Type or member is obsolete + { + _data = data; + } + + /// + /// Gets the value of an input with the given key. + /// + /// The key of the input. + /// The value of the input. + public string this[string key] => _data.First(i => i.Key == key).Value; + + /// + /// Gets all the keys of the inputs. + /// + public IEnumerable Keys => _data.Select(i => i.Key); + + /// + /// Gets all the values of the inputs. + /// + public IEnumerable Values => _data.Select(i => i.Value); + + /// + /// Gets how many inputs there were. + /// + public int Count => _data.Length; + + /// + /// Checks whether any inputs have the given key. + /// + /// The key to look for. + /// A boolean representing whether any inputs have the given key. + public bool ContainsKey(string key) + { + return _data.Any(i => i.Key == key); + } + + /// + /// Gets an enumerator of the inputs. + /// + /// An enumerator of the inputs. + public IEnumerator> GetEnumerator() + { + return _data.Select(i => new KeyValuePair(i.Key, i.Value)).GetEnumerator(); + } + + /// + /// Tries to get the input value for the given key. + /// + /// The key of the input to look for. + /// The value of the input. + /// True if found an input with the specified key, else false. + public bool TryGetValue(string key, out string value) + { + foreach (var item in _data) + { + if (item.Key == key) + { + value = item.Value; + return true; + } + } + + value = null; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/OnActivated.cs b/components/Notifications/src/Toasts/Compat/Desktop/OnActivated.cs new file mode 100644 index 000000000..85952352b --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/OnActivated.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +namespace CommunityToolkit.Notifications +{ + /// + /// Event triggered when a notification is clicked. + /// + /// Arguments that specify what action was taken and the user inputs. + public delegate void OnActivated(ToastNotificationActivatedEventArgsCompat e); +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/ToastNotificationActivatedEventArgsCompat.cs b/components/Notifications/src/Toasts/Compat/Desktop/ToastNotificationActivatedEventArgsCompat.cs new file mode 100644 index 000000000..3083c387b --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/ToastNotificationActivatedEventArgsCompat.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using Windows.Foundation.Collections; + +namespace CommunityToolkit.Notifications +{ + /// + /// Provides information about an event that occurs when the app is activated because a user tapped on the body of a toast notification or performed an action inside a toast notification. + /// + public class ToastNotificationActivatedEventArgsCompat + { + internal ToastNotificationActivatedEventArgsCompat() + { + } + + /// + /// Gets the arguments from the toast XML payload related to the action that was invoked on the toast. + /// + public string Argument { get; internal set; } + + /// + /// Gets a set of values that you can use to obtain the user input from an interactive toast notification. + /// + public ValueSet UserInput { get; internal set; } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/Desktop/Win32AppInfo.cs b/components/Notifications/src/Toasts/Compat/Desktop/Win32AppInfo.cs new file mode 100644 index 000000000..2294d6bc1 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/Desktop/Win32AppInfo.cs @@ -0,0 +1,296 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WIN32 + +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace CommunityToolkit.Notifications +{ + internal class Win32AppInfo + { + /// + /// If an AUMID is greater than 129 characters, scheduled toast notification APIs will throw an exception. + /// + private const int AUMID_MAX_LENGTH = 129; + + public string Aumid { get; set; } + + /// + /// Gets the AUMID before it was fixed up with the backslash issue + /// + public string Pre7_0_1Aumid { get; private set; } + + public string DisplayName { get; set; } + + public string IconPath { get; set; } + + public static Win32AppInfo Get() + { + var process = Process.GetCurrentProcess(); + + // First get the app ID + IApplicationResolver appResolver = (IApplicationResolver)new CAppResolver(); + appResolver.GetAppIDForProcess(Convert.ToUInt32(process.Id), out string appId, out _, out _, out _); + + string aumid; + string pre7_0_1Aumid = null; + + // If the app ID is too long + if (appId.Length > AUMID_MAX_LENGTH) + { + // Hash the AUMID + aumid = HashAppId(appId); + } + + // Else if it contains a backslash + else if (appId.Contains('\\')) + { + // For versions 19042 and older of Windows 10, we can't use backslashes - Issue #3870 + // So we change it to not include those + aumid = appId.Replace('\\', '/'); + pre7_0_1Aumid = appId; + } + else + { + // Use as-is + aumid = appId; + } + + // Then try to get the shortcut (for display name and icon) + IShellItem shortcutItem = null; + try + { + appResolver.GetBestShortcutForAppID(appId, out shortcutItem); + } + catch + { + } + + string displayName = null; + string iconPath = null; + + // First we attempt to use display assets from the shortcut itself + if (shortcutItem != null) + { + try + { + shortcutItem.GetDisplayName(0, out displayName); + + ((IShellItemImageFactory)shortcutItem).GetImage(new SIZE(48, 48), SIIGBF.IconOnly | SIIGBF.BiggerSizeOk, out IntPtr nativeHBitmap); + + if (nativeHBitmap != IntPtr.Zero) + { + try + { + Bitmap bmp = Bitmap.FromHbitmap(nativeHBitmap); + + if (IsAlphaBitmap(bmp, out var bmpData)) + { + var alphaBitmap = GetAlphaBitmapFromBitmapData(bmpData); + iconPath = SaveIconToAppPath(alphaBitmap, aumid); + } + else + { + iconPath = SaveIconToAppPath(bmp, aumid); + } + } + catch + { + } + + NativeMethods.DeleteObject(nativeHBitmap); + } + } + catch + { + } + } + + // If we didn't get a display name from shortcut + if (string.IsNullOrWhiteSpace(displayName)) + { + // We use the one from the process + displayName = GetDisplayNameFromCurrentProcess(process); + } + + // If we didn't get an icon from shortcut + if (string.IsNullOrWhiteSpace(iconPath)) + { + // We use the one from the process + iconPath = ExtractAndObtainIconFromCurrentProcess(process, aumid); + } + + return new Win32AppInfo() + { + Aumid = aumid, + Pre7_0_1Aumid = pre7_0_1Aumid, + DisplayName = displayName, + IconPath = iconPath + }; + } + + private static string HashAppId(string appId) + { + using (SHA1 sha1 = SHA1.Create()) + { + byte[] hashedBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(appId)); + return string.Join(string.Empty, hashedBytes.Select(b => b.ToString("X2"))); + } + } + + private static string GetDisplayNameFromCurrentProcess(Process process) + { + // If AssemblyTitle is set, use that + var assemblyTitleAttr = Assembly.GetEntryAssembly()?.GetCustomAttribute(); + if (assemblyTitleAttr != null) + { + return assemblyTitleAttr.Title; + } + + // Otherwise, fall back to process name + return process.ProcessName; + } + + private static string ExtractAndObtainIconFromCurrentProcess(Process process, string aumid) + { + return ExtractAndObtainIconFromPath(process.MainModule.FileName, aumid); + } + + private static string ExtractAndObtainIconFromPath(string pathToExtract, string aumid) + { + try + { + // Extract the icon + var icon = Icon.ExtractAssociatedIcon(pathToExtract); + + using (var bmp = icon.ToBitmap()) + { + return SaveIconToAppPath(bmp, aumid); + } + } + catch + { + return null; + } + } + + private static string SaveIconToAppPath(Bitmap bitmap, string aumid) + { + try + { + var path = Path.Combine(GetAppDataFolderPath(aumid), "Icon.png"); + + // Ensure the directories exist + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + bitmap.Save(path, ImageFormat.Png); + + return path; + } + catch + { + return null; + } + } + + /// + /// Gets the app data folder path within the ToastNotificationManagerCompat folder, used for storing icon assets and any additional data. + /// + /// Returns a string of the absolute folder path. + public static string GetAppDataFolderPath(string aumid) + { + string conciseAumid = aumid.Contains('\\') || aumid.Contains('/') ? GenerateGuid(aumid) : aumid; + + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ToastNotificationManagerCompat", "Apps", conciseAumid); + } + + // From https://stackoverflow.com/a/9291151 + private static Bitmap GetAlphaBitmapFromBitmapData(BitmapData bmpData) + { + return new Bitmap( + bmpData.Width, + bmpData.Height, + bmpData.Stride, + PixelFormat.Format32bppArgb, + bmpData.Scan0); + } + + // From https://stackoverflow.com/a/9291151 + private static bool IsAlphaBitmap(Bitmap bmp, out BitmapData bmpData) + { + var bmBounds = new Rectangle(0, 0, bmp.Width, bmp.Height); + + bmpData = bmp.LockBits(bmBounds, ImageLockMode.ReadOnly, bmp.PixelFormat); + + try + { + for (int y = 0; y <= bmpData.Height - 1; y++) + { + for (int x = 0; x <= bmpData.Width - 1; x++) + { + var pixelColor = Color.FromArgb( + Marshal.ReadInt32(bmpData.Scan0, (bmpData.Stride * y) + (4 * x))); + + if (pixelColor.A > 0 & pixelColor.A < 255) + { + return true; + } + } + } + } + finally + { + bmp.UnlockBits(bmpData); + } + + return false; + } + + /// + /// From https://stackoverflow.com/a/41622689/1454643 + /// Generates Guid based on String. Key assumption for this algorithm is that name is unique (across where it it's being used) + /// and if name byte length is less than 16 - it will be fetched directly into guid, if over 16 bytes - then we compute sha-1 + /// hash from string and then pass it to guid. + /// + /// Unique name which is unique across where this guid will be used. + /// For example "706C7567-696E-7300-0000-000000000000" for "plugins" + public static string GenerateGuid(string name) + { + byte[] buf = Encoding.UTF8.GetBytes(name); + byte[] guid = new byte[16]; + if (buf.Length < 16) + { + Array.Copy(buf, guid, buf.Length); + } + else + { + using (SHA1 sha1 = SHA1.Create()) + { + byte[] hash = sha1.ComputeHash(buf); + + // Hash is 20 bytes, but we need 16. We loose some of "uniqueness", but I doubt it will be fatal + Array.Copy(hash, guid, 16); + } + } + + // Don't use Guid constructor, it tends to swap bytes. We want to preserve original string as hex dump. + string guidS = $"{guid[0]:X2}{guid[1]:X2}{guid[2]:X2}{guid[3]:X2}-{guid[4]:X2}{guid[5]:X2}-{guid[6]:X2}{guid[7]:X2}-{guid[8]:X2}{guid[9]:X2}-{guid[10]:X2}{guid[11]:X2}{guid[12]:X2}{guid[13]:X2}{guid[14]:X2}{guid[15]:X2}"; + + return guidS; + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs b/components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs new file mode 100644 index 000000000..2741910e0 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP + +using System.Collections.Generic; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Manages the toast notifications for an app including the ability the clear all toast history and removing individual toasts. + /// + public class ToastNotificationHistoryCompat + { + private string _aumid; + private ToastNotificationHistory _history; + + internal ToastNotificationHistoryCompat(string aumid) + { + _aumid = aumid; + _history = ToastNotificationManager.History; + } + + /// + /// Removes all notifications sent by this app from action center. + /// + public void Clear() + { + if (_aumid != null) + { + _history.Clear(_aumid); + } + else + { + _history.Clear(); + } + } + + /// + /// Gets all notifications sent by this app that are currently still in Action Center. + /// + /// A collection of toasts. + public IReadOnlyList GetHistory() + { + return _aumid != null ? _history.GetHistory(_aumid) : _history.GetHistory(); + } + + /// + /// Removes an individual toast, with the specified tag label, from action center. + /// + /// The tag label of the toast notification to be removed. + public void Remove(string tag) + { +#if WIN32 + if (_aumid != null) + { + _history.Remove(tag, ToastNotificationManagerCompat.DEFAULT_GROUP, _aumid); + } + else + { + _history.Remove(tag); + } +#else + _history.Remove(tag); +#endif + } + + /// + /// Removes a toast notification from the action using the notification's tag and group labels. + /// + /// The tag label of the toast notification to be removed. + /// The group label of the toast notification to be removed. + public void Remove(string tag, string group) + { + if (_aumid != null) + { + _history.Remove(tag, group, _aumid); + } + else + { + _history.Remove(tag, group); + } + } + + /// + /// Removes a group of toast notifications, identified by the specified group label, from action center. + /// + /// The group label of the toast notifications to be removed. + public void RemoveGroup(string group) + { + if (_aumid != null) + { + _history.RemoveGroup(group, _aumid); + } + else + { + _history.RemoveGroup(group); + } + } + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs b/components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs new file mode 100644 index 000000000..de2b6e1a9 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs @@ -0,0 +1,657 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using Windows.ApplicationModel; +using Windows.Foundation.Collections; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Provides access to sending and managing toast notifications. Works for all types of apps, even Win32 non-MSIX/sparse apps. + /// + public static class ToastNotificationManagerCompat + { +#if WIN32 + private const string TOAST_ACTIVATED_LAUNCH_ARG = "-ToastActivated"; + private const string REG_HAS_SENT_NOTIFICATION = "HasSentNotification"; + private const string REG_HAS_7_0_1_FIX = "Has7.0.1Fix"; + internal const string DEFAULT_GROUP = "toolkitGroupNull"; + + private const int CLASS_E_NOAGGREGATION = -2147221232; + private const int E_NOINTERFACE = -2147467262; + private const int CLSCTX_LOCAL_SERVER = 4; + private const int REGCLS_MULTIPLEUSE = 1; + private const int S_OK = 0; + private static readonly Guid IUnknownGuid = new Guid("00000000-0000-0000-C000-000000000046"); + + private static bool _registeredOnActivated; + private static List _onActivated = new List(); + + private static bool _hasSentNotification; + + /// + /// Event that is triggered when a notification or notification button is clicked. Subscribe to this event in your app's initial startup code. + /// + public static event OnActivated OnActivated + { + add + { + lock (_onActivated) + { + if (!_registeredOnActivated) + { + // Desktop Bridge apps will dynamically register upon first subscription to event + try + { + CreateAndRegisterActivator(); + } + catch (Exception ex) + { + _initializeEx = new InvalidOperationException("Failed to register notification activator", ex); + } + } + + _onActivated.Add(value); + } + } + + remove + { + lock (_onActivated) + { + _onActivated.Remove(value); + } + } + } + + internal static void OnActivatedInternal(string args, Internal.InternalNotificationActivator.NOTIFICATION_USER_INPUT_DATA[] input, string aumid) + { + ValueSet userInput = new ValueSet(); + + if (input != null) + { + foreach (var val in input) + { + userInput.Add(val.Key, val.Value); + } + } + + var e = new ToastNotificationActivatedEventArgsCompat() + { + Argument = args, + UserInput = userInput + }; + + OnActivated[] listeners; + lock (_onActivated) + { + listeners = _onActivated.ToArray(); + } + + foreach (var listener in listeners) + { + listener(e); + } + } + + private static string _win32Aumid; + private static string _clsid; + private static InvalidOperationException _initializeEx; + + static ToastNotificationManagerCompat() + { + try + { + Initialize(); + } + catch (Exception ex) + { + // We catch the exception so that things like subscribing to the event handler doesn't crash app + _initializeEx = new InvalidOperationException("Failed initializing notifications", ex); + } + } + + private static void Initialize() + { + // If containerized + if (DesktopBridgeHelpers.IsContainerized()) + { + // No need to do anything additional, already registered through manifest + return; + } + + Win32AppInfo win32AppInfo = null; + + // If sparse + if (DesktopBridgeHelpers.HasIdentity()) + { + _win32Aumid = new ManifestHelper().GetAumidFromPackageManifest(); + } + else + { + win32AppInfo = Win32AppInfo.Get(); + _win32Aumid = win32AppInfo.Aumid; + } + + // Create and register activator + var activatorType = CreateAndRegisterActivator(); + + // Register via registry + using (var rootKey = Registry.CurrentUser.CreateSubKey(GetRegistrySubKey())) + { + // If they don't have identity, we need to specify the display assets + if (!DesktopBridgeHelpers.HasIdentity()) + { + // Set the display name and icon uri + rootKey.SetValue("DisplayName", win32AppInfo.DisplayName); + + if (win32AppInfo.IconPath != null) + { + rootKey.SetValue("IconUri", win32AppInfo.IconPath); + } + else + { + if (rootKey.GetValue("IconUri") != null) + { + rootKey.DeleteValue("IconUri"); + } + } + + // Background color only appears in the settings page, format is + // hex without leading #, like "FFDDDDDD" + rootKey.SetValue("IconBackgroundColor", "FFDDDDDD"); + + // Additionally, we need to read whether they've sent a notification before + _hasSentNotification = rootKey.GetValue(REG_HAS_SENT_NOTIFICATION) != null; + + // And read if we've already applied the 7_0_1 fix + bool has7_0_1fix = rootKey.GetValue(REG_HAS_7_0_1_FIX) != null; + + // If it doesn't have the fix yet + if (!has7_0_1fix) + { + // If the AUMID changed + if (win32AppInfo.Pre7_0_1Aumid != null) + { + // Uninstall the old AUMID + CleanUpOldAumid(win32AppInfo.Pre7_0_1Aumid); + } + + // Set that it has the fix so we don't try uninstalling again in the future + rootKey.SetValue(REG_HAS_7_0_1_FIX, 1); + } + } + + rootKey.SetValue("CustomActivator", string.Format("{{{0}}}", activatorType.GUID)); + } + } + + private static string GetRegistrySubKey() + { + return GetRegistrySubKey(_win32Aumid); + } + + private static string GetRegistrySubKey(string win32Aumid) + { + return @"Software\Classes\AppUserModelId\" + win32Aumid; + } + + private static Type CreateActivatorType() + { + // https://stackoverflow.com/questions/24069352/c-sharp-typebuilder-generate-class-with-function-dynamically + // For .NET Core we use https://stackoverflow.com/questions/36937276/is-there-any-replace-of-assemblybuilder-definedynamicassembly-in-net-core + AssemblyName aName = new AssemblyName("DynamicComActivator"); + AssemblyBuilder aBuilder = AssemblyBuilder.DefineDynamicAssembly(aName, AssemblyBuilderAccess.Run); + + // For a single-module assembly, the module name is usually the assembly name plus an extension. + ModuleBuilder mb = aBuilder.DefineDynamicModule(aName.Name); + + // Create class which extends NotificationActivator + TypeBuilder tb = mb.DefineType( + name: "MyNotificationActivator", + attr: TypeAttributes.Public, + parent: typeof(Internal.InternalNotificationActivator), + interfaces: new Type[0]); + + if (DesktopBridgeHelpers.IsContainerized()) + { + _clsid = new ManifestHelper().GetClsidFromPackageManifest(); + } + else + { + _clsid = Win32AppInfo.GenerateGuid(_win32Aumid); + } + + tb.SetCustomAttribute(new CustomAttributeBuilder( + con: typeof(GuidAttribute).GetConstructor(new Type[] { typeof(string) }), + constructorArgs: new object[] { _clsid })); + + tb.SetCustomAttribute(new CustomAttributeBuilder( + con: typeof(ComVisibleAttribute).GetConstructor(new Type[] { typeof(bool) }), + constructorArgs: new object[] { true })); + + tb.SetCustomAttribute(new CustomAttributeBuilder( +#pragma warning disable CS0618 // Type or member is obsolete + con: typeof(ComSourceInterfacesAttribute).GetConstructor(new Type[] { typeof(Type) }), +#pragma warning restore CS0618 // Type or member is obsolete + constructorArgs: new object[] { typeof(Internal.InternalNotificationActivator.INotificationActivationCallback) })); + + tb.SetCustomAttribute(new CustomAttributeBuilder( + con: typeof(ClassInterfaceAttribute).GetConstructor(new Type[] { typeof(ClassInterfaceType) }), + constructorArgs: new object[] { ClassInterfaceType.None })); + + return tb.CreateType(); + } + + private static Type CreateAndRegisterActivator() + { + var activatorType = CreateActivatorType(); + RegisterActivator(activatorType); + _registeredOnActivated = true; + return activatorType; + } + + private static void RegisterActivator(Type activatorType) + { + if (!DesktopBridgeHelpers.IsContainerized()) + { + string exePath = Process.GetCurrentProcess().MainModule.FileName; + RegisterComServer(activatorType, exePath); + } + + // Big thanks to FrecherxDachs for figuring out the following code which works in .NET Core 3: https://github.com/FrecherxDachs/UwpNotificationNetCoreTest + var uuid = activatorType.GUID; + NativeMethods.CoRegisterClassObject( + uuid, + new NotificationActivatorClassFactory(activatorType), + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + out _); + } + + private static void RegisterComServer(Type activatorType, string exePath) + { + // We register the EXE to start up when the notification is activated + string regString = string.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}", activatorType.GUID); + using (var key = Registry.CurrentUser.CreateSubKey(regString + "\\LocalServer32")) + { + // Include a flag so we know this was a toast activation and should wait for COM to process + // We also wrap EXE path in quotes for extra security + key.SetValue(null, '"' + exePath + '"' + " " + TOAST_ACTIVATED_LAUNCH_ARG); + } + + if (IsElevated) + { + //// For elevated apps, we need to ensure they'll activate in existing running process by adding + //// some values in local machine + using (var key = Registry.LocalMachine.CreateSubKey(regString)) + { + // Same as above, except also including AppId to link to our AppId entry below + using (var localServer32 = key.CreateSubKey("LocalServer32")) + { + localServer32.SetValue(null, '"' + exePath + '"' + " " + TOAST_ACTIVATED_LAUNCH_ARG); + } + + key.SetValue("AppId", "{" + activatorType.GUID + "}"); + } + + // This tells COM to match any client, so Action Center will activate our elevated process. + // More info: https://docs.microsoft.com/windows/win32/com/runas + using (var key = Registry.LocalMachine.CreateSubKey(string.Format("SOFTWARE\\Classes\\AppID\\{{{0}}}", activatorType.GUID))) + { + key.SetValue("RunAs", "Interactive User"); + } + } + } + + /// + /// Gets whether the current process was activated due to a toast activation. If so, the OnActivated event will be triggered soon after process launch. + /// + /// True if the current process was activated due to a toast activation, otherwise false. + public static bool WasCurrentProcessToastActivated() + { + return Environment.GetCommandLineArgs().Contains(TOAST_ACTIVATED_LAUNCH_ARG); + } + + [ComImport] + [Guid("00000001-0000-0000-C000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IClassFactory + { + [PreserveSig] + int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject); + + [PreserveSig] + int LockServer(bool fLock); + } + + private class NotificationActivatorClassFactory : IClassFactory + { + private Type _activatorType; + + public NotificationActivatorClassFactory(Type activatorType) + { + _activatorType = activatorType; + } + + public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject) + { + ppvObject = IntPtr.Zero; + + if (pUnkOuter != IntPtr.Zero) + { + Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION); + } + + if (riid == _activatorType.GUID || riid == IUnknownGuid) + { + // Create the instance of the .NET object + ppvObject = Marshal.GetComInterfaceForObject( + Activator.CreateInstance(_activatorType), + typeof(Internal.InternalNotificationActivator.INotificationActivationCallback)); + } + else + { + // The object that ppvObject points to does not support the + // interface identified by riid. + Marshal.ThrowExceptionForHR(E_NOINTERFACE); + } + + return S_OK; + } + + public int LockServer(bool fLock) + { + return S_OK; + } + } + + private static bool IsElevated + { + get + { + return new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent()).IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator); + } + } +#endif + + /// + /// Creates a toast notifier. + /// + /// An instance of the toast notifier. + public static ToastNotifierCompat CreateToastNotifier() + { +#if WIN32 + if (_initializeEx != null) + { + throw _initializeEx; + } + + if (DesktopBridgeHelpers.HasIdentity()) + { + return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier()); + } + else + { + return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier(_win32Aumid)); + } +#else + return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier()); +#endif + } + + /// + /// Gets the object. + /// + public static ToastNotificationHistoryCompat History + { + get + { +#if WIN32 + if (_initializeEx != null) + { + throw _initializeEx; + } + + return new ToastNotificationHistoryCompat(DesktopBridgeHelpers.HasIdentity() ? null : _win32Aumid); +#else + return new ToastNotificationHistoryCompat(null); +#endif + } + } + + /// + /// Gets a value indicating whether http images can be used within toasts. This is true if running with package identity (UWP, MSIX, or sparse package). + /// + public static bool CanUseHttpImages + { + get + { +#if WIN32 + return DesktopBridgeHelpers.HasIdentity(); +#else + return true; +#endif + } + } + +#if WIN32 + /// + /// If you're not using MSIX, call this when your app is being uninstalled to properly clean up all notifications and notification-related resources. Note that this must be called from your app's main EXE (the one that you used notifications for) and not a separate uninstall EXE. If called from a MSIX app, this method no-ops. + /// + public static void Uninstall() + { + if (DesktopBridgeHelpers.IsContainerized()) + { + // Packaged containerized apps automatically clean everything up already + return; + } + + if (!DesktopBridgeHelpers.HasIdentity()) + { + try + { + // Remove all scheduled notifications (do this first before clearing current notifications) + var notifier = CreateToastNotifier(); + foreach (var scheduled in notifier.GetScheduledToastNotifications()) + { + try + { + notifier.RemoveFromSchedule(scheduled); + } + catch + { + } + } + } + catch + { + } + + try + { + // Clear all current notifications + History.Clear(); + } + catch + { + } + } + + try + { + // Remove registry key + if (_win32Aumid != null) + { + Registry.CurrentUser.DeleteSubKey(GetRegistrySubKey()); + } + } + catch + { + } + + try + { + if (_clsid != null) + { + try + { + Registry.CurrentUser.DeleteSubKeyTree(string.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}", _clsid)); + } + catch + { + } + + if (IsElevated) + { + try + { + Registry.LocalMachine.DeleteSubKeyTree(string.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}", _clsid)); + } + catch + { + } + + try + { + Registry.LocalMachine.DeleteSubKeyTree(string.Format("SOFTWARE\\Classes\\AppID\\{{{0}}}", _clsid)); + } + catch + { + } + } + } + } + catch + { + } + + try + { + // Delete any of the app files + var appDataFolderPath = Win32AppInfo.GetAppDataFolderPath(_win32Aumid); + if (Directory.Exists(appDataFolderPath)) + { + Directory.Delete(appDataFolderPath, recursive: true); + } + } + catch + { + } + } + + private static void CleanUpOldAumid(string oldAumid) + { + try + { + // Remove all scheduled notifications (do this first before clearing current notifications) + var notifier = ToastNotificationManager.CreateToastNotifier(oldAumid); + foreach (var scheduled in notifier.GetScheduledToastNotifications()) + { + try + { + notifier.RemoveFromSchedule(scheduled); + } + catch + { + } + } + } + catch + { + } + + try + { + // Clear all current notifications + ToastNotificationManager.History.Clear(oldAumid); + } + catch + { + } + + try + { + // Remove registry key + Registry.CurrentUser.DeleteSubKey(GetRegistrySubKey(oldAumid)); + } + catch + { + } + + try + { + // Delete any of the app files + var appDataFolderPath = Win32AppInfo.GetAppDataFolderPath(oldAumid); + if (Directory.Exists(appDataFolderPath)) + { + Directory.Delete(appDataFolderPath, recursive: true); + } + } + catch + { + } + } +#endif + +#if WIN32 + internal static void SetHasSentToastNotification() + { + // For plain Win32 apps, record that we've sent a notification + if (!_hasSentNotification && !DesktopBridgeHelpers.HasIdentity()) + { + _hasSentNotification = true; + + try + { + using (var rootKey = Registry.CurrentUser.CreateSubKey(GetRegistrySubKey())) + { + rootKey.SetValue(REG_HAS_SENT_NOTIFICATION, 1); + } + } + catch + { + } + } + } + + internal static void PreRegisterIdentityLessApp() + { + // For plain Win32 apps, we first have to have send a toast notification once before using scheduled toasts. + if (!_hasSentNotification && !DesktopBridgeHelpers.HasIdentity()) + { + const string tag = "toolkit1stNotif"; + + // Show the toast + new ToastContentBuilder() + .AddText("New notification") + .Show(toast => + { + // We'll hide the popup and set the toast to expire in case removing doesn't work + toast.SuppressPopup = true; + toast.Tag = tag; + toast.ExpirationTime = DateTime.Now.AddSeconds(15); + }); + + // And then remove it + ToastNotificationManagerCompat.History.Remove(tag); + } + } +#endif + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs b/components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs new file mode 100644 index 000000000..62afbc775 --- /dev/null +++ b/components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP + +using System.Collections.Generic; +using Windows.UI.Notifications; + +namespace CommunityToolkit.Notifications +{ + /// + /// Allows you to show and schedule toast notifications. + /// + public sealed class ToastNotifierCompat + { + private ToastNotifier _notifier; + + internal ToastNotifierCompat(ToastNotifier notifier) + { + _notifier = notifier; + } + + /// + /// Displays the specified toast notification. + /// + /// The object that contains the content of the toast notification to display. + public void Show(ToastNotification notification) + { +#if WIN32 + PreprocessToast(notification); +#endif + + _notifier.Show(notification); + +#if WIN32 + ToastNotificationManagerCompat.SetHasSentToastNotification(); +#endif + } + + /// + /// Hides the specified toast notification from the screen (moves it into Action Center). + /// + /// The object that specifies the toast to hide. + public void Hide(ToastNotification notification) + { +#if WIN32 + PreprocessToast(notification); +#endif + + _notifier.Hide(notification); + } + + /// + /// Adds a ScheduledToastNotification for later display by Windows. + /// + /// The scheduled toast notification, which includes its content and timing instructions. + public void AddToSchedule(ScheduledToastNotification scheduledToast) + { +#if WIN32 + ToastNotificationManagerCompat.PreRegisterIdentityLessApp(); + + PreprocessScheduledToast(scheduledToast); +#endif + + _notifier.AddToSchedule(scheduledToast); + } + + /// + /// Cancels the scheduled display of a specified ScheduledToastNotification. + /// + /// The notification to remove from the schedule. + public void RemoveFromSchedule(ScheduledToastNotification scheduledToast) + { +#if WIN32 + PreprocessScheduledToast(scheduledToast); +#endif + + _notifier.RemoveFromSchedule(scheduledToast); + } + + /// + /// Gets the collection of ScheduledToastNotification objects that this app has scheduled for display. + /// + /// The collection of scheduled toast notifications that the app bound to this notifier has scheduled for timed display. + public IReadOnlyList GetScheduledToastNotifications() + { + return _notifier.GetScheduledToastNotifications(); + } + + /// + /// Updates the existing toast notification that has the specified tag and belongs to the specified notification group. + /// + /// An object that contains the updated info. + /// The identifier of the toast notification to update. + /// The ID of the ToastCollection that contains the notification. + /// A value that indicates the result of the update (failure, success, etc). + public NotificationUpdateResult Update(NotificationData data, string tag, string group) + { + return _notifier.Update(data, tag, group); + } + + /// + /// Updates the existing toast notification that has the specified tag. + /// + /// An object that contains the updated info. + /// The identifier of the toast notification to update. + /// A value that indicates the result of the update (failure, success, etc). + public NotificationUpdateResult Update(NotificationData data, string tag) + { +#if WIN32 + // For apps that don't have identity... + if (!DesktopBridgeHelpers.HasIdentity()) + { + // If group isn't specified, we have to add a group since otherwise can't remove without a group + return Update(data, tag, ToastNotificationManagerCompat.DEFAULT_GROUP); + } +#endif + + return _notifier.Update(data, tag); + } + + /// + /// Gets a value that tells you whether there is an app, user, or system block that prevents the display of a toast notification. + /// + public NotificationSetting Setting + { + get + { +#if WIN32 + // Just like scheduled notifications, apps need to have sent a notification + // before checking the setting value works + ToastNotificationManagerCompat.PreRegisterIdentityLessApp(); +#endif + + return _notifier.Setting; + } + } + +#if WIN32 + private void PreprocessToast(ToastNotification notification) + { + // For apps that don't have identity... + if (!DesktopBridgeHelpers.HasIdentity()) + { + // If tag is specified and group isn't specified + if (!string.IsNullOrEmpty(notification.Tag) && string.IsNullOrEmpty(notification.Group)) + { + // We have to add a group since otherwise can't remove without a group + notification.Group = ToastNotificationManagerCompat.DEFAULT_GROUP; + } + } + } + + private void PreprocessScheduledToast(ScheduledToastNotification notification) + { + // For apps that don't have identity... + if (!DesktopBridgeHelpers.HasIdentity()) + { + // If tag is specified and group isn't specified + if (!string.IsNullOrEmpty(notification.Tag) && string.IsNullOrEmpty(notification.Group)) + { + // We have to add a group since otherwise can't remove without a group + notification.Group = ToastNotificationManagerCompat.DEFAULT_GROUP; + } + } + } +#endif + } +} + +#endif \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_Toast.cs b/components/Notifications/src/Toasts/Elements/Element_Toast.cs new file mode 100644 index 000000000..1e158248c --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_Toast.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_Toast : BaseElement, IElement_ToastActivatable, IHaveXmlAdditionalProperties, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + internal const ToastScenario DEFAULT_SCENARIO = ToastScenario.Default; + internal const Element_ToastActivationType DEFAULT_ACTIVATION_TYPE = Element_ToastActivationType.Foreground; + internal const ToastDuration DEFAULT_DURATION = ToastDuration.Short; + + public Element_ToastActivationType ActivationType { get; set; } = DEFAULT_ACTIVATION_TYPE; + + public string ProtocolActivationTargetApplicationPfn { get; set; } + + public ToastAfterActivationBehavior AfterActivationBehavior + { + get + { + return ToastAfterActivationBehavior.Default; + } + + set + { + if (value != ToastAfterActivationBehavior.Default) + { + throw new InvalidOperationException("AfterActivationBehavior on ToastContent only supports the Default value."); + } + } + } + + public ToastDuration Duration { get; set; } = DEFAULT_DURATION; + + public string Launch { get; set; } + + public ToastScenario Scenario { get; set; } = DEFAULT_SCENARIO; + + public DateTimeOffset? DisplayTimestamp { get; set; } + + public Element_ToastVisual Visual { get; set; } + + public Element_ToastAudio Audio { get; set; } + + public Element_ToastActions Actions { get; set; } + + public Element_ToastHeader Header { get; set; } + + public string HintToastId { get; set; } + + public string HintPeople { get; set; } + + public IReadOnlyDictionary AdditionalProperties { get; set; } + + public static Element_ToastActivationType ConvertActivationType(ToastActivationType publicType) + { + switch (publicType) + { + case ToastActivationType.Foreground: + return Element_ToastActivationType.Foreground; + + case ToastActivationType.Background: + return Element_ToastActivationType.Background; + + case ToastActivationType.Protocol: + return Element_ToastActivationType.Protocol; + + default: + throw new NotImplementedException(); + } + } + + /// + string IHaveXmlName.Name => "toast"; + + /// + IEnumerable IHaveXmlChildren.Children => new object[] { Visual, Audio, Actions, Header }; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + if (ActivationType != DEFAULT_ACTIVATION_TYPE) + { + yield return new("activationType", ActivationType.ToPascalCaseString()); + } + + yield return new("protocolActivationTargetApplicationPfn", ProtocolActivationTargetApplicationPfn); + + if (AfterActivationBehavior != ToastAfterActivationBehavior.Default) + { + yield return new("afterActivationBehavior", AfterActivationBehavior.ToPascalCaseString()); + } + + if (Duration != DEFAULT_DURATION) + { + yield return new("duration", Duration.ToPascalCaseString()); + } + + yield return new("launch", Launch); + + if (Scenario != DEFAULT_SCENARIO) + { + yield return new("scenario", Scenario.ToPascalCaseString()); + } + + yield return new("displayTimestamp", DisplayTimestamp); + yield return new("hint-toastId", HintToastId); + yield return new("hint-people", HintPeople); + } + } + + /// + /// The amount of time the Toast should display. + /// + public enum ToastDuration + { + /// + /// Default value. Toast appears for a short while and then goes into Action Center. + /// + Short, + + /// + /// Toast stays on-screen for longer, and then goes into Action Center. + /// + Long + } + + /// + /// Specifies the scenario, controlling behaviors about the Toast. + /// + public enum ToastScenario + { + /// + /// The normal Toast behavior. The Toast appears for a short duration, and then automatically dismisses into Action Center. + /// + Default, + + /// + /// Causes the Toast to stay on-screen and expanded until the user takes action. Also causes a looping alarm sound to be selected by default. + /// + Alarm, + + /// + /// Causes the Toast to stay on-screen and expanded until the user takes action. + /// + Reminder, + + /// + /// Causes the Toast to stay on-screen and expanded until the user takes action (on Mobile this expands to full screen). Also causes a looping incoming call sound to be selected by default. + /// + IncomingCall + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastAction.cs b/components/Notifications/src/Toasts/Elements/Element_ToastAction.cs new file mode 100644 index 000000000..eaab27b19 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastAction.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastAction : IElement_ToastActionsChild, IElement_ToastActivatable, IHaveXmlName, IHaveXmlNamedProperties + { + internal const Element_ToastActivationType DEFAULT_ACTIVATION_TYPE = Element_ToastActivationType.Foreground; + internal const ToastAfterActivationBehavior DEFAULT_AFTER_ACTIVATION_BEHAVIOR = ToastAfterActivationBehavior.Default; + internal const Element_ToastActionPlacement DEFAULT_PLACEMENT = Element_ToastActionPlacement.Inline; + + /// + /// Gets or sets the text to be displayed on the button. + /// + public string Content { get; set; } + + /// + /// Gets or sets the arguments attribute describing the app-defined data that the app can later retrieve once it is activated from user taking this action. + /// + public string Arguments { get; set; } + + public Element_ToastActivationType ActivationType { get; set; } = DEFAULT_ACTIVATION_TYPE; + + public string ProtocolActivationTargetApplicationPfn { get; set; } + + public ToastAfterActivationBehavior AfterActivationBehavior { get; set; } = DEFAULT_AFTER_ACTIVATION_BEHAVIOR; + + /// + /// Gets or sets optional value to provide an image icon for this action to display inside the button alone with the text content. + /// + public string ImageUri { get; set; } + + /// + /// Gets or sets value used for the quick reply scenario. + /// + public string InputId { get; set; } + + public Element_ToastActionPlacement Placement { get; set; } = DEFAULT_PLACEMENT; + + public string HintActionId { get; set; } + + /// + string IHaveXmlName.Name => "action"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("content", Content); + yield return new("arguments", Arguments); + + if (ActivationType != DEFAULT_ACTIVATION_TYPE) + { + yield return new("activationType", ActivationType.ToPascalCaseString()); + } + + yield return new("protocolActivationTargetApplicationPfn", ProtocolActivationTargetApplicationPfn); + + if (AfterActivationBehavior != DEFAULT_AFTER_ACTIVATION_BEHAVIOR) + { + yield return new("afterActivationBehavior", AfterActivationBehavior.ToPascalCaseString()); + } + + yield return new("imageUri", ImageUri); + yield return new("hint-inputId", InputId); + + if (Placement != DEFAULT_PLACEMENT) + { + yield return new("placement", Placement.ToPascalCaseString()); + } + + yield return new("hint-actionId", HintActionId); + } + } + + internal enum Element_ToastActionPlacement + { + Inline, + ContextMenu + } + + internal enum Element_ToastActivationType + { + /// + /// Default value. Your foreground app is launched. + /// + Foreground, + + /// + /// Your corresponding background task (assuming you set everything up) is triggered, and you can execute code in the background (like sending the user's quick reply message) without interrupting the user. + /// + Background, + + /// + /// Launch a different app using protocol activation. + /// + Protocol, + + /// + /// System handles the activation. + /// + System + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastActions.cs b/components/Notifications/src/Toasts/Elements/Element_ToastActions.cs new file mode 100644 index 000000000..c5ff90347 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastActions.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastActions : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + internal const ToastSystemCommand DEFAULT_SYSTEM_COMMAND = ToastSystemCommand.None; + + public ToastSystemCommand SystemCommands { get; set; } = ToastSystemCommand.None; + + public IList Children { get; private set; } = new List(); + + /// + string IHaveXmlName.Name => "actions"; + + /// + IEnumerable IHaveXmlChildren.Children => Children; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + if (SystemCommands != DEFAULT_SYSTEM_COMMAND) + { + yield return new("hint-systemCommands", SystemCommands); + } + } + } + + internal interface IElement_ToastActionsChild + { + } + + internal enum ToastSystemCommand + { + None, + SnoozeAndDismiss + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs b/components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs new file mode 100644 index 000000000..2a674556a --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastAudio : IHaveXmlName, IHaveXmlNamedProperties + { + internal const bool DEFAULT_LOOP = false; + internal const bool DEFAULT_SILENT = false; + + /// + /// Gets or sets the media file to play in place of the default sound. This can either be a ms-winsoundevent value, or a custom ms-appx:/// or ms-appdata:/// file, or null for the default sound. + /// + public Uri Src { get; set; } + + public bool Loop { get; set; } = DEFAULT_LOOP; + + /// + /// Gets or sets a value indicating whether the sound is muted; false to allow the Toast notification sound to play. + /// + public bool Silent { get; set; } = DEFAULT_SILENT; + + /// + string IHaveXmlName.Name => "audio"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("src", Src); + + if (Loop != DEFAULT_LOOP) + { + yield return new("loop", Loop); + } + + if (Silent != DEFAULT_SILENT) + { + yield return new("silent", Silent); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs b/components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs new file mode 100644 index 000000000..c5a680a74 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastBinding : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + public Element_ToastBinding(ToastTemplateType template) + { + Template = template; + } + + public ToastTemplateType Template { get; private set; } + + /// + /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of + /// + /// "www.website.com/images/hello.png" + /// + /// included in the notification becomes + /// + /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us" + /// + public bool? AddImageQuery { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides that in visual, but can be overridden by that in text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. See Remarks for when this value isn't specified. + /// + public string Language { get; set; } + + public string ExperienceType { get; set; } + + public IList Children { get; private set; } = new List(); + + /// + string IHaveXmlName.Name => "binding"; + + /// + IEnumerable IHaveXmlChildren.Children => Children; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("template", Template); + yield return new("addImageQuery", AddImageQuery); + yield return new("baseUri", BaseUri); + yield return new("lang", Language); + yield return new("experienceType", ExperienceType); + } + } + + internal interface IElement_ToastBindingChild + { + } + + internal enum ToastTemplateType + { + ToastGeneric, + ToastImageAndText01, + ToastImageAndText02, + ToastImageAndText03, + ToastImageAndText04, + ToastText01, + ToastText02, + ToastText03, + ToastText04 + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs b/components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs new file mode 100644 index 000000000..7b1f5aec7 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastHeader : IElement_ToastActivatable, IHaveXmlName, IHaveXmlNamedProperties + { + public string Id { get; set; } + + public string Title { get; set; } + + public string Arguments { get; set; } + + public Element_ToastActivationType ActivationType { get; set; } = Element_ToastActivationType.Foreground; + + public string ProtocolActivationTargetApplicationPfn { get; set; } + + public ToastAfterActivationBehavior AfterActivationBehavior + { + get + { + return ToastAfterActivationBehavior.Default; + } + + set + { + if (value != ToastAfterActivationBehavior.Default) + { + throw new InvalidOperationException("AfterActivationBehavior on ToastHeader only supports the Default value."); + } + } + } + + /// + string IHaveXmlName.Name => "header"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("id", Id); + yield return new("title", Title); + yield return new("arguments", Arguments); + + if (ActivationType != Element_ToastActivationType.Foreground) + { + yield return new("activationType", ActivationType.ToPascalCaseString()); + } + + yield return new("protocolActivationTargetApplicationPfn", ProtocolActivationTargetApplicationPfn); + + if (AfterActivationBehavior != ToastAfterActivationBehavior.Default) + { + yield return new("afterActivationBehavior", AfterActivationBehavior.ToPascalCaseString()); + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastImage.cs b/components/Notifications/src/Toasts/Elements/Element_ToastImage.cs new file mode 100644 index 000000000..e3473a278 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastImage.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastImage : IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties + { + internal const ToastImagePlacement DEFAULT_PLACEMENT = ToastImagePlacement.Inline; + internal const bool DEFAULT_ADD_IMAGE_QUERY = false; + internal const ToastImageCrop DEFAULT_CROP = ToastImageCrop.None; + + public string Src { get; set; } + + public string Alt { get; set; } + + public bool AddImageQuery { get; set; } = DEFAULT_ADD_IMAGE_QUERY; + + public ToastImagePlacement Placement { get; set; } = DEFAULT_PLACEMENT; + + public ToastImageCrop Crop { get; set; } = DEFAULT_CROP; + + /// + string IHaveXmlName.Name => "image"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("src", Src); + yield return new("alt", Alt); + + if (AddImageQuery != DEFAULT_ADD_IMAGE_QUERY) + { + yield return new("addImageQuery", AddImageQuery); + } + + if (Placement != DEFAULT_PLACEMENT) + { + yield return new("placement", Placement.ToPascalCaseString()); + } + + if (Crop != DEFAULT_CROP) + { + yield return new("crop", Crop.ToPascalCaseString()); + } + } + } + + /// + /// Specify the desired cropping of the image. + /// + public enum ToastImageCrop + { + /// + /// Default value. Image is not cropped. + /// + None, + + /// + /// Image is cropped to a circle shape. + /// + Circle + } + + internal enum ToastImagePlacement + { + Inline, + AppLogoOverride, + Hero + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastInput.cs b/components/Notifications/src/Toasts/Elements/Element_ToastInput.cs new file mode 100644 index 000000000..0e8cd882b --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastInput.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastInput : IElement_ToastActionsChild, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + /// + /// Gets or sets the required attributes for developers to retrieve user inputs once the app is activated (in the foreground or background). + /// + public string Id { get; set; } + + public ToastInputType Type { get; set; } + + /// + /// Gets or sets the optional title attribute and is for developers to specify a title for the input for shells to render when there is affordance. + /// + public string Title { get; set; } + + /// + /// Gets or sets the optional placeholderContent attribute and is the grey-out hint text for text input type. This attribute is ignored when the input type is not �text�. + /// + public string PlaceholderContent { get; set; } + + /// + /// Gets or sets the optional defaultInput attribute and it allows developer to provide a default input value. + /// + public string DefaultInput { get; set; } + + public IList Children { get; private set; } = new List(); + + /// + string IHaveXmlName.Name => "input"; + + /// + IEnumerable IHaveXmlChildren.Children => Children; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("id", Id); + yield return new("type", Type.ToPascalCaseString()); + yield return new("title", Title); + yield return new("placeHolderContent", PlaceholderContent); + yield return new("defaultInput", DefaultInput); + } + } + + internal interface IElement_ToastInputChild + { + } + + internal enum ToastInputType + { + Text, + Selection + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs b/components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs new file mode 100644 index 000000000..4092ad554 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastSelection : IElement_ToastInputChild, IHaveXmlName, IHaveXmlNamedProperties + { + /// + /// Gets or sets the id attribute for apps to retrieve back the user selected input after the app is activated. Required + /// + public string Id { get; set; } + + /// + /// Gets or sets the text to display for this selection element. + /// + public string Content { get; set; } + + /// + string IHaveXmlName.Name => "selection"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("id", Id); + yield return new("content", Content); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastText.cs b/components/Notifications/src/Toasts/Elements/Element_ToastText.cs new file mode 100644 index 000000000..f9b9947aa --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastText.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastText : IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlText + { + internal const ToastTextPlacement DEFAULT_PLACEMENT = ToastTextPlacement.Inline; + + public string Text { get; set; } + + public string Lang { get; set; } + + public ToastTextPlacement Placement { get; set; } = DEFAULT_PLACEMENT; + + /// + string IHaveXmlName.Name => "text"; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("lang", Lang); + + if (Placement != DEFAULT_PLACEMENT) + { + yield return new("placement", Placement.ToPascalCaseString()); + } + } + } + + internal enum ToastTextPlacement + { + Inline, + Attribution + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs b/components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs new file mode 100644 index 000000000..0baf45b1a --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + internal sealed class Element_ToastVisual : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren + { + internal const bool DEFAULT_ADD_IMAGE_QUERY = false; + + public bool? AddImageQuery { get; set; } + + public Uri BaseUri { get; set; } + + public string Language { get; set; } + + public int? Version { get; set; } + + public IList Bindings { get; private set; } = new List(); + + /// + string IHaveXmlName.Name => "visual"; + + /// + IEnumerable IHaveXmlChildren.Children => Bindings; + + /// + IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties() + { + yield return new("addImageQuery", AddImageQuery); + yield return new("baseUri", BaseUri); + yield return new("lang", Language); + yield return new("version", Version); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs b/components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs new file mode 100644 index 000000000..bc4a7d293 --- /dev/null +++ b/components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + internal interface IElement_ToastActivatable + { + Element_ToastActivationType ActivationType { get; set; } + + string ProtocolActivationTargetApplicationPfn { get; set; } + + ToastAfterActivationBehavior AfterActivationBehavior { get; set; } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/IToastActions.cs b/components/Notifications/src/Toasts/IToastActions.cs new file mode 100644 index 000000000..4b7627307 --- /dev/null +++ b/components/Notifications/src/Toasts/IToastActions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// Actions to display on a Toast notification. One of or . + /// + public interface IToastActions + { + /// + /// Gets custom context menu items, providing additional actions when the user right clicks the Toast notification. New in Anniversary Update + /// + IList ContextMenuItems { get; } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/IToastActivateableBuilder.cs b/components/Notifications/src/Toasts/IToastActivateableBuilder.cs new file mode 100644 index 000000000..f3e9a5f1f --- /dev/null +++ b/components/Notifications/src/Toasts/IToastActivateableBuilder.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Interfaces for classes that can have activation info added to them. + /// + /// The type of the host object. + internal interface IToastActivateableBuilder + { + /// + /// Adds a key (without value) to the activation arguments that will be returned when the content is clicked. + /// + /// The key. + /// The current instance of the object. + T AddArgument(string key); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + T AddArgument(string key, string value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, int value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, double value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, float value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, bool value); + +#if !WINRT + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current instance of the object. + T AddArgument(string key, Enum value); +#endif + + /// + /// Configures the content to use background activation when it is clicked. + /// + /// The current instance of the object. + T SetBackgroundActivation(); + + /// + /// Configures the content to use protocol activation when it is clicked. + /// + /// The protocol to launch. + /// The current instance of the object. + T SetProtocolActivation(Uri protocol); + + /// + /// Configures the content to use protocol activation when it is clicked. + /// + /// The protocol to launch. + /// New in Creators Update: The target PFN, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// The current instance of the object. + T SetProtocolActivation(Uri protocol, string targetApplicationPfn); + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/IToastBindingGenericChild.cs b/components/Notifications/src/Toasts/IToastBindingGenericChild.cs new file mode 100644 index 000000000..444cefbe6 --- /dev/null +++ b/components/Notifications/src/Toasts/IToastBindingGenericChild.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Elements that can be direct children of , including (, , and ). + /// + public interface IToastBindingGenericChild + { + // Blank interface simply for compile-enforcing the child types in the list. + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/IToastButton.cs b/components/Notifications/src/Toasts/IToastButton.cs new file mode 100644 index 000000000..0f52fee31 --- /dev/null +++ b/components/Notifications/src/Toasts/IToastButton.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// One of , , or . + /// + public interface IToastButton + { + /// + /// Gets or sets an optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). + /// + string ImageUri { get; set; } + + /// + /// Gets or sets an identifier used in telemetry to identify your category of action. This should be something + /// like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will + /// be able to view how frequently your actions are being clicked. + /// + string HintActionId { get; set; } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/IToastInput.cs b/components/Notifications/src/Toasts/IToastInput.cs new file mode 100644 index 000000000..7b13e568d --- /dev/null +++ b/components/Notifications/src/Toasts/IToastInput.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// An input element on a Toast notification. One of or . + /// + public interface IToastInput + { + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastActionsCustom.cs b/components/Notifications/src/Toasts/ToastActionsCustom.cs new file mode 100644 index 000000000..a40f8d702 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastActionsCustom.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// Create your own custom actions, using controls like , , and . + /// + public sealed class ToastActionsCustom : IToastActions + { + /// + /// Gets inputs like and . Only up to 5 inputs can be added; after that, an exception is thrown. + /// + public IList Inputs { get; private set; } = new LimitedList(5); + + /// + /// Gets buttons displayed after all the inputs (or adjacent to inputs if used as quick reply buttons). Only up to 5 buttons can be added (or fewer if you are also including context menu items). After that, an exception is thrown. You can add , , or + /// + public IList Buttons { get; private set; } = new LimitedList(5); + + /// + /// Gets custom context menu items, providing additional actions when the user right clicks the Toast notification. + /// You can only have up to 5 buttons and context menu items *combined*. Thus, if you have one context menu item, + /// you can only have four buttons, etc. New in Anniversary Update: + /// + public IList ContextMenuItems { get; private set; } = new List(); + + internal Element_ToastActions ConvertToElement() + { + if (Buttons.Count + ContextMenuItems.Count > 5) + { + throw new InvalidOperationException("You have too many buttons/context menu items. You can only have up to 5 total."); + } + + var el = new Element_ToastActions(); + + foreach (var input in Inputs) + { + el.Children.Add(ConvertToInputElement(input)); + } + + foreach (var button in this.Buttons) + { + el.Children.Add(ConvertToActionElement(button)); + } + + foreach (var item in ContextMenuItems) + { + el.Children.Add(item.ConvertToElement()); + } + + return el; + } + + private static Element_ToastAction ConvertToActionElement(IToastButton button) + { + if (button is ToastButton) + { + return (button as ToastButton).ConvertToElement(); + } + + if (button is ToastButtonDismiss) + { + return (button as ToastButtonDismiss).ConvertToElement(); + } + + if (button is ToastButtonSnooze) + { + return (button as ToastButtonSnooze).ConvertToElement(); + } + + throw new NotImplementedException("Unknown button child: " + button.GetType()); + } + + private static Element_ToastInput ConvertToInputElement(IToastInput input) + { + if (input is ToastTextBox) + { + return (input as ToastTextBox).ConvertToElement(); + } + + if (input is ToastSelectionBox) + { + return (input as ToastSelectionBox).ConvertToElement(); + } + + throw new NotImplementedException("Unknown input child: " + input.GetType()); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs b/components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs new file mode 100644 index 000000000..13d5a76ff --- /dev/null +++ b/components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// Automatically constructs a selection box for snooze intervals, and snooze/dismiss buttons, all automatically localized, and snoozing logic is automatically handled by the system. + /// + public sealed class ToastActionsSnoozeAndDismiss : IToastActions + { + /// + /// Gets custom context menu items, providing additional actions when the user right clicks the Toast notification. + /// You can only have up to 5 items. New in Anniversary Update + /// + public IList ContextMenuItems { get; private set; } = new List(); + + internal Element_ToastActions ConvertToElement() + { + if (ContextMenuItems.Count > 5) + { + throw new InvalidOperationException("You have too many context menu items. You can only have up to 5."); + } + + var el = new Element_ToastActions() + { + SystemCommands = ToastSystemCommand.SnoozeAndDismiss + }; + + foreach (var item in ContextMenuItems) + { + el.Children.Add(item.ConvertToElement()); + } + + return el; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastActivationOptions.cs b/components/Notifications/src/Toasts/ToastActivationOptions.cs new file mode 100644 index 000000000..81609903a --- /dev/null +++ b/components/Notifications/src/Toasts/ToastActivationOptions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// New in Creators Update: Additional options relating to activation. + /// + public sealed class ToastActivationOptions + { + /// + /// Gets or sets the target PFN if you are using . You can optionally specify, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// + public string ProtocolActivationTargetApplicationPfn { get; set; } + + /// + /// Gets or sets the behavior that the toast should use when the user invokes this action. + /// Note that this option only works on and . + /// Desktop-only, supported in builds 16251 or higher. New in Fall Creators Update + /// + public ToastAfterActivationBehavior AfterActivationBehavior { get; set; } = ToastAfterActivationBehavior.Default; + + internal void PopulateElement(IElement_ToastActivatable el) + { + // If protocol PFN is specified but protocol activation isn't used, throw exception + if (ProtocolActivationTargetApplicationPfn != null && el.ActivationType != Element_ToastActivationType.Protocol) + { + throw new InvalidOperationException($"You cannot specify {nameof(ProtocolActivationTargetApplicationPfn)} without using ActivationType of Protocol."); + } + + el.ProtocolActivationTargetApplicationPfn = ProtocolActivationTargetApplicationPfn; + el.AfterActivationBehavior = AfterActivationBehavior; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastArguments.cs b/components/Notifications/src/Toasts/ToastArguments.cs new file mode 100644 index 000000000..572c06be5 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastArguments.cs @@ -0,0 +1,462 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace CommunityToolkit.Notifications +{ + /// + /// A class that supports serializing simple key/value pairs into a format that's friendly for being used within toast notifications. The serialized format is similar to a query string, however optimized for being placed within an XML property (uses semicolons instead of ampersands since those don't need to be XML-escaped, doesn't url-encode all special characters since not being used within a URL, etc). + /// + public sealed class ToastArguments : IEnumerable> + { + private Dictionary _dictionary = new Dictionary(); + + internal ToastArguments Clone() + { + return new ToastArguments() + { + _dictionary = new Dictionary(_dictionary) + }; + } + +#if !WINRT + /// + /// Gets the value of the specified key. Throws if the key could not be found. + /// + /// The key to find. + /// The value of the specified key. + public string this[string key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (TryGetValue(key, out string value)) + { + return value; + } + + throw new KeyNotFoundException($"A key with name '{key}' could not be found."); + } + + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _dictionary[key] = value; + } + } +#endif + + /// + /// Attempts to get the value of the specified key. If no key exists, returns false. + /// + /// The key to find. + /// The key's value will be written here if found. + /// True if found the key and set the value, otherwise false. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("found")] +#endif + public bool TryGetValue(string key, out string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _dictionary.TryGetValue(key, out value); + } + +#if !WINRT + /// + /// Attempts to get the value of the specified key. If no key exists, returns false. + /// + /// The enum to parse. + /// The key to find. + /// The key's value will be written here if found. + /// True if found the key and set the value, otherwise false. + public bool TryGetValue(string key, out T value) + where T : struct, Enum + { + if (TryGetValue(key, out string strValue)) + { + return Enum.TryParse(strValue, out value); + } + + value = default(T); + return false; + } +#endif + + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public string Get(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (_dictionary.TryGetValue(key, out string value)) + { + return value; + } + + throw new KeyNotFoundException(); + } + + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public int GetInt(string key) + { + return int.Parse(Get(key)); + } + + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public double GetDouble(string key) + { + return double.Parse(Get(key)); + } + + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public float GetFloat(string key) + { + return float.Parse(Get(key)); + } + + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public byte GetByte(string key) + { + return byte.Parse(Get(key)); + } + + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public bool GetBool(string key) + { + return Get(key) == "1" ? true : false; + } + +#if !WINRT + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The enum to parse. + /// The key to get. + /// The value of the key. + public T GetEnum(string key) + where T : struct, Enum + { + if (TryGetValue(key, out T value)) + { + return value; + } + + throw new KeyNotFoundException(); + } +#endif + + /// + /// Gets the number of key/value pairs contained in the toast arguments. + /// + public int Count => _dictionary.Count; + + /// + /// Adds a key. If there is an existing key, it is replaced. + /// + /// The key. + /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Add(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _dictionary[key] = null; + + return this; + } + + /// + /// Adds a key and optional value. If there is an existing key, it is replaced. + /// + /// The key. + /// The optional value of the key. + /// The current object. +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Add(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _dictionary[key] = value; + + return this; + } + + /// + /// Adds a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Add(string key, int value) + { + return AddHelper(key, value); + } + + /// + /// Adds a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Add(string key, double value) + { + return AddHelper(key, value); + } + + /// + /// Adds a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Add(string key, float value) + { + return AddHelper(key, value); + } + + /// + /// Adds a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Add(string key, bool value) + { + return Add(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + } + +#if !WINRT + /// + /// Adds a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current object. + public ToastArguments Add(string key, Enum value) + { + return Add(key, (int)(object)value); + } +#endif + + private ToastArguments AddHelper(string key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _dictionary[key] = value.ToString(); + + return this; + } + + /// + /// Determines if the specified key is present. + /// + /// The key to look for. + /// True if the key is present, otherwise false. + public bool Contains(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _dictionary.ContainsKey(key); + } + + /// + /// Determines if specified key and value are present. + /// + /// The key to look for. + /// The value to look for when the key has been matched. + /// True if the key and value were found, else false. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("doesContain")] +#endif + public bool Contains(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _dictionary.TryGetValue(key, out string actualValue) && actualValue == value; + } + + /// + /// Removes the specified key and its associated value. + /// + /// The key to remove. + /// True if the key was removed, else false. + public bool Remove(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _dictionary.Remove(key); + } + + private static string Encode(string str) + { + return str + .Replace("%", "%25") + .Replace(";", "%3B") + .Replace("=", "%3D"); + } + + private static string Decode(string str) + { + return str + .Replace("%25", "%") + .Replace("%3B", ";") + .Replace("%3D", "="); + } + + /// + /// Parses a string that was generated using ToastArguments into a object. + /// + /// The toast arguments string to deserialize. + /// The parsed toast arguments. + public static ToastArguments Parse(string toastArgumentsStr) + { + if (string.IsNullOrWhiteSpace(toastArgumentsStr)) + { + return new ToastArguments(); + } + + string[] pairs = toastArgumentsStr.Split(';'); + + ToastArguments answer = new ToastArguments(); + + foreach (string pair in pairs) + { + string name; + string value; + + int indexOfEquals = pair.IndexOf('='); + + if (indexOfEquals == -1) + { + name = Decode(pair); + value = null; + } + else + { + name = Decode(pair.Substring(0, indexOfEquals)); + value = Decode(pair.Substring(indexOfEquals + 1)); + } + + answer.Add(name, value); + } + + return answer; + } + + /// + /// Serializes the key-value pairs into a string that can be used within a toast notification. + /// + /// A string that can be used within a toast notification. + public sealed override string ToString() + { + return string.Join(Separator, this.Select(pair => EncodePair(pair.Key, pair.Value))); + } + + internal static string EncodePair(string key, string value) + { + // Key + return Encode(key) + + + // Write value if not null + ((value == null) ? string.Empty : ("=" + Encode(value))); + } + + internal const string Separator = ";"; + + /// + /// Gets an enumerator to enumerate the arguments. Note that order of the arguments is NOT preserved. + /// + /// An enumeartor of the key/value pairs. + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + /// + /// Gets an enumerator to enumerate the query string parameters. + /// + /// An enumeartor of the key/value pairs. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastAudio.cs b/components/Notifications/src/Toasts/ToastAudio.cs new file mode 100644 index 000000000..1f7f3a949 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastAudio.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Specify audio to be played when the Toast notification is received. + /// + public sealed class ToastAudio + { + /// + /// Gets or sets the media file to play in place of the default sound. + /// + public Uri Src { get; set; } + + /// + /// Gets or sets a value indicating whether sound should repeat as long as the Toast is shown; false to play only once (default). + /// + public bool Loop { get; set; } = Element_ToastAudio.DEFAULT_LOOP; + + /// + /// Gets or sets a value indicating whether sound is muted; false to allow the Toast notification sound to play (default). + /// + public bool Silent { get; set; } = Element_ToastAudio.DEFAULT_SILENT; + + internal Element_ToastAudio ConvertToElement() + { + return new Element_ToastAudio() + { + Src = Src, + Loop = Loop, + Silent = Silent + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastBindingGeneric.cs b/components/Notifications/src/Toasts/ToastBindingGeneric.cs new file mode 100644 index 000000000..7ea818807 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastBindingGeneric.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using CommunityToolkit.Notifications.Adaptive; + +namespace CommunityToolkit.Notifications +{ + /// + /// Generic Toast binding, where you provide text, images, and other visual elements for your Toast notification. + /// + public sealed class ToastBindingGeneric + { + /// + /// Gets the contents of the body of the Toast, which can include , , + /// and (added in Anniversary Update). Also, elements must come + /// before any other elements. If an element is placed after any other element, an exception + /// will be thrown when you try to retrieve the Toast XML content. And finally, certain properties + /// like HintStyle aren't supported on the root children text elements, and only work inside an . + /// If you use on devices without the Anniversary Update, the group content will simply be dropped. + /// + public IList Children { get; private set; } = new List(); + + /// + /// Gets or sets an optional override of the logo displayed on the Toast notification. + /// + public ToastGenericAppLogo AppLogoOverride { get; set; } + + /// + /// Gets or sets an optional hero image (a visually impactful image displayed on the Toast notification). + /// On devices without the Anniversary Update, the hero image will simply be ignored. + /// + public ToastGenericHeroImage HeroImage { get; set; } + + /// + /// Gets or sets an optional text element that is displayed as attribution text. On devices without + /// the Anniversary Update, this text will appear as if it's another + /// element at the end of your Children list. + /// + public ToastGenericAttributionText Attribution { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as BCP-47 language tags such as "en-US" + /// or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is + /// a literal string, this attribute defaults to the user's UI language. If this value is a string reference, + /// this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Language { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Toast notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + internal Element_ToastBinding ConvertToElement() + { + Element_ToastBinding binding = new Element_ToastBinding(ToastTemplateType.ToastGeneric) + { + BaseUri = BaseUri, + AddImageQuery = AddImageQuery, + Language = Language + }; + + // Add children + foreach (var child in Children) + { + var el = (IElement_ToastBindingChild)AdaptiveHelper.ConvertToElement(child); + binding.Children.Add(el); + } + + // Add attribution + if (Attribution != null) + { + binding.Children.Add(Attribution.ConvertToElement()); + } + + // If there's hero, add it + if (HeroImage != null) + { + binding.Children.Add(HeroImage.ConvertToElement()); + } + + // If there's app logo, add it + if (AppLogoOverride != null) + { + binding.Children.Add(AppLogoOverride.ConvertToElement()); + } + + return binding; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastBindingShoulderTap.cs b/components/Notifications/src/Toasts/ToastBindingShoulderTap.cs new file mode 100644 index 000000000..0097eb721 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastBindingShoulderTap.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Specifies content you want to appear in a My People shoulder tap notification. For more info, see the My People notifications documentation. New in Fall Creators Update. + /// + public sealed class ToastBindingShoulderTap + { + /// + /// Gets or sets the image to be displayed in the shoulder tap notification. Required. + /// + public ToastShoulderTapImage Image { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as BCP-47 language tags such as "en-US" + /// or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is + /// a literal string, this attribute defaults to the user's UI language. If this value is a string reference, + /// this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Language { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Toast notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + internal Element_ToastBinding ConvertToElement() + { + Element_ToastBinding binding = new Element_ToastBinding(ToastTemplateType.ToastGeneric) + { + ExperienceType = "shoulderTap", + BaseUri = BaseUri, + AddImageQuery = AddImageQuery, + Language = Language + }; + + // If there's an image, add it + if (Image != null) + { + binding.Children.Add(Image.ConvertToElement()); + } + + return binding; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastButton.cs b/components/Notifications/src/Toasts/ToastButton.cs new file mode 100644 index 000000000..2b57ebf97 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastButton.cs @@ -0,0 +1,466 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// A button that the user can click on a Toast notification. + /// + public sealed class ToastButton : +#if !WINRT + IToastActivateableBuilder, +#endif + IToastButton + { + private Dictionary _arguments = new Dictionary(); + + private bool _usingCustomArguments; + + private bool _usingSnoozeActivation; + private string _snoozeSelectionBoxId; + + private bool _usingDismissActivation; + + internal bool NeedsContent() + { + // Snooze/dismiss buttons don't need content (the system will auto-add the localized strings). + return !_usingDismissActivation && !_usingSnoozeActivation; + } + + /// + /// Initializes a new instance of the class. + /// + /// The text to display on the button. + /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button. + public ToastButton(string content, string arguments) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (arguments == null) + { + throw new ArgumentNullException(nameof(arguments)); + } + + Content = content; + Arguments = arguments; + + _usingCustomArguments = arguments.Length > 0; + } + + /// + /// Initializes a new instance of the class. + /// + public ToastButton() + { + // Arguments are required (we'll initialize to empty string which is fine). + Arguments = string.Empty; + } + + /// + /// Gets the text to display on the button. Required + /// + public string Content { get; private set; } + + /// + /// Gets app-defined string of arguments that the app can later retrieve once it is + /// activated when the user clicks the button. Required + /// + public string Arguments { get; internal set; } + + /// + /// Gets or sets what type of activation this button will use when clicked. Defaults to Foreground. + /// + public ToastActivationType ActivationType { get; set; } = ToastActivationType.Foreground; + + /// + /// Gets or sets additional options relating to activation of the toast button. New in Creators Update + /// + public ToastActivationOptions ActivationOptions { get; set; } + + /// + /// Gets or sets an optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). + /// + public string ImageUri { get; set; } + + /// + /// Gets or sets the ID of an existing in order to have this button display + /// to the right of the input, achieving a quick reply scenario. + /// + public string TextBoxId { get; set; } + + /// + /// Gets or sets an identifier used in telemetry to identify your category of action. This should be something + /// like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will + /// be able to view how frequently your actions are being clicked. + /// + public string HintActionId { get; set; } + + /// + /// Sets the text to display on the button. + /// + /// The text to display on the button. + /// The current instance of the . + public ToastButton SetContent(string content) + { + Content = content; + return this; + } + + /// + /// Adds a key (without value) to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key. + /// The current instance of + public ToastButton AddArgument(string key) + { + return AddArgumentHelper(key, null); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, string value) + { + return AddArgumentHelper(key, value); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, int value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, double value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, float value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, bool value) + { + return AddArgumentHelper(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + } + +#if !WINRT + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current instance of + public ToastButton AddArgument(string key, Enum value) + { + return AddArgumentHelper(key, ((int)(object)value).ToString()); + } +#endif + + private ToastButton AddArgumentHelper(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (_usingCustomArguments) + { + throw new InvalidOperationException("You cannot use the AddArgument methods if you've set the Arguments property. Use the default ToastButton constructor instead."); + } + + if (ActivationType == ToastActivationType.Protocol) + { + throw new InvalidOperationException("You cannot use the AddArgument methods when using protocol activation."); + } + + if (_usingDismissActivation || _usingSnoozeActivation) + { + throw new InvalidOperationException("You cannot use the AddArgument methods when using dismiss or snooze activation."); + } + + bool alreadyExists = _arguments.ContainsKey(key); + + _arguments[key] = value; + + Arguments = alreadyExists ? SerializeArgumentsHelper(_arguments) : AddArgumentHelper(Arguments, key, value); + + return this; + } + + private string SerializeArgumentsHelper(IDictionary arguments) + { + var args = new ToastArguments(); + + foreach (var a in arguments) + { + args.Add(a.Key, a.Value); + } + + return args.ToString(); + } + + private string AddArgumentHelper(string existing, string key, string value) + { + string pair = ToastArguments.EncodePair(key, value); + + if (string.IsNullOrEmpty(existing)) + { + return pair; + } + else + { + return existing + ToastArguments.Separator + pair; + } + } + + /// + /// Configures the button to launch the specified url when the button is clicked. + /// + /// The protocol to launch. + /// The current instance of + public ToastButton SetProtocolActivation(Uri protocol) + { + return SetProtocolActivation(protocol, default); + } + + /// + /// Configures the button to launch the specified url when the button is clicked. + /// + /// The protocol to launch. + /// New in Creators Update: The target PFN, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// The current instance of + public ToastButton SetProtocolActivation(Uri protocol, string targetApplicationPfn) + { + if (_arguments.Count > 0) + { + throw new InvalidOperationException("SetProtocolActivation cannot be used in conjunction with AddArgument"); + } + + Arguments = protocol.ToString(); + ActivationType = ToastActivationType.Protocol; + + if (targetApplicationPfn != null) + { + if (ActivationOptions == null) + { + ActivationOptions = new ToastActivationOptions(); + } + + ActivationOptions.ProtocolActivationTargetApplicationPfn = targetApplicationPfn; + } + + return this; + } + + /// + /// Configures the button to use background activation when the button is clicked. + /// + /// The current instance of + public ToastButton SetBackgroundActivation() + { + ActivationType = ToastActivationType.Background; + return this; + } + + /// + /// Sets the behavior that the toast should use when the user invokes this button. Desktop-only, supported in builds 16251 or higher. New in Fall Creators Update. + /// + /// The behavior that the toast should use when the user invokes this button. + /// The current instance of + public ToastButton SetAfterActivationBehavior(ToastAfterActivationBehavior afterActivationBehavior) + { + if (ActivationOptions == null) + { + ActivationOptions = new ToastActivationOptions(); + } + + ActivationOptions.AfterActivationBehavior = afterActivationBehavior; + + return this; + } + + /// + /// Configures the button to use system snooze activation when the button is clicked, using the default system snooze time. + /// + /// The current instance of + public ToastButton SetSnoozeActivation() + { + return SetSnoozeActivation(null); + } + + /// + /// Configures the button to use system snooze activation when the button is clicked, with a snooze time defined by the specified selection box. + /// + /// The ID of an existing which allows the user to pick a custom snooze time. The ID's of the s inside the selection box must represent the snooze interval in minutes. For example, if the user selects an item that has an ID of "120", then the notification will be snoozed for 2 hours. When the user clicks this button, if you specified a SelectionBoxId, the system will parse the ID of the selected item and snooze by that amount of minutes. + /// The current instance of + public ToastButton SetSnoozeActivation(string selectionBoxId) + { + if (_arguments.Count > 0) + { + throw new InvalidOperationException($"{nameof(SetSnoozeActivation)} cannot be used in conjunction with ${nameof(AddArgument)}."); + } + + _usingSnoozeActivation = true; + _snoozeSelectionBoxId = selectionBoxId; + + return this; + } + + /// + /// Configures the button to use system dismiss activation when the button is clicked (the toast will simply dismiss rather than activating). + /// + /// The current instance of + public ToastButton SetDismissActivation() + { + if (_arguments.Count > 0) + { + throw new InvalidOperationException($"{nameof(SetDismissActivation)} cannot be used in conjunction with ${nameof(AddArgument)}."); + } + + _usingDismissActivation = true; + return this; + } + + /// + /// Sets an identifier used in telemetry to identify your category of action. This should be something like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will be able to view how frequently your actions are being clicked. + /// + /// An identifier used in telemetry to identify your category of action. + /// The current instance of + public ToastButton SetHintActionId(string actionId) + { + HintActionId = actionId; + return this; + } + + /// + /// Sets an optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). + /// + /// An optional image icon for the button to display. + /// The current instance of + public ToastButton SetImageUri(Uri imageUri) + { + ImageUri = imageUri.ToString(); + return this; + } + + /// + /// Sets the ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. + /// + /// The ID of an existing . + /// The current instance of + public ToastButton SetTextBoxId(string textBoxId) + { + TextBoxId = textBoxId; + return this; + } + + internal bool CanAddArguments() + { + return ActivationType != ToastActivationType.Protocol && !_usingCustomArguments && !_usingDismissActivation && !_usingSnoozeActivation; + } + + internal bool ContainsArgument(string key) + { + return _arguments.ContainsKey(key); + } + + internal Element_ToastAction ConvertToElement() + { + var el = new Element_ToastAction() + { + Content = Content, + ImageUri = ImageUri, + InputId = TextBoxId, + HintActionId = HintActionId + }; + + if (_usingSnoozeActivation) + { + el.ActivationType = Element_ToastActivationType.System; + el.Arguments = "snooze"; + + if (_snoozeSelectionBoxId != null) + { + el.InputId = _snoozeSelectionBoxId; + } + + // Content needs to be specified as empty for auto-generated Snooze content + if (el.Content == null) + { + el.Content = string.Empty; + } + } + else if (_usingDismissActivation) + { + el.ActivationType = Element_ToastActivationType.System; + el.Arguments = "dismiss"; + + // Content needs to be specified as empty for auto-generated Dismiss content + if (el.Content == null) + { + el.Content = string.Empty; + } + } + else + { + el.ActivationType = Element_Toast.ConvertActivationType(ActivationType); + el.Arguments = Arguments; + } + + ActivationOptions?.PopulateElement(el); + + return el; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastButtonDismiss.cs b/components/Notifications/src/Toasts/ToastButtonDismiss.cs new file mode 100644 index 000000000..9a92683b5 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastButtonDismiss.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// A button that, when clicked, is interpreted as a "dismiss" by the system, and the Toast is dismissed just like if the user swiped the Toast away. + /// + public sealed class ToastButtonDismiss : IToastButton + { + /// + /// Gets custom text displayed on the button that overrides the default localized "Dismiss" text. + /// + public string CustomContent { get; private set; } + + /// + /// Gets or sets an optional image icon for the button to display. + /// + public string ImageUri { get; set; } + + /// + /// Gets or sets an identifier used in telemetry to identify your category of action. This should be something + /// like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will + /// be able to view how frequently your actions are being clicked. + /// + public string HintActionId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ToastButtonDismiss() + { + } + + /// + /// Initializes a new instance of the class. + /// Constructs a system-handled dismiss button that displays your text on the button. + /// + /// The text you want displayed on the button. + public ToastButtonDismiss(string customContent) + { + if (customContent == null) + { + throw new ArgumentNullException(nameof(customContent)); + } + + CustomContent = customContent; + } + + internal Element_ToastAction ConvertToElement() + { + return new Element_ToastAction() + { + Content = this.CustomContent == null ? string.Empty : this.CustomContent, // If not using custom content, we need to provide empty string, otherwise Toast doesn't get displayed + Arguments = "dismiss", + ActivationType = Element_ToastActivationType.System, + ImageUri = ImageUri, + HintActionId = HintActionId + + // InputId is useless since dismiss button can't be placed to the right of text box (shell doesn't display it) + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastButtonSnooze.cs b/components/Notifications/src/Toasts/ToastButtonSnooze.cs new file mode 100644 index 000000000..f2f29283a --- /dev/null +++ b/components/Notifications/src/Toasts/ToastButtonSnooze.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// A system-handled snooze button that automatically handles snoozing of a Toast notification. + /// + public sealed class ToastButtonSnooze : IToastButton + { + /// + /// Gets custom text displayed on the button that overrides the default localized "Snooze" text. + /// + public string CustomContent { get; private set; } + + /// + /// Gets or sets an optional image icon for the button to display. + /// + public string ImageUri { get; set; } + + /// + /// Gets or sets the ID of an existing in order to allow the + /// user to pick a custom snooze time. Optional. The ID's of the s + /// inside the selection box must represent the snooze interval in minutes. For example, + /// if the user selects an item that has an ID of "120", then the notification will be snoozed + /// for 2 hours. When the user clicks this button, if you specified a SelectionBoxId, the system + /// will parse the ID of the selected item and snooze by that amount of minutes. If you didn't specify + /// a SelectionBoxId, the system will snooze by the default system snooze time. + /// + public string SelectionBoxId { get; set; } + + /// + /// Gets or sets an identifier used in telemetry to identify your category of action. This should be something + /// like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will + /// be able to view how frequently your actions are being clicked. + /// + public string HintActionId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ToastButtonSnooze() + { + } + + /// + /// Initializes a new instance of the class. + /// Initializes a system-handled snooze button that displays your text on the button and automatically handles snoozing. + /// + /// The text you want displayed on the button. + public ToastButtonSnooze(string customContent) + { + if (customContent == null) + { + throw new ArgumentNullException(nameof(customContent)); + } + + CustomContent = customContent; + } + + internal Element_ToastAction ConvertToElement() + { + return new Element_ToastAction() + { + Content = CustomContent ?? string.Empty, // If not using custom content, we need to provide empty string, otherwise Toast doesn't get displayed + Arguments = "snooze", + ActivationType = Element_ToastActivationType.System, + InputId = SelectionBoxId, + ImageUri = ImageUri, + HintActionId = HintActionId + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastCommon.cs b/components/Notifications/src/Toasts/ToastCommon.cs new file mode 100644 index 000000000..088ec6ca0 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastCommon.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Decides the type of activation that will be used when the user interacts with the Toast notification. + /// + public enum ToastActivationType + { + /// + /// Default value. Your foreground app is launched. + /// + Foreground, + + /// + /// Your corresponding background task (assuming you set everything up) is triggered, and you can execute code in the background (like sending the user's quick reply message) without interrupting the user. + /// + Background, + + /// + /// Launch a different app using protocol activation. + /// + Protocol + } + + /// + /// Specifies the behavior that the toast should use when the user takes action on the toast. + /// + public enum ToastAfterActivationBehavior + { + /// + /// Default behavior. The toast will be dismissed when the user takes action on the toast. + /// + Default, + + /// + /// After the user clicks a button on your toast, the notification will remain present, in a "pending update" visual state. You should immediately update your toast from a background task so that the user does not see this "pending update" visual state for too long. + /// + PendingUpdate + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastContent.cs b/components/Notifications/src/Toasts/ToastContent.cs new file mode 100644 index 000000000..9af5e6044 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastContent.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +#if WINDOWS_UWP +using Windows.Data.Xml.Dom; +using Windows.UI.Notifications; +#endif + +namespace CommunityToolkit.Notifications +{ + /// + /// Base Toast element, which contains at least a visual element. + /// + public sealed class ToastContent : INotificationContent + { + /// + /// Gets or sets the visual element (Required). + /// + public ToastVisual Visual { get; set; } + + /// + /// Gets or sets custom audio options. + /// + public ToastAudio Audio { get; set; } + + /// + /// Gets or sets optional custom actions with buttons and inputs (using ) + /// or optionally use the system-default snooze/dismiss controls (with ). + /// + public IToastActions Actions { get; set; } + + /// + /// Gets or sets an optional header for the toast notification. Requires Creators Update + /// + public ToastHeader Header { get; set; } + + /// + /// Gets or sets the scenario, to make the Toast behave like an alarm, reminder, or more. + /// + public ToastScenario Scenario { get; set; } + + /// + /// Gets or sets the amount of time the Toast should display. You typically should use the + /// Scenario attribute instead, which impacts how long a Toast stays on screen. + /// + public ToastDuration Duration { get; set; } + + /// + /// Gets or sets a string that is passed to the application when it is activated by the Toast. + /// The format and contents of this string are defined by the app for its own use. When the user + /// taps or clicks the Toast to launch its associated app, the launch string provides the context + /// to the app that allows it to show the user a view relevant to the Toast content, rather than + /// launching in its default way. + /// + public string Launch { get; set; } + + /// + /// Gets or sets what activation type will be used when the user clicks the body of this Toast. + /// + public ToastActivationType ActivationType { get; set; } + + /// + /// Gets or sets additional options relating to activation of the toast notification. Requires Creators Updated + /// + public ToastActivationOptions ActivationOptions { get; set; } + + /// + /// Gets or sets an optional custom time to use for the notification's timestamp, visible within Action Center. + /// If provided, this date/time will be used on the notification instead of the date/time that the notification was received. + /// Requires Creators Update + /// + public DateTimeOffset? DisplayTimestamp { get; set; } + + /// + /// Gets or sets an identifier used in telemetry to identify your category of toast notification. This should be something + /// like "NewMessage", "AppointmentReminder", "Promo30Off", or "PleaseRate". In the upcoming toast telemetry dashboard + /// in Dev Center, you will be able to view activation info filtered by toast identifier. + /// + public string HintToastId { get; set; } + + /// + /// Gets or sets the person that this toast is related to. For more info, see the My People documentation. New in Fall Creators Update. + /// + public ToastPeople HintPeople { get; set; } + + /// + /// Gets a dictionary where you can assign additional properties. + /// + public IDictionary AdditionalProperties { get; } = new Dictionary(); + + /// + /// Retrieves the notification XML content as a string, so that it can be sent with a HTTP POST in a push notification. + /// + /// The notification XML content as a string. + public string GetContent() + { + return ConvertToElement().GetContent(); + } + +#if WINDOWS_UWP + + /// + /// Retrieves the notification XML content as a WinRT XmlDocument, so that it can be used with a local Toast notification's constructor on either or . + /// + /// The notification XML content as a WinRT XmlDocument. + public XmlDocument GetXml() + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(GetContent()); + + return doc; + } + +#endif + + internal Element_Toast ConvertToElement() + { + if (ActivationOptions != null) + { + if (ActivationOptions.AfterActivationBehavior != ToastAfterActivationBehavior.Default) + { + throw new InvalidOperationException("ToastContent does not support a custom AfterActivationBehavior. Please ensure ActivationOptions.AfterActivationBehavior is set to Default."); + } + } + + DateTimeOffset? strippedDisplayTimestamp = null; + if (DisplayTimestamp != null) + { + // We need to make sure we don't include more than 3 decimal points on seconds + // The Millisecond value itself is limited to 3 decimal points, thus by doing the following + // we bypass the more granular value that can come from Ticks and ensure we only have 3 decimals at most. + var val = DisplayTimestamp.Value; + strippedDisplayTimestamp = new DateTimeOffset(val.Year, val.Month, val.Day, val.Hour, val.Minute, val.Second, val.Millisecond, val.Offset); + } + + var toast = new Element_Toast() + { + ActivationType = Element_Toast.ConvertActivationType(ActivationType), + Duration = Duration, + Launch = Launch, + Scenario = Scenario, + DisplayTimestamp = strippedDisplayTimestamp, + HintToastId = HintToastId, + AdditionalProperties = (Dictionary)AdditionalProperties + }; + + ActivationOptions?.PopulateElement(toast); + + if (Visual != null) + { + toast.Visual = Visual.ConvertToElement(); + } + + if (Audio != null) + { + toast.Audio = Audio.ConvertToElement(); + } + + if (Actions != null) + { + toast.Actions = ConvertToActionsElement(Actions); + } + + if (Header != null) + { + toast.Header = Header.ConvertToElement(); + } + + HintPeople?.PopulateToastElement(toast); + + return toast; + } + + private static Element_ToastActions ConvertToActionsElement(IToastActions actions) + { + if (actions is ToastActionsCustom) + { + return (actions as ToastActionsCustom).ConvertToElement(); + } + + if (actions is ToastActionsSnoozeAndDismiss) + { + return (actions as ToastActionsSnoozeAndDismiss).ConvertToElement(); + } + + throw new NotImplementedException("Unknown actions type: " + actions.GetType()); + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastContextMenuItem.cs b/components/Notifications/src/Toasts/ToastContextMenuItem.cs new file mode 100644 index 000000000..a98b1a9e4 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastContextMenuItem.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// A Toast context menu item. + /// + public sealed class ToastContextMenuItem + { + /// + /// Initializes a new instance of the class. + /// A Toast context menu item with the required properties. + /// + /// The text to display on the menu item. + /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the menu item. + public ToastContextMenuItem(string content, string arguments) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (arguments == null) + { + throw new ArgumentNullException(nameof(arguments)); + } + + Content = content; + Arguments = arguments; + } + + /// + /// Gets the text to display on the menu item. Required + /// + public string Content { get; private set; } + + /// + /// Gets app-defined string of arguments that the app can later retrieve once it is activated when the user clicks the menu item. Required + /// + public string Arguments { get; private set; } + + /// + /// Gets or sets what type of activation this menu item will use when clicked. Defaults to Foreground. + /// + public ToastActivationType ActivationType { get; set; } = ToastActivationType.Foreground; + + /// + /// Gets or sets additional options relating to activation of the toast context menu item. New in Creators Update + /// + public ToastActivationOptions ActivationOptions { get; set; } + + /// + /// Gets or sets an identifier used in telemetry to identify your category of action. This should be something + /// like "TurnOff" or "ManageSettings". In the upcoming toast telemetry dashboard in Dev Center, you will + /// be able to view how frequently your actions are being clicked. + /// + public string HintActionId { get; set; } + + internal Element_ToastAction ConvertToElement() + { + var el = new Element_ToastAction + { + Content = Content, + Arguments = Arguments, + ActivationType = Element_Toast.ConvertActivationType(ActivationType), + Placement = Element_ToastActionPlacement.ContextMenu, + HintActionId = HintActionId + }; + + ActivationOptions?.PopulateElement(el); + + return el; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastGenericAppLogo.cs b/components/Notifications/src/Toasts/ToastGenericAppLogo.cs new file mode 100644 index 000000000..c2c8fd0b7 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastGenericAppLogo.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// The logo that is displayed on your Toast notification. + /// + public sealed class ToastGenericAppLogo : IBaseImage + { + /// + /// Initializes a new instance of the class, + /// a logo that is displayed on your Toast notification. + /// + public ToastGenericAppLogo() + { + } + + private string _source; + + /// + /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets set a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + /// + /// Gets or sets specify how the image should be cropped. + /// + public ToastGenericAppLogoCrop HintCrop { get; set; } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage el = BaseImageHelper.CreateBaseElement(this); + + el.Placement = AdaptiveImagePlacement.AppLogoOverride; + el.Crop = GetAdaptiveImageCrop(); + + return el; + } + + private AdaptiveImageCrop GetAdaptiveImageCrop() + { + switch (HintCrop) + { + case ToastGenericAppLogoCrop.Circle: + return AdaptiveImageCrop.Circle; + + case ToastGenericAppLogoCrop.None: + return AdaptiveImageCrop.None; + + default: + return AdaptiveImageCrop.Default; + } + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs b/components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs new file mode 100644 index 000000000..b8f79aa7f --- /dev/null +++ b/components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Specify the desired cropping of the image. + /// + public enum ToastGenericAppLogoCrop + { + /// + /// Cropping uses the default behavior of the renderer. + /// + Default, + + /// + /// Image is not cropped. + /// + None, + + /// + /// Image is cropped to a circle shape. + /// + Circle + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastGenericAttributionText.cs b/components/Notifications/src/Toasts/ToastGenericAttributionText.cs new file mode 100644 index 000000000..b3ec7d2dc --- /dev/null +++ b/components/Notifications/src/Toasts/ToastGenericAttributionText.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// Defines an attribution text element to be displayed on the Toast notification. + /// + public sealed class ToastGenericAttributionText : IBaseText + { + /// + /// Initializes a new instance of the class. + /// An attribution text element to be displayed on the Toast notification. + /// + public ToastGenericAttributionText() + { + } + + /// + /// Gets or sets the text to display. + /// + public string Text { get; set; } + + /// + /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Language { get; set; } + + internal Element_AdaptiveText ConvertToElement() + { + var el = BaseTextHelper.CreateBaseElement(this); + + el.Placement = AdaptiveTextPlacement.Attribution; + + return el; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastGenericHeroImage.cs b/components/Notifications/src/Toasts/ToastGenericHeroImage.cs new file mode 100644 index 000000000..adbb18809 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastGenericHeroImage.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// A hero image for the Toast notification. + /// + public sealed class ToastGenericHeroImage : IBaseImage + { + /// + /// Initializes a new instance of the class. + /// A hero image for the Toast notification. + /// + public ToastGenericHeroImage() + { + } + + private string _source; + + /// + /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage el = BaseImageHelper.CreateBaseElement(this); + + el.Placement = AdaptiveImagePlacement.Hero; + + return el; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastHeader.cs b/components/Notifications/src/Toasts/ToastHeader.cs new file mode 100644 index 000000000..2f9b9dabd --- /dev/null +++ b/components/Notifications/src/Toasts/ToastHeader.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Defines a visual header for the toast notification. + /// + public sealed class ToastHeader + { + /// + /// Initializes a new instance of the class. + /// Constructs a toast header with all the required properties. + /// + /// A developer-created identifier that uniquely identifies this header. If two notifications have the same header id, they will be displayed underneath the same header in Action Center. + /// A title for the header. + /// A developer-defined string of arguments that is returned to the app when the user clicks this header. + public ToastHeader(string id, string title, string arguments) + { + Id = id; + Title = title; + Arguments = arguments; + } + + private string _id; + + /// + /// Gets or sets a developer-created identifier that uniquely identifies this header. If two notifications have the same header id, they will be displayed underneath the same header in Action Center. Cannot be null. + /// + public string Id + { + get { return _id; } + set { ArgumentValidator.SetProperty(ref _id, value, nameof(Id), ArgumentValidatorOptions.NotNull); } + } + + private string _title; + + /// + /// Gets or sets a title for the header. Cannot be null. + /// + public string Title + { + get { return _title; } + set { ArgumentValidator.SetProperty(ref _title, value, nameof(Title), ArgumentValidatorOptions.NotNull); } + } + + private string _arguments; + + /// + /// Gets or sets a developer-defined string of arguments that is returned to the app when the user clicks this header. Cannot be null. + /// + public string Arguments + { + get { return _arguments; } + set { ArgumentValidator.SetProperty(ref _arguments, value, nameof(Arguments), ArgumentValidatorOptions.NotNull); } + } + + private ToastActivationType _activationType = ToastActivationType.Foreground; + + /// + /// Gets or sets the type of activation this header will use when clicked. Defaults to Foreground. Note that only Foreground and Protocol are supported. + /// + public ToastActivationType ActivationType + { + get + { + return _activationType; + } + + set + { + switch (value) + { + case ToastActivationType.Foreground: + case ToastActivationType.Protocol: + _activationType = value; + break; + + default: + throw new ArgumentException($"ActivationType of {value} is not supported on ToastHeader."); + } + } + } + + /// + /// Gets or sets additional options relating to activation of the toast header. New in Creators Update + /// + public ToastActivationOptions ActivationOptions { get; set; } + + internal Element_ToastHeader ConvertToElement() + { + if (ActivationOptions != null) + { + if (ActivationOptions.AfterActivationBehavior != ToastAfterActivationBehavior.Default) + { + throw new InvalidOperationException("ToastHeader does not support a custom AfterActivationBehavior. Please ensure ActivationOptions.AfterActivationBehavior is set to Default."); + } + } + + var el = new Element_ToastHeader() + { + Id = Id, + Title = Title, + Arguments = Arguments, + ActivationType = Element_Toast.ConvertActivationType(ActivationType) + }; + + ActivationOptions?.PopulateElement(el); + + return el; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastPeople.cs b/components/Notifications/src/Toasts/ToastPeople.cs new file mode 100644 index 000000000..3c24740b4 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastPeople.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Notifications +{ + /// + /// Specify what person this toast is related to. For more info, see the My People documentation. New in Fall Creators Update. + /// + public sealed class ToastPeople + { + /// + /// Gets or sets a remote identifier that corresponds with the RemoteId you set on a Contact you created + /// with the ContactStore APIs. For more info, see the My People documentation. + /// + public string RemoteId { get; set; } + + /// + /// Gets or sets an email address that corresponds with a contact in the local Windows ContactStore. Note + /// that if is specified, this property will be ignored. For more info, + /// see the My People documentation. + /// + public string EmailAddress { get; set; } + + /// + /// Gets or sets a phone number that corresponds with a contact in the local Windows ContactStore. Note + /// that if is specified, this property will be ignored. + /// For more info, see the My People documentation. + /// + public string PhoneNumber { get; set; } + + internal void PopulateToastElement(Element_Toast toast) + { + string hintPeople; + + if (RemoteId != null) + { + hintPeople = "remoteid:" + RemoteId; + } + else if (EmailAddress != null) + { + hintPeople = "mailto:" + EmailAddress; + } + else if (PhoneNumber != null) + { + hintPeople = "tel:" + PhoneNumber; + } + else + { + return; + } + + toast.HintPeople = hintPeople; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastSelectionBox.cs b/components/Notifications/src/Toasts/ToastSelectionBox.cs new file mode 100644 index 000000000..40d110057 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastSelectionBox.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.Notifications +{ + /// + /// A selection box control, which lets users pick from a dropdown list of options. + /// + public sealed class ToastSelectionBox : IToastInput + { + /// + /// Initializes a new instance of the class. + /// A Toast SelectionBox input control with the required elements. + /// + /// Developer-provided ID that the developer uses later to retrieve input when the Toast is interacted with. + public ToastSelectionBox(string id) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + } + + /// + /// Gets the required ID property used so that developers can retrieve user input once the app is activated. + /// + public string Id { get; private set; } + + /// + /// Gets or sets title text to display above the SelectionBox. + /// + public string Title { get; set; } + + /// + /// Gets or sets which item is selected by default, and refers to the Id property of . If you do not provide this, the default selection will be empty (user sees nothing). + /// + public string DefaultSelectionBoxItemId { get; set; } + + /// + /// Gets the selection items that the user can pick from in this SelectionBox. Only 5 items can be added. + /// + public IList Items { get; private set; } = new LimitedList(5); + + internal Element_ToastInput ConvertToElement() + { + var input = new Element_ToastInput() + { + Type = ToastInputType.Selection, + Id = Id, + DefaultInput = DefaultSelectionBoxItemId, + Title = Title + }; + + foreach (var item in Items) + { + input.Children.Add(item.ConvertToElement()); + } + + return input; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastSelectionBoxItem.cs b/components/Notifications/src/Toasts/ToastSelectionBoxItem.cs new file mode 100644 index 000000000..225a4f7ab --- /dev/null +++ b/components/Notifications/src/Toasts/ToastSelectionBoxItem.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// A selection box item (an item that the user can select from the drop down list). + /// + public sealed class ToastSelectionBoxItem + { + /// + /// Initializes a new instance of the class. + /// Constructs a new Toast SelectionBoxItem with the required elements. + /// + /// Developer-provided ID that the developer uses later to retrieve input when the Toast is interacted with. + /// String that is displayed on the selection item. This is what the user sees. + public ToastSelectionBoxItem(string id, string content) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + } + + /// + /// Gets the required ID property used so that developers can retrieve user input once the app is activated. + /// + public string Id { get; private set; } + + /// + /// Gets the required string that is displayed on the selection item. + /// + public string Content { get; private set; } + + internal Element_ToastSelection ConvertToElement() + { + return new Element_ToastSelection() + { + Id = Id, + Content = Content + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastShoulderTapImage.cs b/components/Notifications/src/Toasts/ToastShoulderTapImage.cs new file mode 100644 index 000000000..58a4e2a52 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastShoulderTapImage.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// Specifies the image to be displayed on a My People shoulder tap notification. New in Fall Creators Update. + /// + public sealed class ToastShoulderTapImage : IBaseImage + { + private string _source; + + /// + /// Gets or sets the URI of the image (Required). This will be used if the sprite sheet isn't provided, or + /// if the sprite sheet cannot be loaded. Can be from your application package, application data, or the internet. + /// Internet images must obey the toast image size restrictions. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets an optional sprite sheet that can be used instead of the image to display an animated sprite sheet. + /// + public ToastSpriteSheet SpriteSheet { get; set; } + + /// + /// Gets or sets a description of the image, for users of assistive technologies. + /// + public string AlternateText { get; set; } + + /// + /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the property. + /// Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the + /// query strings or by ignoring the query string and returning the image as specified without the query string. + /// This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + internal Element_AdaptiveImage ConvertToElement() + { + Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this); + + if (SpriteSheet != null) + { + SpriteSheet.PopulateImageElement(image); + } + + return image; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastSpriteSheet.cs b/components/Notifications/src/Toasts/ToastSpriteSheet.cs new file mode 100644 index 000000000..b8e0941f9 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastSpriteSheet.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.Notifications.Adaptive.Elements; + +namespace CommunityToolkit.Notifications +{ + /// + /// Specifies a sprite sheet. New in Fall Creators Update. + /// + public sealed class ToastSpriteSheet + { + private string _source; + + /// + /// Gets or sets the URI of the sprite sheet (Required). + /// Can be from your application package, application data, or the internet. + /// Internet sources must obey the toast image size restrictions. + /// + public string Source + { + get { return _source; } + set { BaseImageHelper.SetSource(ref _source, value); } + } + + /// + /// Gets or sets the frame-height of the sprite sheet. Required value that must be greater than 0. + /// + public uint? FrameHeight { get; set; } + + /// + /// Gets or sets the frames per second at which to animate the sprite sheet. Required value that must be greater than 0 but no larger than 120. + /// + public uint? Fps { get; set; } + + /// + /// Gets or sets the starting frame of the sprite sheet. If not specified, it will start at frame zero. + /// + public uint? StartingFrame { get; set; } + + internal void PopulateImageElement(Element_AdaptiveImage image) + { + if (Source == null) + { + throw new NullReferenceException("Source cannot be null on ToastSpriteSheet"); + } + + image.SpriteSheetSrc = Source; + image.SpriteSheetHeight = FrameHeight; + image.SpriteSheetFps = Fps; + image.SpriteSheetStartingFrame = StartingFrame; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastTextBox.cs b/components/Notifications/src/Toasts/ToastTextBox.cs new file mode 100644 index 000000000..432b75e70 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastTextBox.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// A text box control on the Toast that a user can type text into. + /// + public sealed class ToastTextBox : IToastInput + { + /// + /// Initializes a new instance of the class. + /// A new Toast TextBox input control with the required elements. + /// + /// Developer-provided ID that the developer uses later to retrieve input when the Toast is interacted with. + public ToastTextBox(string id) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + } + + /// + /// Gets the required ID property so that developers can retrieve user input once the app is activated. + /// + public string Id { get; private set; } + + /// + /// Gets or sets title text to display above the text box. + /// + public string Title { get; set; } + + /// + /// Gets or sets placeholder text to be displayed on the text box when the user hasn't typed any text yet. + /// + public string PlaceholderContent { get; set; } + + /// + /// Gets or sets the initial text to place in the text box. Leave this null for a blank text box. + /// + public string DefaultInput { get; set; } + + internal Element_ToastInput ConvertToElement() + { + return new Element_ToastInput() + { + Id = Id, + Type = ToastInputType.Text, + DefaultInput = DefaultInput, + PlaceholderContent = PlaceholderContent, + Title = Title + }; + } + } +} \ No newline at end of file diff --git a/components/Notifications/src/Toasts/ToastVisual.cs b/components/Notifications/src/Toasts/ToastVisual.cs new file mode 100644 index 000000000..f6f349783 --- /dev/null +++ b/components/Notifications/src/Toasts/ToastVisual.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.Notifications +{ + /// + /// Defines the visual aspects of a Toast notification. + /// + public sealed class ToastVisual + { + /// + /// Gets or sets the target locale of the XML payload, specified as BCP-47 language tags such as "en-US" or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. + /// + public string Language { get; set; } + + /// + /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. + /// + public Uri BaseUri { get; set; } + + /// + /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Toast notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language. + /// + public bool? AddImageQuery { get; set; } + + /// + /// Gets or sets the generic Toast binding, which can be rendered on all devices. This binding is required and cannot be null. + /// + public ToastBindingGeneric BindingGeneric { get; set; } + + /// + /// Gets or sets a binding for shoulder tap notifications, which integrate with My People. See the My People documentation for more info. New in Fall Creators Update. + /// + public ToastBindingShoulderTap BindingShoulderTap { get; set; } + + internal Element_ToastVisual ConvertToElement() + { + var visual = new Element_ToastVisual() + { + Language = Language, + BaseUri = BaseUri, + AddImageQuery = AddImageQuery + }; + + if (BindingGeneric == null) + { + throw new NullReferenceException("BindingGeneric must be initialized"); + } + + Element_ToastBinding binding = BindingGeneric.ConvertToElement(); + + // TODO: If a BaseUri wasn't provided, we can potentially optimize the payload size by calculating the best BaseUri + visual.Bindings.Add(binding); + + if (BindingShoulderTap != null) + { + visual.Bindings.Add(BindingShoulderTap.ConvertToElement()); + } + + return visual; + } + } +} \ No newline at end of file diff --git a/components/Notifications/tests/Notifications.Tests.projitems b/components/Notifications/tests/Notifications.Tests.projitems new file mode 100644 index 000000000..34ce1542c --- /dev/null +++ b/components/Notifications/tests/Notifications.Tests.projitems @@ -0,0 +1,24 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 695EB17B-60C6-4D00-9D3F-1BC54B6A9500 + + + NotificationsExperiment.Tests + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/Notifications/tests/Notifications.Tests.shproj b/components/Notifications/tests/Notifications.Tests.shproj new file mode 100644 index 000000000..910121c08 --- /dev/null +++ b/components/Notifications/tests/Notifications.Tests.shproj @@ -0,0 +1,13 @@ + + + + 695EB17B-60C6-4D00-9D3F-1BC54B6A9500 + 14.0 + + + + + + + + diff --git a/components/Notifications/tests/TestAssertHelper.cs b/components/Notifications/tests/TestAssertHelper.cs new file mode 100644 index 000000000..df1fea6bf --- /dev/null +++ b/components/Notifications/tests/TestAssertHelper.cs @@ -0,0 +1,457 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class TestAssertHelper + { + [TestMethod] + public void TestAssertXmlElement() + { + AssertHelper.AssertXml(" Hello world", " Hello world "); + } + + [TestMethod] + public void TestAssertXmlElement_002() + { + try + { + AssertHelper.AssertXml(" Hello world", " Hello world "); + } + catch + { + return; + } + + Assert.Fail("tile element name was different, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_003() + { + try + { + AssertHelper.AssertXml(" Hello world", " Hello world "); + } + catch + { + return; + } + + Assert.Fail("visual element name was incorrect, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_004() + { + try + { + AssertHelper.AssertXml(" Hello world", " Hello world "); + } + catch + { + return; + } + + Assert.Fail("visual version number was incorrect, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_005() + { + try + { + AssertHelper.AssertXml(" Hello world", " Hello world! "); + } + catch + { + return; + } + + Assert.Fail("text content was different, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_006() + { + AssertHelper.AssertXml("", ""); + } + + [TestMethod] + public void TestAssertXmlElement_006_1() + { + try + { + AssertHelper.AssertXml("", ""); + } + catch + { + return; + } + + Assert.Fail("Version number was incorrect, should have thrown exception."); + } + + [TestMethod] + public void TestAssertXmlElement_006_2() + { + try + { + AssertHelper.AssertXml("", ""); + } + catch + { + return; + } + + Assert.Fail("ID number was incorrect, should have thrown exception."); + } + + [TestMethod] + public void TestAssertXmlElement_007() + { + AssertHelper.AssertXml("", ""); + } + + [TestMethod] + public void TestAssertXmlElement_008() + { + AssertHelper.AssertXml("", ""); + + try + { + AssertHelper.AssertXml("", ""); + } + catch + { + return; + } + + Assert.Fail("Visual element was missing, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_009() + { + AssertHelper.AssertXml("", ""); + + try + { + AssertHelper.AssertXml("", ""); + } + catch + { + return; + } + + Assert.Fail("Child elements were different order, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_010() + { + try + { + AssertHelper.AssertXml("", ""); + } + catch + { + return; + } + + Assert.Fail("id attribute value wasn't the same, should have thrown exception"); + } + + [TestMethod] + public void TestAssertXmlElement_011() + { + try + { + AssertHelper.AssertXml("", ""); + } + catch + { + return; + } + + Assert.Fail("id attribute was missing, should have thrown exception"); + } + } + +#pragma warning disable SA1204 // Static elements should appear before instance elements +#pragma warning disable SA1402 // File may only contain a single type + public static class AssertHelper +#pragma warning restore SA1402 // File may only contain a single type +#pragma warning restore SA1204 // Static elements should appear before instance elements + { + private class XmlElementHelper + { + } + + /* + public static void AssertXml(string expected, string actual) + { + XmlDocument expectedDoc = new XmlDocument(); + expectedDoc.LoadXml(expected); + + XmlDocument actualDoc = new XmlDocument(); + actualDoc.LoadXml(actual); + + AssertXmlElement(expectedDoc.DocumentElement, actualDoc.DocumentElement); + } + + private static void AssertXmlElement(XmlElement expected, XmlElement actual) + { + // If both null, good, done + if (expected == null && actual == null) + return; + + // If one is null and other isn't, bad + if (expected == null) + Assert.Fail("Expected XML element was null, while actual was initialized"); + + if (actual == null) + Assert.Fail("Actual XML element was null, while expected was initialized"); + + // If name doesn't match + Assert.AreEqual(expected.Name, actual.Name, "Element names did not match."); + + // If attribute count doesn't match + Assert.AreEqual(expected.Attributes.Count, actual.Attributes.Count, "Element attributes counts didn't match"); + + // Make sure attributes match (order does NOT matter) + foreach (XmlAttribute expectedAttr in expected.Attributes) + { + var actualAttr = actual.Attributes.GetNamedItem(expectedAttr.Name); + + // If didn't find the attribute + if (actualAttr == null) + Assert.Fail("Expected element to have attribute " + expectedAttr.Name + " but it didn't."); + + // Make sure value matches + Assert.AreEqual(expectedAttr.Value, actualAttr.Value, $@"Attribute values for ""{expectedAttr.Name}"" didn't match."); + } + + // Make sure children elements match (order DOES matter) + + // Obtain the child elements (ignore any comments, w + XmlElement[] expectedChildren = expected.ChildNodes.OfType().ToArray(); + XmlElement[] actualChildren = actual.ChildNodes.OfType().ToArray(); + + Assert.AreEqual(expectedChildren.Length, actualChildren.Length, "Number of child elements did not match."); + + // If no elements, compare inner text + if (expectedChildren.Length == 0) + { + Assert.AreEqual(expected.InnerText, actual.InnerText, "Inner text did not match."); + } + + // Otherwise compare elements + else + { + for (int i = 0; i < expectedChildren.Length; i++) + { + AssertXmlElement(expectedChildren[i], actualChildren[i]); + } + } + } + */ + + public static void AssertToast(string expected, ToastContent toast) + { + AssertHelper.AssertXml(expected.ToLower(), toast.GetContent().ToLower()); + +#if WINDOWS_UWP + // For WinRT, we'll test the XmlDocument method too, make sure it works. + AssertHelper.AssertXml(expected, toast.GetXml().GetXml()); +#endif + } + + public static void AssertTile(string expected, TileContent tile) + { + AssertHelper.AssertXml(expected.ToLower(), tile.GetContent().ToLower()); + +#if WINDOWS_UWP + // For WinRT, we'll test the XmlDocument method too, make sure it works. + AssertHelper.AssertXml(expected, tile.GetXml().GetXml()); +#endif + } + + public static void AssertXml(string expected, string actual) + { + MyXmlElement expectedEl = ParseXml(expected); + MyXmlElement actualEl = ParseXml(actual); + + AssertXmlElement(expectedEl, actualEl); + } + + private static string AttributesToString(IEnumerable attributes) + { + return string.Join(", ", attributes.Select(i => i.Name)); + } + + private static void AssertXmlElement(MyXmlElement expected, MyXmlElement actual) + { + // If both null, good, done + if (expected == null && actual == null) + { + return; + } + + // If one is null and other isn't, bad + if (expected == null) + { + Assert.Fail("Expected XML element was null, while actual was initialized"); + } + + if (actual == null) + { + Assert.Fail("Actual XML element was null, while expected was initialized"); + } + + // If name doesn't match + Assert.AreEqual(expected.Name.ToLower(), actual.Name.ToLower(), "Element names did not match."); + + // If attribute count doesn't match + Assert.AreEqual(expected.Attributes.Count, actual.Attributes.Count, $"Different number of attributes on <{expected.Name}>\n\nExpected: " + AttributesToString(expected.Attributes) + "\nActual: " + AttributesToString(actual.Attributes)); + + // Make sure attributes match (order does NOT matter) + foreach (MyXmlAttribute expectedAttr in expected.Attributes) + { + var actualAttr = actual.Attributes.FirstOrDefault(i => i.Name.Equals(expectedAttr.Name)); + + // If didn't find the attribute + if (actualAttr == null) + { + Assert.Fail("Expected element to have attribute " + expectedAttr.Name + " but it didn't."); + } + + // Make sure value matches + Assert.AreEqual(expectedAttr.Value.ToLower(), actualAttr.Value.ToLower(), $@"Attribute values for ""{expectedAttr.Name}"" didn't match."); + } + + // Make sure children elements match (order DOES matter) + + // Obtain the child elements (ignore any comments, w + MyXmlElement[] expectedChildren = expected.ChildNodes.ToArray(); + MyXmlElement[] actualChildren = actual.ChildNodes.ToArray(); + + Assert.AreEqual(expectedChildren.Length, actualChildren.Length, "Number of child elements did not match."); + + // Compare elements + for (int i = 0; i < expectedChildren.Length; i++) + { + AssertXmlElement(expectedChildren[i], actualChildren[i]); + } + } + + private class MyXmlElement + { + public string Name { get; set; } + + public List ChildNodes { get; private set; } = new List(); + + public List Attributes { get; private set; } = new List(); + } + + private class MyXmlAttribute + { + public string Name { get; set; } + + public string Value { get; set; } + } + + private static MyXmlElement ParseXml(string xml) + { + XmlReader reader = XmlReader.Create(new StringReader(xml)); + + MyXmlElement documentElement = new MyXmlElement(); + + reader.Read(); + + while (true) + { + if (reader.ReadState == ReadState.EndOfFile) + { + break; + } + + if (reader.ReadState == ReadState.Error) + { + throw new Exception("ReadState was Error"); + } + + if (reader.NodeType == XmlNodeType.Element) + { + PopulateElement(reader, documentElement); + ParseXml(reader, documentElement); + break; + } + + reader.Read(); + } + + return documentElement; + } + + private static void PopulateElement(XmlReader reader, MyXmlElement into) + { + into.Name = reader.Name; + + int attrCount = reader.AttributeCount; + for (int i = 0; i < attrCount; i++) + { + reader.MoveToNextAttribute(); + + into.Attributes.Add(new MyXmlAttribute() + { + Name = reader.Name, + Value = reader.Value + }); + } + } + + private static void ParseXml(XmlReader reader, MyXmlElement intoElement) + { + if (!reader.Read()) + { + return; + } + + while (true) + { + switch (reader.NodeType) + { + // Found child + case XmlNodeType.Element: + case XmlNodeType.Text: + MyXmlElement child = new MyXmlElement(); + PopulateElement(reader, child); + ParseXml(reader, child); + intoElement.ChildNodes.Add(child); + break; + + // All done + case XmlNodeType.EndElement: + return; + } + + if (!reader.Read()) + { + return; + } + } + } + } +} diff --git a/components/Notifications/tests/TestMail.cs b/components/Notifications/tests/TestMail.cs new file mode 100644 index 000000000..5a56fb751 --- /dev/null +++ b/components/Notifications/tests/TestMail.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class TestMail + { + private const string FirstFrom = "Jennifer Parker"; + private const string FirstSubject = "Photos from our trip"; + private const string FirstBody = "Check out these awesome photos I took while in New Zealand!"; + + private const string SecondFrom = "Steve Bosniak"; + private const string SecondSubject = "Build 2015 Dinner"; + private const string SecondBody = "Want to go out for dinner after Build tonight?"; + + [TestCategory("EndToEnd/Mail")] + [TestMethod] + public void TestMailTile() + { + TileBinding small = new TileBinding() + { + Content = new TileBindingContentIconic() + { + Icon = new TileBasicImage() { Source = "Assets\\Mail.png" } + } + }; + + TileBinding medium = new TileBinding() + { + Branding = TileBranding.Logo, + + Content = new TileBindingContentAdaptive() + { + Children = + { + GenerateFirstMessage(false), + GenerateSpacer(), + GenerateSecondMessage(false) + } + } + }; + + TileBinding wideAndLarge = new TileBinding() + { + Branding = TileBranding.NameAndLogo, + + Content = new TileBindingContentAdaptive() + { + Children = + { + GenerateFirstMessage(true), + GenerateSpacer(), + GenerateSecondMessage(true) + } + } + }; + + TileContent content = new TileContent() + { + Visual = new TileVisual() + { + TileSmall = small, + TileMedium = medium, + TileWide = wideAndLarge, + TileLarge = wideAndLarge + } + }; + + string expectedXml = $@""; + + // Medium + expectedXml += @""; + expectedXml += GenerateXmlGroups(false); + expectedXml += ""; + + // Wide + expectedXml += @""; + expectedXml += GenerateXmlGroups(true); + expectedXml += ""; + + // Large + expectedXml += @""; + expectedXml += GenerateXmlGroups(true); + expectedXml += ""; + + expectedXml += ""; + + AssertHelper.AssertTile(expectedXml, content); + } + + private static string GenerateXmlGroups(bool makeLarge) + { + return GenerateXmlGroup(FirstFrom, FirstSubject, FirstBody, makeLarge) + "" + GenerateXmlGroup(SecondFrom, SecondSubject, SecondBody, makeLarge); + } + + private static string GenerateXmlGroup(string from, string subject, string body, bool makeLarge) + { + string xml = "{from}{subject}{body}"; + + return xml; + } + + private static AdaptiveText GenerateSpacer() + { + return new AdaptiveText(); + } + + private static AdaptiveGroup GenerateFirstMessage(bool makeLarge) + { + return GenerateMessage(FirstFrom, FirstSubject, FirstBody, makeLarge); + } + + private static AdaptiveGroup GenerateSecondMessage(bool makeLarge) + { + return GenerateMessage(SecondFrom, SecondSubject, SecondBody, makeLarge); + } + + private static AdaptiveGroup GenerateMessage(string from, string subject, string body, bool makeLarge) + { + return new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveText() + { + Text = from, + HintStyle = makeLarge ? AdaptiveTextStyle.Subtitle : AdaptiveTextStyle.Caption + }, + + new AdaptiveText() + { + Text = subject, + HintStyle = AdaptiveTextStyle.CaptionSubtle + }, + + new AdaptiveText() + { + Text = body, + HintStyle = AdaptiveTextStyle.CaptionSubtle + } + } + } + } + }; + } + } +} diff --git a/components/Notifications/tests/TestTileContentBuilder.cs b/components/Notifications/tests/TestTileContentBuilder.cs new file mode 100644 index 000000000..9c6554ebb --- /dev/null +++ b/components/Notifications/tests/TestTileContentBuilder.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ +#if !WINRT + [TestClass] + public class TestTileContentBuilder + { + [TestMethod] + public void AddTextTest_OnSmallTileOnly() + { + // Arrange + string text = "text on small tile"; + TileContentBuilder builder = new TileContentBuilder(); + builder.AddTile(TileSize.Small); + + // Act + builder.AddText(text); + + // Assert + var tileText = GetTileAdaptiveText(builder, TileSize.Small); + Assert.IsNotNull(tileText); + Assert.AreSame("text on small tile", (string)tileText.Text); + } + + [TestMethod] + public void AddTextTest_OnMediumTileOnly() + { + // Arrange + string text = "text on medium tile"; + TileContentBuilder builder = new TileContentBuilder(); + builder.AddTile(TileSize.Medium); + + // Act + builder.AddText(text); + + // Assert + var tileText = GetTileAdaptiveText(builder, TileSize.Medium); + Assert.IsNotNull(tileText); + Assert.AreSame("text on medium tile", (string)tileText.Text); + } + + [TestMethod] + public void AddTextTest_OnWideTileOnly() + { + // Arrange + string text = "text on wide tile"; + TileContentBuilder builder = new TileContentBuilder(); + builder.AddTile(TileSize.Wide); + + // Act + builder.AddText(text); + + // Assert + var tileText = GetTileAdaptiveText(builder, TileSize.Wide); + Assert.IsNotNull(tileText); + Assert.AreSame("text on wide tile", (string)tileText.Text); + } + + [TestMethod] + public void AddTextTest_OnLargeTileOnly() + { + // Arrange + string text = "text on large tile"; + TileContentBuilder builder = new TileContentBuilder(); + builder.AddTile(TileSize.Large); + + // Act + builder.AddText(text); + + // Assert + var tileText = GetTileAdaptiveText(builder, TileSize.Large); + Assert.IsNotNull(tileText); + Assert.AreSame("text on large tile", (string)tileText.Text); + } + + private static AdaptiveText GetTileAdaptiveText(TileContentBuilder builder, TileSize size) + { + TileBinding tileBinding; + switch (size) + { + case TileSize.Small: + tileBinding = builder.Content.Visual.TileSmall; + break; + + case TileSize.Medium: + tileBinding = builder.Content.Visual.TileMedium; + break; + + case TileSize.Wide: + tileBinding = builder.Content.Visual.TileWide; + break; + + case TileSize.Large: + tileBinding = builder.Content.Visual.TileLarge; + break; + + default: + return null; + } + + var content = (TileBindingContentAdaptive)tileBinding.Content; + return content.Children.FirstOrDefault() as AdaptiveText; + } + } +#endif +} diff --git a/components/Notifications/tests/TestToastArguments.cs b/components/Notifications/tests/TestToastArguments.cs new file mode 100644 index 000000000..9bafbad08 --- /dev/null +++ b/components/Notifications/tests/TestToastArguments.cs @@ -0,0 +1,511 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class TestToastArguments + { + [TestMethod] + public void TestAddExceptions_NullName() + { + ToastArguments query = new ToastArguments(); + + try + { + query.Add(null, "value"); + } + catch (ArgumentNullException) + { + return; + } + + Assert.Fail("Adding null name shouldn't be allowed."); + } + + [TestMethod] + public void TestParsing() + { + AssertParse(new ToastArguments(), string.Empty); + AssertParse(new ToastArguments(), " "); + AssertParse(new ToastArguments(), "\n"); + AssertParse(new ToastArguments(), "\t \n"); + AssertParse(new ToastArguments(), null); + + AssertParse( + new ToastArguments() + { + { "isBook" } + }, "isBook"); + + AssertParse( + new ToastArguments() + { + { "isBook" }, + { "isRead" } + }, "isBook;isRead"); + + AssertParse( + new ToastArguments() + { + { "isBook" }, + { "isRead" }, + { "isLiked" } + }, "isBook;isRead;isLiked"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" } + }, "name=Andrew"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" }, + { "isAdult" } + }, "name=Andrew;isAdult"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" }, + { "isAdult" } + }, "isAdult;name=Andrew"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" }, + { "age", "22" } + }, "age=22;name=Andrew"); + } + + [TestMethod] + public void TestToString_ExactString() + { + Assert.AreEqual(string.Empty, new ToastArguments().ToString()); + + Assert.AreEqual("isBook", new ToastArguments() + { + { "isBook" } + }.ToString()); + + Assert.AreEqual("name=Andrew", new ToastArguments() + { + { "name", "Andrew" } + }.ToString()); + } + + [TestMethod] + public void TestSpecialCharacters() + { + Assert.AreEqual("full name=Andrew Leader", new ToastArguments() + { + { "full name", "Andrew Leader" } + }.ToString()); + + Assert.AreEqual("name%3Bcompany=Andrew%3BMicrosoft", new ToastArguments() + { + { "name;company", "Andrew;Microsoft" } + }.ToString()); + + Assert.AreEqual("name/company=Andrew/Microsoft", new ToastArguments() + { + { "name/company", "Andrew/Microsoft" } + }.ToString()); + + Assert.AreEqual("message=Dinner?", new ToastArguments() + { + { "message", "Dinner?" } + }.ToString()); + + Assert.AreEqual("message=to: Andrew", new ToastArguments() + { + { "message", "to: Andrew" } + }.ToString()); + + Assert.AreEqual("email=andrew@live.com", new ToastArguments() + { + { "email", "andrew@live.com" } + }.ToString()); + + Assert.AreEqual("messsage=food%3Dyummy", new ToastArguments() + { + { "messsage", "food=yummy" } + }.ToString()); + + Assert.AreEqual("messsage=$$$", new ToastArguments() + { + { "messsage", "$$$" } + }.ToString()); + + Assert.AreEqual("messsage=-_.!~*'()", new ToastArguments() + { + { "messsage", "-_.!~*'()" } + }.ToString()); + + Assert.AreEqual("messsage=this & that", new ToastArguments() + { + { "messsage", "this & that" } + }.ToString()); + + Assert.AreEqual("messsage=20%25 off!", new ToastArguments() + { + { "messsage", "20% off!" } + }.ToString()); + + Assert.AreEqual("messsage=Nonsense %2526 %2525 %253D", new ToastArguments() + { + { "messsage", "Nonsense %26 %25 %3D" } + }.ToString()); + } + + [TestMethod] + public void TestDecoding() + { + AssertDecode("Hello world", "Hello world"); + + AssertDecode(";/?:@&=+$%", "%3B/?:@&%3D+$%25"); + AssertDecode("-_.!~*'()", "-_.!~*'()"); + } + +#if !WINRT + [TestMethod] + public void TestIndexer_NullException() + { + try + { + string val = new ToastArguments()[null]; + } + catch (ArgumentNullException) + { + return; + } + + Assert.Fail("Exception should have been thrown."); + } + + [TestMethod] + public void TestIndexer_NotFoundException() + { + try + { + var args = new ToastArguments() + { + { "name", "Andrew" } + }; + + _ = args["Name"]; + } + catch (KeyNotFoundException) + { + return; + } + + Assert.Fail("Exception should have been thrown."); + } + + [TestMethod] + public void TestIndexer() + { + AssertIndexer(null, "isBook;name=Andrew", "isBook"); + + AssertIndexer("Andrew", "isBook;name=Andrew", "name"); + + AssertIndexer("Andrew", "count=2;name=Andrew", "name"); + } +#endif + + [TestMethod] + public void TestRemove_OnlyKey() + { + ToastArguments qs = new ToastArguments() + { + { "name", "Andrew" }, + { "age", "22" } + }; + + Assert.IsTrue(qs.Remove("age")); + + AssertEqual( + new ToastArguments() + { + { "name", "Andrew" } + }, qs); + + Assert.IsFalse(qs.Remove("age")); + } + + [TestMethod] + public void TestContains() + { + ToastArguments qs = new ToastArguments(); + + Assert.IsFalse(qs.Contains("name")); + Assert.IsFalse(qs.Contains("name", "Andrew")); + + qs.Add("isBook"); + + Assert.IsFalse(qs.Contains("name")); + Assert.IsFalse(qs.Contains("name", "Andrew")); + + Assert.IsTrue(qs.Contains("isBook")); + Assert.IsTrue(qs.Contains("isBook", null)); + Assert.IsFalse(qs.Contains("isBook", "True")); + + qs.Add("isBook", "True"); + + Assert.IsTrue(qs.Contains("isBook")); + Assert.IsFalse(qs.Contains("isBook", null)); + Assert.IsTrue(qs.Contains("isBook", "True")); + + qs.Add("name", "Andrew"); + + Assert.IsTrue(qs.Contains("name")); + Assert.IsFalse(qs.Contains("name", null)); // Value doesn't exist + Assert.IsTrue(qs.Contains("name", "Andrew")); + Assert.IsFalse(qs.Contains("Name", "Andrew")); // Wrong case on name + Assert.IsFalse(qs.Contains("name", "andrew")); // Wrong case on value + Assert.IsFalse(qs.Contains("Name")); // Wrong case on name + } + + [TestMethod] + public void TestAdd() + { + ToastArguments qs = new ToastArguments(); + + qs.Add("name", "Andrew"); + + AssertEqual( + new ToastArguments() + { + { "name", "Andrew" } + }, qs); + + qs.Add("age", "22"); + + AssertEqual( + new ToastArguments() + { + { "name", "Andrew" }, + { "age", "22" } + }, qs); + + qs.Add("name", "Lei"); + + AssertEqual( + new ToastArguments() + { + { "name", "Lei" }, + { "age", "22" } + }, qs); + + string nullStr = null; + qs.Add("name", nullStr); + + AssertEqual( + new ToastArguments() + { + { "name" }, + { "age", "22" } + }, qs); + } + + [TestMethod] + public void TestEnumerator() + { + KeyValuePair[] parameters = ToastArguments.Parse("name=Andrew;age=22;isOld").ToArray(); + + Assert.AreEqual(3, parameters.Length); + Assert.AreEqual(1, parameters.Count(i => i.Key.Equals("name"))); + Assert.AreEqual(1, parameters.Count(i => i.Key.Equals("age"))); + Assert.AreEqual(1, parameters.Count(i => i.Key.Equals("isOld"))); + Assert.IsTrue(parameters.Any(i => i.Key.Equals("name") && i.Value.Equals("Andrew"))); + Assert.IsTrue(parameters.Any(i => i.Key.Equals("age") && i.Value.Equals("22"))); + Assert.IsTrue(parameters.Any(i => i.Key.Equals("isOld") && i.Value == null)); + } + + [TestMethod] + public void TestCount() + { + ToastArguments qs = new ToastArguments(); + + Assert.AreEqual(0, qs.Count); + + qs.Add("name", "Andrew"); + + Assert.AreEqual(1, qs.Count); + + qs.Add("age", "22"); + + Assert.AreEqual(2, qs.Count); + + qs.Remove("age"); + + Assert.AreEqual(1, qs.Count); + + qs.Remove("name"); + + Assert.AreEqual(0, qs.Count); + } + + [TestMethod] + public void TestStronglyTyped() + { + ToastArguments args = new ToastArguments() + .Add("isAdult", true) + .Add("isPremium", false) + .Add("age", 22) + .Add("level", 0) + .Add("gpa", 3.97) + .Add("percent", 97.3f); + +#if !WINRT + args.Add("activationKind", ToastActivationType.Background); +#endif + + AssertEqual( + new ToastArguments() + { + { "isAdult", "1" }, + { "isPremium", "0" }, + { "age", "22" }, + { "level", "0" }, + { "gpa", "3.97" }, + { "percent", "97.3" }, +#if !WINRT + { "activationKind", "1" } +#endif + }, args); + + Assert.AreEqual(true, args.GetBool("isAdult")); + Assert.AreEqual("1", args.Get("isAdult")); + + Assert.AreEqual(false, args.GetBool("isPremium")); + Assert.AreEqual("0", args.Get("isPremium")); + + Assert.AreEqual(22, args.GetInt("age")); + Assert.AreEqual(22d, args.GetDouble("age")); + Assert.AreEqual(22f, args.GetFloat("age")); + Assert.AreEqual("22", args.Get("age")); + + Assert.AreEqual(0, args.GetInt("level")); + + Assert.AreEqual(3.97d, args.GetDouble("gpa")); + + Assert.AreEqual(97.3f, args.GetFloat("percent")); + +#if !WINRT + Assert.AreEqual(ToastActivationType.Background, args.GetEnum("activationKind")); + + if (args.TryGetValue("activationKind", out ToastActivationType activationType)) + { + Assert.AreEqual(ToastActivationType.Background, activationType); + } + else + { + Assert.Fail("TryGetValue as enum failed"); + } + + // Trying to get enum that isn't an enum should return false + Assert.IsFalse(args.TryGetValue("percent", out activationType)); +#endif + + // After serializing and deserializing, the same should work + args = ToastArguments.Parse(args.ToString()); + + Assert.AreEqual(true, args.GetBool("isAdult")); + Assert.AreEqual("1", args.Get("isAdult")); + + Assert.AreEqual(false, args.GetBool("isPremium")); + Assert.AreEqual("0", args.Get("isPremium")); + + Assert.AreEqual(22, args.GetInt("age")); + Assert.AreEqual(22d, args.GetDouble("age")); + Assert.AreEqual(22f, args.GetFloat("age")); + Assert.AreEqual("22", args.Get("age")); + + Assert.AreEqual(0, args.GetInt("level")); + + Assert.AreEqual(3.97d, args.GetDouble("gpa")); + + Assert.AreEqual(97.3f, args.GetFloat("percent")); + +#if !WINRT + Assert.AreEqual(ToastActivationType.Background, args.GetEnum("activationKind")); + + if (args.TryGetValue("activationKind", out activationType)) + { + Assert.AreEqual(ToastActivationType.Background, activationType); + } + else + { + Assert.Fail("TryGetValue as enum failed"); + } + + // Trying to get enum that isn't an enum should return false + Assert.IsFalse(args.TryGetValue("percent", out activationType)); +#endif + } + +#if !WINRT + private static void AssertIndexer(string expected, string queryString, string paramName) + { + ToastArguments q = ToastArguments.Parse(queryString); + + Assert.AreEqual(expected, q[paramName]); + } +#endif + + private static void AssertDecode(string expected, string encoded) + { + Assert.AreEqual(expected, ToastArguments.Parse("message=" + encoded).Get("message")); + } + + private static void AssertParse(ToastArguments expected, string inputQueryString) + { + Assert.IsTrue(IsSame(expected, ToastArguments.Parse(inputQueryString)), "Expected: " + expected + "\nActual: " + inputQueryString); + } + + private static void AssertEqual(ToastArguments expected, ToastArguments actual) + { + Assert.IsTrue(IsSame(expected, actual), "Expected: " + expected + "\nActual: " + actual); + + Assert.IsTrue(IsSame(expected, ToastArguments.Parse(actual.ToString())), "After serializing and parsing actual, result changed.\n\nExpected: " + expected + "\nActual: " + ToastArguments.Parse(actual.ToString())); + Assert.IsTrue(IsSame(ToastArguments.Parse(expected.ToString()), actual), "After serializing and parsing expected, result changed.\n\nExpected: " + ToastArguments.Parse(expected.ToString()) + "\nActual: " + actual); + Assert.IsTrue(IsSame(ToastArguments.Parse(expected.ToString()), ToastArguments.Parse(actual.ToString())), "After serializing and parsing both, result changed.\n\nExpected: " + ToastArguments.Parse(expected.ToString()) + "\nActual: " + ToastArguments.Parse(actual.ToString())); + } + + private static bool IsSame(ToastArguments expected, ToastArguments actual) + { + if (expected.Count != actual.Count) + { + return false; + } + + foreach (var pair in expected) + { + if (!actual.Contains(pair.Key)) + { + return false; + } + + if (actual.Get(pair.Key) != pair.Value) + { + return false; + } + } + + return true; + } + } +} diff --git a/components/Notifications/tests/TestToastContentBuilder.cs b/components/Notifications/tests/TestToastContentBuilder.cs new file mode 100644 index 000000000..9152c04fa --- /dev/null +++ b/components/Notifications/tests/TestToastContentBuilder.cs @@ -0,0 +1,1201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ +#if !WINRT + [TestClass] + public class TestToastContentBuilder + { + [TestMethod] + public void AddCustomTimeStampTest_WithCustomTimeStamp_ReturnSelfWithCustomTimeStampAdded() + { + // Arrange + DateTime testCustomTimeStamp = DateTime.UtcNow; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddCustomTimeStamp(testCustomTimeStamp); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testCustomTimeStamp, builder.Content.DisplayTimestamp); + } + + [TestMethod] + public void AddHeaderTest_WithExpectedArgs_ReturnSelfWithHeaderAdded() + { + // Arrange + string testToastHeaderId = "Test Header ID"; + string testToastTitle = "Test Toast Title"; + string testToastArguments = "Test Toast Arguments"; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddHeader(testToastHeaderId, testToastTitle, testToastArguments); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testToastHeaderId, builder.Content.Header.Id); + Assert.AreEqual(testToastTitle, builder.Content.Header.Title); + Assert.AreEqual(testToastArguments, builder.Content.Header.Arguments); + } + + [TestMethod] + public void AddHeaderTest_WithExpectedArgsAndToastArguments_ReturnSelfWithHeaderAdded() + { + // Arrange + string testToastHeaderId = "Test Header ID"; + string testToastTitle = "Test Toast Title"; + ToastArguments testToastArguments = new ToastArguments() + .Add("arg1", 5) + .Add("arg2", "tacos"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddHeader(testToastHeaderId, testToastTitle, testToastArguments); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testToastHeaderId, builder.Content.Header.Id); + Assert.AreEqual(testToastTitle, builder.Content.Header.Title); + Assert.AreEqual(testToastArguments.ToString(), builder.Content.Header.Arguments); + } + + [TestMethod] + public void AddToastActivationInfoTest_WithExpectedArgs_ReturnSelfWithActivationInfoAdded() + { + // Arrange + string testToastLaunchArugments = "Test Toast Launch Args"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddToastActivationInfo(testToastLaunchArugments, testToastActivationType); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testToastLaunchArugments, builder.Content.Launch); + Assert.AreEqual(testToastActivationType, builder.Content.ActivationType); + } + + [TestMethod] + public void AddArgumentTest_Basic_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("userId", 542) + .AddArgument("name", "Andrew"); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("userId=542;name=Andrew", builder.Content.Launch); + } + + [TestMethod] + public void AddArgumentTest_NoValue_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("isPurelyInformational"); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("isPurelyInformational", builder.Content.Launch); + } + + [TestMethod] + public void AddArgumentTest_Escaping_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("user;Id", "andrew=leader%26bares"); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("user%3BId=andrew%3Dleader%2526bares", builder.Content.Launch); + } + + [TestMethod] + public void AddArgumentTest_Replacing_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("userId", 542) + .AddArgument("name", "Andrew") + .AddArgument("userId", 601); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("userId=601;name=Andrew", builder.Content.Launch); + } + + [TestMethod] + public void AddArgumentTest_Generic_ReturnSelfWithArgumentsAdded() + { + // Arrange + const string userIdKey = "userId"; + const int userIdValue = 542; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddButton(new ToastButton() + .SetContent("Accept") + .AddArgument("action", "accept") + .SetBackgroundActivation()) + .AddButton(new ToastButtonSnooze()) + .AddButton("View", ToastActivationType.Protocol, "https://msn.com") + + // Add generic arguments halfway through (should be applied to existing buttons and to any subsequent buttons added later) + .AddArgument(userIdKey, userIdValue) + + .AddButton(new ToastButton() + .SetContent("Decline") + .AddArgument("action", "decline") + .SetBackgroundActivation()) + .AddButton(new ToastButton() + .SetContent("Report") + .SetProtocolActivation(new Uri("https://microsoft.com"))); + + // Assert + Assert.AreSame(builder, anotherReference); + + // Top level arguments should be present + Assert.AreEqual("userId=542", builder.Content.Launch); + + // All foreground/background activation buttons should have received generic arguments. Protocol and system activation buttons shouldn't have had any arguments changed. + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("action=accept;userId=542", button1.Arguments); + + var button2 = actions.Buttons[1]; + Assert.IsInstanceOfType(button2, typeof(ToastButtonSnooze)); + + var button3 = actions.Buttons[2] as ToastButton; + Assert.AreEqual("View", button3.Content); + Assert.AreEqual("https://msn.com", button3.Arguments); + + var button4 = actions.Buttons[3] as ToastButton; + Assert.AreEqual("Decline", button4.Content); + Assert.AreEqual("action=decline;userId=542", button4.Arguments); + + var button5 = actions.Buttons[4] as ToastButton; + Assert.AreEqual("Report", button5.Content); + Assert.AreEqual("https://microsoft.com/", button5.Arguments); + } + + [TestMethod] + public void AddArgumentTest_ReplacingWithinButton_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddButton(new ToastButton() + .SetContent("Accept") + .AddArgument("action", "accept") + .AddArgument("userId", 601) + .SetBackgroundActivation()) + + // Add generic arguments halfway through (in this case shouldn't overwrite anything) + .AddArgument("userId", 542) + + .AddButton(new ToastButton() + .SetContent("Decline") + .AddArgument("action", "decline") + .AddArgument("userId", 601) + .SetBackgroundActivation()); + + // Assert + Assert.AreSame(builder, anotherReference); + + // Top level arguments should be present + Assert.AreEqual("userId=542", builder.Content.Launch); + + // Buttons should have overridden the generic userId + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("action=accept;userId=601", button1.Arguments); + + var button2 = actions.Buttons[1] as ToastButton; + Assert.AreEqual("Decline", button2.Content); + Assert.AreEqual("action=decline;userId=601", button2.Arguments); + } + + [TestMethod] + public void AddArgumentTest_AvoidModifyingCustomButtons_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddToastActivationInfo("myCustomLaunchStr", ToastActivationType.Foreground) + + .AddButton("Accept", ToastActivationType.Background, "myAcceptStr") + + // userId shouldn't be added to any of these except view + .AddArgument("userId", 542) + + .AddButton("Decline", ToastActivationType.Background, "myDeclineStr") + + .AddButton(new ToastButton() + .SetContent("View") + .AddArgument("action", "view")); + + // Assert + Assert.AreSame(builder, anotherReference); + + // Top level arguments should be the custom string since user set that + Assert.AreEqual("myCustomLaunchStr", builder.Content.Launch); + + // Buttons should have their custom strings except the last + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("myAcceptStr", button1.Arguments); + + var button2 = actions.Buttons[1] as ToastButton; + Assert.AreEqual("Decline", button2.Content); + Assert.AreEqual("myDeclineStr", button2.Arguments); + + var button3 = actions.Buttons[2] as ToastButton; + Assert.AreEqual("View", button3.Content); + Assert.AreEqual("action=view;userId=542", button3.Arguments); + } + + [TestMethod] + public void AddArgumentTest_BackgroundActivation_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("userId", 542) + .SetBackgroundActivation(); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("userId=542", builder.Content.Launch); + Assert.AreEqual(ToastActivationType.Background, builder.Content.ActivationType); + } + + [TestMethod] + public void SetProtocolActivationTest_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddButton(new ToastButton() + .SetContent("Accept") + .AddArgument("action", "accept") + .SetBackgroundActivation()) + + .AddArgument("userId", 542) + + .SetProtocolActivation(new Uri("https://msn.com/")) + + .AddArgument("name", "Andrew") + + .AddButton(new ToastButton() + .SetContent("Decline") + .AddArgument("action", "decline") + .SetBackgroundActivation()); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("https://msn.com/", builder.Content.Launch); + Assert.AreEqual(ToastActivationType.Protocol, builder.Content.ActivationType); + + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("action=accept;userId=542;name=Andrew", button1.Arguments); + + var button2 = actions.Buttons[1] as ToastButton; + Assert.AreEqual("Decline", button2.Content); + Assert.AreEqual("action=decline;userId=542;name=Andrew", button2.Arguments); + } + + [TestMethod] + public void ToastButtonBuilders_General_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .AddArgument("action", "view") + .AddArgument("imageId", 601); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("action=view;imageId=601", button.Arguments); + Assert.AreEqual(ToastActivationType.Foreground, button.ActivationType); + } + + [TestMethod] + public void ToastButtonBuilders_AllProperties_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .SetImageUri(new Uri("ms-appx:///Assets/view.png")) + .AddArgument("action", "view") + .SetBackgroundActivation() + .SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate) + .SetHintActionId("viewImage"); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("action=view", button.Arguments); + Assert.AreEqual("ms-appx:///Assets/view.png", button.ImageUri); + Assert.AreEqual(ToastActivationType.Background, button.ActivationType); + Assert.AreEqual(ToastAfterActivationBehavior.PendingUpdate, button.ActivationOptions.AfterActivationBehavior); + Assert.AreEqual("viewImage", button.HintActionId); + } + + [TestMethod] + public void ToastButtonBuilders_ProtocolActivation_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .SetProtocolActivation(new Uri("https://msn.com")); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("https://msn.com/", button.Arguments); + Assert.AreEqual(ToastActivationType.Protocol, button.ActivationType); + } + + [TestMethod] + public void ToastButtonBuilders_ProtocolActivationWithPfn_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .SetProtocolActivation(new Uri("https://msn.com"), "MyPfn"); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("https://msn.com/", button.Arguments); + Assert.AreEqual(ToastActivationType.Protocol, button.ActivationType); + Assert.AreEqual("MyPfn", button.ActivationOptions.ProtocolActivationTargetApplicationPfn); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidProtocolAfterArguments_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .AddArgument("action", "view") + .SetProtocolActivation(new Uri("https://msn.com")); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidDismissAfterArguments_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .AddArgument("action", "view") + .SetDismissActivation(); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidSnoozeAfterArguments_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .AddArgument("action", "view") + .SetSnoozeActivation(); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidSnoozeWithIdAfterArguments_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .AddArgument("action", "view") + .SetSnoozeActivation("snoozeId"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterProtocol_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .SetProtocolActivation(new Uri("https://msn.com")) + .AddArgument("action", "view"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterCustomArguments_ReturnSelf() + { + var button = new ToastButton("View", "viewArgs"); + + button.AddArgument("action", "view"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterSnooze_ReturnSelf() + { + new ToastButton() + .SetContent("Later") + .SetSnoozeActivation() + .AddArgument("action", "later"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterSnoozeWithId_ReturnSelf() + { + new ToastButton() + .SetContent("Later") + .SetSnoozeActivation("myId") + .AddArgument("action", "later"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterDismissActivation_ReturnSelf() + { + new ToastButton() + .SetContent("Later") + .SetDismissActivation() + .AddArgument("action", "later"); + } + + [TestMethod] + public void SetToastDurationTest_WithCustomToastDuration_ReturnSelfWithCustomToastDurationSet() + { + // Arrange + ToastDuration testToastDuration = ToastDuration.Long; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.SetToastDuration(testToastDuration); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testToastDuration, builder.Content.Duration); + } + + [TestMethod] + public void SetToastScenarioTest_WithCustomToastScenario_ReturnSelfWithCustomToastScenarioSet() + { + // Arrange + ToastScenario testToastScenario = ToastScenario.Default; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.SetToastScenario(testToastScenario); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testToastScenario, builder.Content.Scenario); + } + + [TestMethod] + public void AddAudioTest_WithAudioUriOnly_ReturnSelfWithCustomAudioAdded() + { + // Arrange + Uri testAudioUriSrc = new Uri("C:/justatesturi.mp3"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString); + } + + [TestMethod] + public void AddAudioTest_WithFullArgs_ReturnSelfWithCustomAudioAddedWithAllOptionsSet() + { + // Arrange + Uri testAudioUriSrc = new Uri("C:/justatesturi.mp3"); + bool testToastAudioLoop = true; + bool testToastAudioSilent = true; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc, testToastAudioLoop, testToastAudioSilent); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString); + Assert.AreEqual(testToastAudioLoop, builder.Content.Audio.Loop); + Assert.AreEqual(testToastAudioSilent, builder.Content.Audio.Silent); + } + + [TestMethod] + public void AddAudioTest_WithMsWinSoundEvent_ReturnSelfWithCustomAudioAdded() + { + // Arrange + Uri testAudioUriSrc = new Uri("ms-winsoundevent:Notification.Reminder"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString); + } + + [TestMethod] + public void AddAudioTest_WithMsAppx_ReturnSelfWithCustomAudioAdded() + { + // Arrange + Uri testAudioUriSrc = new Uri("ms-appx:///Assets/Audio.mp3"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void AddAudioTest_WithInvalidMsUri_ThrowException() + { + // Arrange + Uri testAudioUriSrc = new Uri("ms-doesntexist:Notification.Reminder"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + builder.AddAudio(testAudioUriSrc); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void AddAudioTest_WithInvalidAppDataUri_ThrowException() + { + // Arrange (ms-appdata isn't currently supported) + Uri testAudioUriSrc = new Uri("ms-appdata:///local/Sound.mp3"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + builder.AddAudio(testAudioUriSrc); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void AddAudioTest_WithInvalidHttpUri_ThrowException() + { + // Arrange + Uri testAudioUriSrc = new Uri("https://myaudio.com/song.mp3"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + builder.AddAudio(testAudioUriSrc); + } + + [TestMethod] + public void AddAudioTest_WithAudioObject_ReturnSelfWithCustomAudioAdded() + { + // Arrange + var audio = new ToastAudio() + { + Silent = true + }; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAudio(audio); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreSame(audio, builder.Content.Audio); + } + + [TestMethod] + public void AddAttributionTextTest_WithSimpleText_ReturnSelfWithCustomAttributionTextAdded() + { + // Arrange + string testAttributionText = "Test Attribution Text"; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAttributionText(testAttributionText); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAttributionText, builder.Content.Visual.BindingGeneric.Attribution.Text); + } + + [TestMethod] + public void AddAttributionTextTest_WithTextAndLanguage_ReturnSelfWithCustomAttributionTextAndLanguageAdded() + { + // Arrange + string testAttributionText = "Test Attribution Text"; + string testAttributionTextLanguage = "en-US"; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAttributionText(testAttributionText, testAttributionTextLanguage); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAttributionText, builder.Content.Visual.BindingGeneric.Attribution.Text); + Assert.AreEqual(testAttributionTextLanguage, builder.Content.Visual.BindingGeneric.Attribution.Language); + } + + [TestMethod] + public void AddAppLogoOverrideTest_WithLogoUriOnly_ReturnSelfWithCustomLogoAdded() + { + // Arrange + Uri testAppLogoUriSrc = new Uri("C:/justatesturi.jpg"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAppLogoOverride(testAppLogoUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAppLogoUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.AppLogoOverride.Source); + } + + [TestMethod] + public void AddAppLogoOverrideTest_WithCustomLogoAndFullOptions_ReturnSelfWithCustomLogoAndOptionsAdded() + { + // Arrange + Uri testAppLogoUriSrc = new Uri("C:/justatesturi.jpg"); + ToastGenericAppLogoCrop testCropOption = ToastGenericAppLogoCrop.Circle; + string testLogoAltText = "Test Logo Alt Text"; + bool testLogoAddImageQuery = true; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddAppLogoOverride(testAppLogoUriSrc, testCropOption, testLogoAltText, testLogoAddImageQuery); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testAppLogoUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.AppLogoOverride.Source); + Assert.AreEqual(testCropOption, builder.Content.Visual.BindingGeneric.AppLogoOverride.HintCrop); + Assert.AreEqual(testLogoAltText, builder.Content.Visual.BindingGeneric.AppLogoOverride.AlternateText); + Assert.AreEqual(testLogoAddImageQuery, builder.Content.Visual.BindingGeneric.AppLogoOverride.AddImageQuery); + } + + [TestMethod] + public void AddHeroImageTest_WithHeroImageUriOnly_ReturnSelfWithHeroImageAdded() + { + // Arrange + Uri testHeroImageUriSrc = new Uri("C:/justatesturi.jpg"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddHeroImage(testHeroImageUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testHeroImageUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.HeroImage.Source); + } + + [TestMethod] + public void AddHeroImageTest_WithHeroImageUriAndFullOptions_ReturnSelfWithHeroImageAndOptionsAdded() + { + // Arrange + Uri testHeroImageUriSrc = new Uri("C:/justatesturi.jpg"); + string testHeroImageAltText = "Test Hero Image Text"; + bool testHeroImageAddImageQuery = true; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddHeroImage(testHeroImageUriSrc, testHeroImageAltText, testHeroImageAddImageQuery); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testHeroImageUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.HeroImage.Source); + Assert.AreEqual(testHeroImageAltText, builder.Content.Visual.BindingGeneric.HeroImage.AlternateText); + Assert.AreEqual(testHeroImageAddImageQuery, builder.Content.Visual.BindingGeneric.HeroImage.AddImageQuery); + } + + [TestMethod] + public void AddInlineImageTest_WithInlineImageUriOnly_ReturnSelfWithInlineImageAdded() + { + // Arrange + Uri testInlineImageUriSrc = new Uri("C:/justatesturi.jpg"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddInlineImage(testInlineImageUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testInlineImageUriSrc.OriginalString, (builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveImage).Source); + } + + [TestMethod] + public void AddInlineImageTest_WithInlineImageAndFullOptions_ReturnSelfWithInlineImageAndOptionsAdded() + { + // Arrange + Uri testInlineImageUriSrc = new Uri("C:/justatesturi.jpg"); + string testInlineImageAltText = "Test Inline Image Text"; + bool testInlineImageAddImageQuery = true; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddInlineImage(testInlineImageUriSrc, testInlineImageAltText, testInlineImageAddImageQuery); + + // Assert + Assert.AreSame(builder, anotherReference); + + var image = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveImage; + + Assert.AreEqual(testInlineImageUriSrc.OriginalString, image.Source); + Assert.AreEqual(testInlineImageAltText, image.AlternateText); + Assert.AreEqual(testInlineImageAddImageQuery, image.AddImageQuery); + } + + [TestMethod] + public void AddProgressBarTest_WithoutInputArgs_ReturnSelfWithNonIndeterminateBindableProgressBarAdded() + { + // Arrange + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddProgressBar(); + + // Assert + Assert.AreSame(builder, anotherReference); + var progressBar = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveProgressBar; + + Assert.IsNotNull(progressBar.Title.BindingName); + Assert.IsNotNull(progressBar.Value.BindingName); + Assert.IsNotNull(progressBar.ValueStringOverride.BindingName); + Assert.IsNotNull(progressBar.Status.BindingName); + } + + [TestMethod] + public void AddProgressBarTest_WithFixedPropertiesAndDeterminateValue_ReturnSelfWithFixedValueAndPropertiesProgressBarAdded() + { + // Arrange + string testProgressBarTitle = "Test Progress Bar Title"; + double testProgressBarValue = 0.25; + string testValueStringOverride = "Test Value String Override"; + string testProgressBarStatus = "Test Progress Bar Status"; + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddProgressBar(testProgressBarTitle, testProgressBarValue, false, testValueStringOverride, testProgressBarStatus); + + // Assert + Assert.AreSame(builder, anotherReference); + var progressBar = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveProgressBar; + + Assert.IsNull(progressBar.Title.BindingName); + Assert.AreEqual(testProgressBarTitle, (string)progressBar.Title); + + Assert.IsNull(progressBar.Value.BindingName); + Assert.AreEqual(testProgressBarValue, ((AdaptiveProgressBarValue)progressBar.Value).Value); + + Assert.IsNull(progressBar.ValueStringOverride.BindingName); + Assert.AreEqual(testValueStringOverride, (string)progressBar.ValueStringOverride); + + Assert.IsNull(progressBar.Status.BindingName); + Assert.AreEqual(testProgressBarStatus, (string)progressBar.Status); + } + + [TestMethod] + public void AddProgressBarTest_WithIndeterminateValue_ReturnSelfWithIndeterminateProgressBarAdded() + { + // Arrange + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddProgressBar(isIndeterminate: true); + + // Assert + Assert.AreSame(builder, anotherReference); + var progressBar = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveProgressBar; + + Assert.IsTrue(((AdaptiveProgressBarValue)progressBar.Value).IsIndeterminate); + } + + [TestMethod] + public void AddTextTest_WithSimpleText_ReturnSelfWithTextAdded() + { + // Arrange + string testText = "Test Text"; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddText(testText); + + // Assert + Assert.AreSame(builder, anotherReference); + var text = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveText; + + Assert.AreEqual(testText, (string)text.Text); + } + + [TestMethod] + public void AddTextTest_WithMultipleTexts_ReturnSelfWithAllTextsAdded() + { + // Arrange + string testText1 = "Test Header"; + string testText2 = "Test Text"; + string testText3 = "Test Text Again"; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddText(testText1) + .AddText(testText2) + .AddText(testText3); + + // Assert + Assert.AreSame(builder, anotherReference); + var texts = builder.Content.Visual.BindingGeneric.Children.Take(3).Cast().ToList(); + + Assert.AreEqual(testText1, (string)texts[0].Text); + Assert.AreEqual(testText2, (string)texts[1].Text); + Assert.AreEqual(testText3, (string)texts[2].Text); + } + + [TestMethod] + public void AddTextTest_WithTextAndFullOptions_ReturnSelfWithTextAndAllOptionsAdded() + { + // Arrange + string testText = "Test Text"; + AdaptiveTextStyle testStyle = AdaptiveTextStyle.Header; + bool testWrapHint = true; + int testHintMaxLine = 2; + int testHintMinLine = 1; + AdaptiveTextAlign testAlign = AdaptiveTextAlign.Auto; + string testLanguage = "en-US"; + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddText(testText, testStyle, testWrapHint, testHintMaxLine, testHintMinLine, testAlign, testLanguage); + + // Assert + Assert.AreSame(builder, anotherReference); + var text = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveText; + + Assert.AreEqual(testText, (string)text.Text); + Assert.AreEqual(testHintMaxLine, text.HintMaxLines); + Assert.AreEqual(testLanguage, text.Language); + + // These values should still be the default values, since they aren't used for top-level text + Assert.AreEqual(AdaptiveTextStyle.Default, text.HintStyle); + Assert.IsNull(text.HintWrap); + Assert.IsNull(text.HintMinLines); + Assert.AreEqual(AdaptiveTextAlign.Default, text.HintAlign); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void AddTextTest_WithMoreThan4LinesOfText_ThrowInvalidOperationException() + { + // Arrange + string testText1 = "Test Header"; + string testText2 = "Test Text"; + string testText3 = "Test Text Again"; + string testText4 = "Test Text Again x2"; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + _ = builder.AddText(testText1) + .AddText(testText2) + .AddText(testText3) + .AddText(testText4); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddTextTest_WithMaxLinesValueLargerThan2_ThrowArgumentOutOfRangeException() + { + // Arrange + string testText1 = "Test Header"; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + _ = builder.AddText(testText1, hintMaxLines: 3); + } + + [TestMethod] + public void AddVisualChildTest_WithCustomVisual_ReturnSelfWithCustomVisualAdded() + { + // Arrange + // Taken from : https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#adaptive-content + AdaptiveGroup group = new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveText() + { + Text = "52 attendees", + HintStyle = AdaptiveTextStyle.Base + }, + new AdaptiveText() + { + Text = "23 minute drive", + HintStyle = AdaptiveTextStyle.CaptionSubtle + } + } + }, + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveText() + { + Text = "1 Microsoft Way", + HintStyle = AdaptiveTextStyle.CaptionSubtle, + HintAlign = AdaptiveTextAlign.Right + }, + new AdaptiveText() + { + Text = "Bellevue, WA 98008", + HintStyle = AdaptiveTextStyle.CaptionSubtle, + HintAlign = AdaptiveTextAlign.Right + } + } + } + } + }; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddVisualChild(group); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.IsInstanceOfType(builder.Content.Visual.BindingGeneric.Children.First(), typeof(AdaptiveGroup)); + } + + [TestMethod] + public void AddButtonTest_WithTextOnlyButton_ReturnSelfWithButtonAdded() + { + // Arrange + string testButtonContent = "Test Button Content"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + string testButtonLaunchArgs = "Test Launch Args"; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddButton(testButtonContent, testToastActivationType, testButtonLaunchArgs); + + // Assert + Assert.AreSame(builder, anotherReference); + + var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; + Assert.AreEqual(testButtonContent, button.Content); + Assert.AreEqual(testToastActivationType, button.ActivationType); + Assert.AreEqual(testButtonLaunchArgs, button.Arguments); + } + + [TestMethod] + public void AddButtonTest_WithCustomImageAndTextButton_ReturnSelfWithButtonAdded() + { + // Arrange + string testButtonContent = "Test Button Content"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + string testButtonLaunchArgs = "Test Launch Args"; + Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddButton(testButtonContent, testToastActivationType, testButtonLaunchArgs, testImageUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + + var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; + Assert.AreEqual(testButtonContent, button.Content); + Assert.AreEqual(testToastActivationType, button.ActivationType); + Assert.AreEqual(testButtonLaunchArgs, button.Arguments); + Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); + } + + [TestMethod] + public void AddButtonTest_WithTextBoxId_ReturnSelfWithButtonAdded() + { + // Arrange + string testInputTextBoxId = Guid.NewGuid().ToString(); + string testButtonContent = "Test Button Content"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + string testButtonLaunchArgs = "Test Launch Args"; + Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddButton(testInputTextBoxId, testButtonContent, testToastActivationType, testButtonLaunchArgs, testImageUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + + var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; + Assert.AreEqual(testInputTextBoxId, button.TextBoxId); + Assert.AreEqual(testButtonContent, button.Content); + Assert.AreEqual(testToastActivationType, button.ActivationType); + Assert.AreEqual(testButtonLaunchArgs, button.Arguments); + Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); + } + + [TestMethod] + public void AddInputTextBoxTest_WithStringIdOnly_ReturnSelfWithInputTextBoxAdded() + { + // Arrange + string testInputTextBoxId = Guid.NewGuid().ToString(); + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddInputTextBox(testInputTextBoxId); + + // Assert + Assert.AreSame(builder, anotherReference); + var inputTextBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastTextBox; + + Assert.AreEqual(testInputTextBoxId, inputTextBox.Id); + } + + [TestMethod] + public void AddInputTextBoxTest_WithPlaceHolderContentAndTitle_ReturnSelfWithInputTextBoxAndAllOptionsAdded() + { + // Arrange + string testInputTextBoxId = Guid.NewGuid().ToString(); + string testInputTextBoxPlaceHolderContent = "Placeholder Content"; + string testInputTextBoxTitle = "Test Title"; + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddInputTextBox(testInputTextBoxId, testInputTextBoxPlaceHolderContent, testInputTextBoxTitle); + + // Assert + Assert.AreSame(builder, anotherReference); + var inputTextBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastTextBox; + + Assert.AreEqual(testInputTextBoxId, inputTextBox.Id); + Assert.AreEqual(testInputTextBoxPlaceHolderContent, inputTextBox.PlaceholderContent); + Assert.AreEqual(testInputTextBoxTitle, inputTextBox.Title); + } + + [TestMethod] + public void AddComboBoxTest_WithMultipleChoices_ReturnSelfWithComboBoxAndAllChoicesAdded() + { + // Arrange + string testComboBoxId = Guid.NewGuid().ToString(); + var choice1 = (Guid.NewGuid().ToString(), "Test Choice 1"); + var choice2 = (Guid.NewGuid().ToString(), "Test Choice 2"); + var choice3 = (Guid.NewGuid().ToString(), "Test Choice 3"); + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddComboBox(testComboBoxId, choice1, choice2, choice3); + + // Assert + Assert.AreSame(builder, anotherReference); + var comboBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastSelectionBox; + + Assert.AreEqual(testComboBoxId, comboBox.Id); + Assert.AreEqual(choice1.Item1, comboBox.Items[0].Id); + Assert.AreEqual(choice2.Item1, comboBox.Items[1].Id); + Assert.AreEqual(choice3.Item1, comboBox.Items[2].Id); + + Assert.AreEqual(choice1.Item2, comboBox.Items[0].Content); + Assert.AreEqual(choice2.Item2, comboBox.Items[1].Content); + Assert.AreEqual(choice3.Item2, comboBox.Items[2].Content); + } + + [TestMethod] + public void AddComboBoxTest_WithMultipleChoicesAndDefaultSelected_ReturnSelfWithComboBoxAddedWithAllChoicesAndDefaultSelection() + { + // Arrange + string testComboBoxId = Guid.NewGuid().ToString(); + var choice1 = (Guid.NewGuid().ToString(), "Test Choice 1"); + var choice2 = (Guid.NewGuid().ToString(), "Test Choice 2"); + var choice3 = (Guid.NewGuid().ToString(), "Test Choice 3"); + string defaultChoice = choice2.Item1; + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddComboBox(testComboBoxId, defaultChoice, choice1, choice2, choice3); + + // Assert + Assert.AreSame(builder, anotherReference); + var comboBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastSelectionBox; + + Assert.AreEqual(testComboBoxId, comboBox.Id); + Assert.AreEqual(choice1.Item1, comboBox.Items[0].Id); + Assert.AreEqual(choice2.Item1, comboBox.Items[1].Id); + Assert.AreEqual(choice3.Item1, comboBox.Items[2].Id); + + Assert.AreEqual(choice1.Item2, comboBox.Items[0].Content); + Assert.AreEqual(choice2.Item2, comboBox.Items[1].Content); + Assert.AreEqual(choice3.Item2, comboBox.Items[2].Content); + + Assert.AreEqual(defaultChoice, comboBox.DefaultSelectionBoxItemId); + } + + [TestMethod] + public void AddComboBoxTest_WithMultipleChoiceAndDefaultSelectedAndTitle_ReturnSelfWithComboBoxAddedWithAllChoicesAndDefaultSelectionAndTitle() + { + // Arrange + string testComboBoxId = Guid.NewGuid().ToString(); + var choice1 = (Guid.NewGuid().ToString(), "Test Choice 1"); + var choice2 = (Guid.NewGuid().ToString(), "Test Choice 2"); + var choice3 = (Guid.NewGuid().ToString(), "Test Choice 3"); + string defaultChoice = choice2.Item1; + string testComboBoxTitle = "Test Title"; + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddComboBox(testComboBoxId, testComboBoxTitle, defaultChoice, choice1, choice2, choice3); + + // Assert + Assert.AreSame(builder, anotherReference); + var comboBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastSelectionBox; + + Assert.AreEqual(testComboBoxId, comboBox.Id); + Assert.AreEqual(choice1.Item1, comboBox.Items[0].Id); + Assert.AreEqual(choice2.Item1, comboBox.Items[1].Id); + Assert.AreEqual(choice3.Item1, comboBox.Items[2].Id); + + Assert.AreEqual(choice1.Item2, comboBox.Items[0].Content); + Assert.AreEqual(choice2.Item2, comboBox.Items[1].Content); + Assert.AreEqual(choice3.Item2, comboBox.Items[2].Content); + + Assert.AreEqual(defaultChoice, comboBox.DefaultSelectionBoxItemId); + Assert.AreEqual(testComboBoxTitle, comboBox.Title); + } + } + +#endif +} diff --git a/components/Notifications/tests/TestWeather.cs b/components/Notifications/tests/TestWeather.cs new file mode 100644 index 000000000..2ae75f69e --- /dev/null +++ b/components/Notifications/tests/TestWeather.cs @@ -0,0 +1,292 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class TestWeather + { + private const string ImageMostlyCloudy = "Assets\\Tiles\\Mostly Cloudy.png"; + private const string ImageSunny = "Assets\\Tiles\\Sunny.png"; + private const string ImageCloudy = "Assets\\Tiles\\Cloudy.png"; + + private const string BackgroundImageMostlyCloudy = "Assets\\Tiles\\Mostly Cloudy-Background.jpg"; + + [TestMethod] + public void TestWeatherTile() + { + var backgroundImage = BackgroundImageMostlyCloudy; + int overlay = 30; + + TileBindingContentAdaptive smallContent = new TileBindingContentAdaptive() + { + TextStacking = TileTextStacking.Center, + BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay }, + Children = + { + new AdaptiveText() + { + Text = "Mon", + HintStyle = AdaptiveTextStyle.Body, + HintAlign = AdaptiveTextAlign.Center + }, + + new AdaptiveText() + { + Text = "63°", + HintStyle = AdaptiveTextStyle.Base, + HintAlign = AdaptiveTextAlign.Center + } + } + }; + + TileBindingContentAdaptive mediumContent = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay }, + Children = + { + new AdaptiveGroup() + { + Children = + { + GenerateMediumSubgroup("Mon", ImageMostlyCloudy, 63, 42), + GenerateMediumSubgroup("Tue", ImageCloudy, 57, 38) + } + } + } + }; + + TileBindingContentAdaptive wideContent = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay }, + Children = + { + new AdaptiveGroup() + { + Children = + { + GenerateWideSubgroup("Mon", ImageMostlyCloudy, 63, 42), + GenerateWideSubgroup("Tue", ImageCloudy, 57, 38), + GenerateWideSubgroup("Wed", ImageSunny, 59, 43), + GenerateWideSubgroup("Thu", ImageSunny, 62, 42), + GenerateWideSubgroup("Fri", ImageSunny, 71, 66) + } + } + } + }; + + TileBindingContentAdaptive largeContent = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay }, + Children = + { + new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + HintWeight = 30, + Children = + { + new AdaptiveImage() { Source = ImageMostlyCloudy } + } + }, + + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveText() + { + Text = "Monday", + HintStyle = AdaptiveTextStyle.Base + }, + + new AdaptiveText() + { + Text = "63° / 42°" + }, + + new AdaptiveText() + { + Text = "20% chance of rain", + HintStyle = AdaptiveTextStyle.CaptionSubtle + }, + + new AdaptiveText() + { + Text = "Winds 5 mph NE", + HintStyle = AdaptiveTextStyle.CaptionSubtle + } + } + } + } + }, + + // For spacing + new AdaptiveText(), + + new AdaptiveGroup() + { + Children = + { + GenerateLargeSubgroup("Tue", ImageCloudy, 57, 38), + GenerateLargeSubgroup("Wed", ImageSunny, 59, 43), + GenerateLargeSubgroup("Thu", ImageSunny, 62, 42), + GenerateLargeSubgroup("Fri", ImageSunny, 71, 66) + } + } + } + }; + + TileContent content = new TileContent() + { + Visual = new TileVisual() + { + DisplayName = "Seattle", + + TileSmall = new TileBinding() + { + Content = smallContent + }, + + TileMedium = new TileBinding() + { + Content = mediumContent, + Branding = TileBranding.Name + }, + + TileWide = new TileBinding() + { + Content = wideContent, + Branding = TileBranding.NameAndLogo + }, + + TileLarge = new TileBinding() + { + Content = largeContent, + Branding = TileBranding.NameAndLogo + } + } + }; + + string expectedPayload = $@"{GenerateStringBackgroundImage()}Mon63°{GenerateStringBackgroundImage()}"; + + // Medium tile subgroups + expectedPayload += GenerateStringMediumSubgroup("Mon", ImageMostlyCloudy, 63, 42); + expectedPayload += GenerateStringMediumSubgroup("Tue", ImageCloudy, 57, 38); + + expectedPayload += ""; + + // Wide tile + expectedPayload += @""; + expectedPayload += GenerateStringBackgroundImage(); + expectedPayload += ""; + + // Wide tile subgroups + expectedPayload += GenerateStringWideSubgroup("Mon", ImageMostlyCloudy, 63, 42); + expectedPayload += GenerateStringWideSubgroup("Tue", ImageCloudy, 57, 38); + expectedPayload += GenerateStringWideSubgroup("Wed", ImageSunny, 59, 43); + expectedPayload += GenerateStringWideSubgroup("Thu", ImageSunny, 62, 42); + expectedPayload += GenerateStringWideSubgroup("Fri", ImageSunny, 71, 66); + + expectedPayload += ""; + + // Large tile + expectedPayload += @""; + expectedPayload += GenerateStringBackgroundImage(); + expectedPayload += $@"Monday63° / 42°20% chance of rainWinds 5 mph NE"; + + expectedPayload += ""; + expectedPayload += ""; + + // Large tile subgroups + expectedPayload += GenerateStringLargeSubgroup("Tue", ImageCloudy, 57, 38); + expectedPayload += GenerateStringLargeSubgroup("Wed", ImageSunny, 59, 43); + expectedPayload += GenerateStringLargeSubgroup("Thu", ImageSunny, 62, 42); + expectedPayload += GenerateStringLargeSubgroup("Fri", ImageSunny, 71, 66); + + expectedPayload += ""; + + AssertHelper.AssertTile(expectedPayload, content); + } + + private static string GenerateStringBackgroundImage() + { + return $@""; + } + + private static string GenerateStringMediumSubgroup(string day, string image, int high, int low) + { + return $@"{day}{high}°{low}°"; + } + + private static string GenerateStringWideSubgroup(string day, string image, int high, int low) + { + return $@"{day}{high}°{low}°"; + } + + private static string GenerateStringLargeSubgroup(string day, string image, int high, int low) + { + return $@"{day}{high}°{low}°"; + } + + private static AdaptiveSubgroup GenerateMediumSubgroup(string day, string image, int high, int low) + { + return new AdaptiveSubgroup() + { + Children = + { + new AdaptiveText() + { + Text = day, + HintAlign = AdaptiveTextAlign.Center + }, + + new AdaptiveImage() + { + Source = image, + HintRemoveMargin = true + }, + + new AdaptiveText() + { + Text = high + "°", + HintAlign = AdaptiveTextAlign.Center + }, + + new AdaptiveText() + { + Text = low + "°", + HintAlign = AdaptiveTextAlign.Center, + HintStyle = AdaptiveTextStyle.CaptionSubtle + } + } + }; + } + + private static AdaptiveSubgroup GenerateWideSubgroup(string day, string image, int high, int low) + { + var subgroup = GenerateMediumSubgroup(day, image, high, low); + + subgroup.HintWeight = 1; + + return subgroup; + } + + private static AdaptiveSubgroup GenerateLargeSubgroup(string day, string image, int high, int low) + { + var subgroup = GenerateWideSubgroup(day, image, high, low); + + (subgroup.Children[1] as AdaptiveImage).HintRemoveMargin = null; + + return subgroup; + } + } +} diff --git a/components/Notifications/tests/Test_Adaptive_Xml.cs b/components/Notifications/tests/Test_Adaptive_Xml.cs new file mode 100644 index 000000000..d1fca5b12 --- /dev/null +++ b/components/Notifications/tests/Test_Adaptive_Xml.cs @@ -0,0 +1,604 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class Test_Adaptive_Xml + { + [TestMethod] + public void Test_Adaptive_Text_Defaults() + { + AssertAdaptiveChild("", new AdaptiveText()); + } + + [TestMethod] + public void Test_Adaptive_Text_Text() + { + AssertAdaptiveChild("Hello & Goodbye", new AdaptiveText() + { + Text = "Hello & Goodbye" + }); + + // Data binding should work + AssertAdaptiveChild("{title}", new AdaptiveText() + { +#if WINRT + Bindings = + { + { AdaptiveTextBindableProperty.Text, "title" } + } +#else + Text = new BindableString("title") +#endif + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintStyle_Values() + { + AssertAdaptiveTextStyle("caption", AdaptiveTextStyle.Caption); + AssertAdaptiveTextStyle("captionSubtle", AdaptiveTextStyle.CaptionSubtle); + AssertAdaptiveTextStyle("base", AdaptiveTextStyle.Base); + AssertAdaptiveTextStyle("baseSubtle", AdaptiveTextStyle.BaseSubtle); + AssertAdaptiveTextStyle("body", AdaptiveTextStyle.Body); + AssertAdaptiveTextStyle("bodySubtle", AdaptiveTextStyle.BodySubtle); + AssertAdaptiveTextStyle("subtitle", AdaptiveTextStyle.Subtitle); + AssertAdaptiveTextStyle("subtitleSubtle", AdaptiveTextStyle.SubtitleSubtle); + AssertAdaptiveTextStyle("title", AdaptiveTextStyle.Title); + AssertAdaptiveTextStyle("titleSubtle", AdaptiveTextStyle.TitleSubtle); + AssertAdaptiveTextStyle("titleNumeral", AdaptiveTextStyle.TitleNumeral); + AssertAdaptiveTextStyle("subheader", AdaptiveTextStyle.Subheader); + AssertAdaptiveTextStyle("subheaderSubtle", AdaptiveTextStyle.SubheaderSubtle); + AssertAdaptiveTextStyle("subheaderNumeral", AdaptiveTextStyle.SubheaderNumeral); + AssertAdaptiveTextStyle("header", AdaptiveTextStyle.Header); + AssertAdaptiveTextStyle("headerSubtle", AdaptiveTextStyle.HeaderSubtle); + AssertAdaptiveTextStyle("headerNumeral", AdaptiveTextStyle.HeaderNumeral); + } + + private static void AssertAdaptiveTextStyle(string expectedPropertyValue, AdaptiveTextStyle style) + { + AssertAdaptiveTextPropertyValue("hint-style", expectedPropertyValue, new AdaptiveText() + { + HintStyle = style + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintAlign_Values() + { + AssertAdaptiveTextAlign("auto", AdaptiveTextAlign.Auto); + AssertAdaptiveTextAlign("left", AdaptiveTextAlign.Left); + AssertAdaptiveTextAlign("center", AdaptiveTextAlign.Center); + AssertAdaptiveTextAlign("right", AdaptiveTextAlign.Right); + } + + private static void AssertAdaptiveTextAlign(string expectedPropertyValue, AdaptiveTextAlign align) + { + AssertAdaptiveTextPropertyValue("hint-align", expectedPropertyValue, new AdaptiveText() + { + HintAlign = align + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMaxLines_MinValue() + { + AssertAdaptiveTextPropertyValue("hint-maxLines", "1", new AdaptiveText() + { + HintMaxLines = 1 + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMaxLines_NormalValue() + { + AssertAdaptiveTextPropertyValue("hint-maxLines", "3", new AdaptiveText() + { + HintMaxLines = 3 + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMaxLines_MaxValue() + { + AssertAdaptiveTextPropertyValue("hint-maxLines", int.MaxValue.ToString(), new AdaptiveText() + { + HintMaxLines = int.MaxValue + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMaxLines_BelowMin() + { + Assert.ThrowsException( + () => + { + new AdaptiveText() { HintMaxLines = 0 }; + }, "ArgumentOutOfRangeExceptions should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMaxLines_AboveMax() + { + Assert.ThrowsException( + () => + { + new AdaptiveText() { HintMaxLines = -54 }; + }, "ArgumentOutOfRangeExceptions should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMinLines_MinValue() + { + AssertAdaptiveTextPropertyValue("hint-minLines", "1", new AdaptiveText() + { + HintMinLines = 1 + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMinLines_NormalValue() + { + AssertAdaptiveTextPropertyValue("hint-minLines", "3", new AdaptiveText() + { + HintMinLines = 3 + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMinLines_MaxValue() + { + AssertAdaptiveTextPropertyValue("hint-minLines", int.MaxValue.ToString(), new AdaptiveText() + { + HintMinLines = int.MaxValue + }); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMinLines_BelowMin() + { + Assert.ThrowsException( + () => + { + new AdaptiveText() { HintMinLines = 0 }; + }, "ArgumentOutOfRangeExceptions should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Text_HintMinLines_AboveMax() + { + Assert.ThrowsException( + () => + { + new AdaptiveText() { HintMinLines = -54 }; + }, "ArgumentOutOfRangeExceptions should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Text_HintWrap_Values() + { + AssertAdaptiveTextPropertyValue("hint-wrap", "false", new AdaptiveText() + { + HintWrap = false + }); + + AssertAdaptiveTextPropertyValue("hint-wrap", "true", new AdaptiveText() + { + HintWrap = true + }); + } + + [TestMethod] + public void Test_Adaptive_Text_DefaultNullValues() + { + AssertAdaptiveChild("", new AdaptiveText() + { + HintAlign = AdaptiveTextAlign.Default, + HintStyle = AdaptiveTextStyle.Default, + HintMaxLines = null, + HintMinLines = null, + HintWrap = null, + Language = null, + Text = null + }); + } + + private static void AssertAdaptiveTextPropertyValue(string expectedPropertyName, string expectedPropertyValue, AdaptiveText text) + { + AssertAdaptiveChild($"", text); + } + + [TestMethod] + public void Test_Adaptive_Image_Defaults() + { + Assert.ThrowsException( + () => + { + AssertAdaptiveChild("exception should be thrown", new AdaptiveImage()); + }, "NullReferenceException should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Image_Source() + { + AssertAdaptiveImagePropertyValue("src", "ms-appdata:///local/MyImage.png", new AdaptiveImage() + { + Source = "ms-appdata:///local/MyImage.png" + }); + + AssertAdaptiveImagePropertyValue("src", "ms-appx:///Assets/MyImage.png", new AdaptiveImage() + { + Source = "ms-appx:///Assets/MyImage.png" + }); + + AssertAdaptiveImagePropertyValue("src", "http://msn.com/img.png", new AdaptiveImage() + { + Source = "http://msn.com/img.png" + }); + + AssertAdaptiveImagePropertyValue("src", "Assets/MyImage.png", new AdaptiveImage() + { + Source = "Assets/MyImage.png" + }); + } + + [TestMethod] + public void Test_Adaptive_Image_Source_Null() + { + Assert.ThrowsException( + () => + { + new AdaptiveImage() + { + Source = null + }; + }, "ArgumentNullException should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Image_AddImageQuery() + { + AssertAdaptiveImagePropertyValue("addImageQuery", "false", new AdaptiveImage() + { + AddImageQuery = false + }); + + AssertAdaptiveImagePropertyValue("addImageQuery", "true", new AdaptiveImage() + { + AddImageQuery = true + }); + } + + [TestMethod] + public void Test_Adaptive_Image_AlternateText() + { + AssertAdaptiveImagePropertyValue("alt", "image of puppies", new AdaptiveImage() + { + AlternateText = "image of puppies" + }); + } + + [TestMethod] + public void Test_Adaptive_Image_HintAlign() + { + AssertAdaptiveImageAlign("stretch", AdaptiveImageAlign.Stretch); + AssertAdaptiveImageAlign("left", AdaptiveImageAlign.Left); + AssertAdaptiveImageAlign("center", AdaptiveImageAlign.Center); + AssertAdaptiveImageAlign("right", AdaptiveImageAlign.Right); + } + + private static void AssertAdaptiveImageAlign(string expectedValue, AdaptiveImageAlign align) + { + AssertAdaptiveImagePropertyValue("hint-align", expectedValue, new AdaptiveImage() + { + HintAlign = align + }); + } + + [TestMethod] + public void Test_Adaptive_Image_HintCrop() + { + AssertAdaptiveImageCrop("none", AdaptiveImageCrop.None); + AssertAdaptiveImageCrop("circle", AdaptiveImageCrop.Circle); + } + + private static void AssertAdaptiveImageCrop(string expectedValue, AdaptiveImageCrop crop) + { + AssertAdaptiveImagePropertyValue("hint-crop", expectedValue, new AdaptiveImage() + { + HintCrop = crop + }); + } + + [TestMethod] + public void Test_Adaptive_Image_HintRemoveMargin() + { + AssertAdaptiveImagePropertyValue("hint-removeMargin", "false", new AdaptiveImage() + { + HintRemoveMargin = false + }); + + AssertAdaptiveImagePropertyValue("hint-removeMargin", "true", new AdaptiveImage() + { + HintRemoveMargin = true + }); + } + + [TestMethod] + public void Test_Adaptive_Image_DefaultNullValues() + { + AssertAdaptiveChild("", new AdaptiveImage() + { + Source = "img.png", + AddImageQuery = null, + AlternateText = null, + HintAlign = AdaptiveImageAlign.Default, + HintCrop = AdaptiveImageCrop.Default, + HintRemoveMargin = null + }); + } + + private static void AssertAdaptiveImagePropertyValue(string expectedPropertyName, string expectedPropertyValue, AdaptiveImage image) + { + bool addedSource = false; + if (image.Source == null) + { + image.Source = "img.png"; + addedSource = true; + } + + string xml = $"( + () => + { + AssertAdaptiveChild("exception should be thrown since groups need at least one subgroup child", new AdaptiveGroup()); + }, "InvalidOperationException should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Group_OneChild() + { + AssertAdaptiveChild("", new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + } + }); + } + + [TestMethod] + public void Test_Adaptive_Group_TwoChildren() + { + AssertAdaptiveChild("", new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup(), + new AdaptiveSubgroup() + } + }); + } + + [TestMethod] + public void Test_Adaptive_Group_ThreeChildren() + { + AssertAdaptiveChild("", new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup(), + new AdaptiveSubgroup(), + new AdaptiveSubgroup() + } + }); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_Defaults() + { + AssertAdaptiveSubgroup("", new AdaptiveSubgroup()); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_HintWeight_MinValue() + { + AssertAdaptiveSubgroupProperty("hint-weight", "1", new AdaptiveSubgroup() + { + HintWeight = 1 + }); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_HintWeight_NormalValue() + { + AssertAdaptiveSubgroupProperty("hint-weight", "20", new AdaptiveSubgroup() + { + HintWeight = 20 + }); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_HintWeight_MaxValue() + { + AssertAdaptiveSubgroupProperty("hint-weight", int.MaxValue.ToString(), new AdaptiveSubgroup() + { + HintWeight = int.MaxValue + }); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_HintWeight_JustBelowMin() + { + Assert.ThrowsException( + () => + { + AssertAdaptiveSubgroup("exception should be thrown", new AdaptiveSubgroup() + { + HintWeight = 0 + }); + }, "ArgumentOutOfRangeException should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_HintWeight_BelowMin() + { + Assert.ThrowsException( + () => + { + AssertAdaptiveSubgroup("exception should be thrown", new AdaptiveSubgroup() + { + HintWeight = -53 + }); + }, "ArgumentOutOfRangeException should have been thrown."); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_HintTextStacking() + { + AssertAdaptiveSubgroupTextStacking("top", AdaptiveSubgroupTextStacking.Top); + AssertAdaptiveSubgroupTextStacking("center", AdaptiveSubgroupTextStacking.Center); + AssertAdaptiveSubgroupTextStacking("bottom", AdaptiveSubgroupTextStacking.Bottom); + } + + private static void AssertAdaptiveSubgroupTextStacking(string expectedValue, AdaptiveSubgroupTextStacking textStacking) + { + AssertAdaptiveSubgroupProperty("hint-textStacking", expectedValue, new AdaptiveSubgroup() + { + HintTextStacking = textStacking + }); + } + + [TestMethod] + public void Test_Adaptive_Subgroup_DefaultNullValues() + { + AssertAdaptiveSubgroup("", new AdaptiveSubgroup() + { + HintTextStacking = AdaptiveSubgroupTextStacking.Default, + HintWeight = null + }); + } + + private static void AssertAdaptiveSubgroupProperty(string expectedPropertyName, string expectedPropertyValue, AdaptiveSubgroup subgroup) + { + AssertAdaptiveSubgroup($"", subgroup); + } + + private static void AssertAdaptiveSubgroup(string expectedSubgroupXml, AdaptiveSubgroup subgroup) + { + AdaptiveGroup group = new AdaptiveGroup() + { + Children = + { + subgroup + } + }; + + AssertAdaptiveChild("" + expectedSubgroupXml + "", group); + } + + private static void AssertAdaptiveChild(string expectedAdaptiveChildXml, IAdaptiveChild child) + { + AssertAdaptiveChildInToast(expectedAdaptiveChildXml, child); + AssertAdaptiveChildInTile(expectedAdaptiveChildXml, child); + + // Also assert them within group/subgroup if possible! + if (child is IAdaptiveSubgroupChild) + { + AdaptiveGroup group = new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + Children = + { + child as IAdaptiveSubgroupChild + } + } + } + }; + + string expectedGroupXml = "" + expectedAdaptiveChildXml + ""; + + AssertAdaptiveChildInToast(expectedGroupXml, group); + AssertAdaptiveChildInTile(expectedGroupXml, group); + } + } + + private static void AssertAdaptiveChildInToast(string expectedAdaptiveChildXml, IAdaptiveChild child) + { + var binding = new ToastBindingGeneric(); + + // If the child isn't text, we need to add a text element so notification is valid + if (!(child is AdaptiveText)) + { + binding.Children.Add(new AdaptiveText() + { + Text = "Required text element" + }); + + expectedAdaptiveChildXml = "Required text element" + expectedAdaptiveChildXml; + } + + binding.Children.Add((IToastBindingGenericChild)child); + + var content = new ToastContent() + { + Visual = new ToastVisual() + { + BindingGeneric = binding + } + }; + + string expectedFinalXml = "" + expectedAdaptiveChildXml + ""; + + AssertHelper.AssertToast(expectedFinalXml, content); + } + + private static void AssertAdaptiveChildInTile(string expectedAdaptiveChildXml, IAdaptiveChild child) + { + var content = new TileContent() + { + Visual = new TileVisual() + { + TileMedium = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + (ITileBindingContentAdaptiveChild)child + } + } + } + } + }; + + string expectedFinalXml = "" + expectedAdaptiveChildXml + ""; + + AssertHelper.AssertTile(expectedFinalXml, content); + } + } +} diff --git a/components/Notifications/tests/Test_Badge_Xml.cs b/components/Notifications/tests/Test_Badge_Xml.cs new file mode 100644 index 000000000..0d58113ea --- /dev/null +++ b/components/Notifications/tests/Test_Badge_Xml.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class Test_Badge_Xml + { + [TestMethod] + public void Test_Badge_Xml_Numeric_0() + { + AssertBadgeValue("0", new BadgeNumericContent(0)); + } + + [TestMethod] + public void Test_Badge_Xml_Numeric_1() + { + AssertBadgeValue("1", new BadgeNumericContent(1)); + } + + [TestMethod] + public void Test_Badge_Xml_Numeric_2() + { + AssertBadgeValue("2", new BadgeNumericContent(2)); + } + + [TestMethod] + public void Test_Badge_Xml_Numeric_546() + { + AssertBadgeValue("546", new BadgeNumericContent(546)); + } + + [TestMethod] + public void Test_Badge_Xml_Numeric_Max() + { + AssertBadgeValue(uint.MaxValue.ToString(), new BadgeNumericContent(uint.MaxValue)); + } + + [TestMethod] + public void Test_Badge_Xml_Glyph_None() + { + AssertBadgeValue("none", new BadgeGlyphContent(BadgeGlyphValue.None)); + } + + [TestMethod] + public void Test_Badge_Xml_Glyph_Alert() + { + AssertBadgeValue("alert", new BadgeGlyphContent(BadgeGlyphValue.Alert)); + } + + [TestMethod] + public void Test_Badge_Xml_Glyph_Error() + { + AssertBadgeValue("error", new BadgeGlyphContent(BadgeGlyphValue.Error)); + } + + private static void AssertBadgeValue(string expectedValue, INotificationContent notificationContent) + { + AssertPayload("", notificationContent); + } + + private static void AssertPayload(string expectedXml, INotificationContent notificationContent) + { + AssertHelper.AssertXml(expectedXml, notificationContent.GetContent()); + +#if WINDOWS_UWP + // For WinRT, we'll test the XmlDocument method too, make sure it works. + AssertHelper.AssertXml(expectedXml, notificationContent.GetXml().GetXml()); +#endif + } + } +} diff --git a/components/Notifications/tests/Test_Tile_Xml.cs b/components/Notifications/tests/Test_Tile_Xml.cs new file mode 100644 index 000000000..745248c16 --- /dev/null +++ b/components/Notifications/tests/Test_Tile_Xml.cs @@ -0,0 +1,1676 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class Test_Tile_Xml + { + [TestMethod] + public void Test_Tile_Xml_Tile_Default() + { + TileContent tile = new TileContent(); + + AssertPayload("", tile); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Default() + { + // Assert the defaults + AssertVisual("", new TileVisual()); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_AddImageQuery_False() + { + AssertVisual( + "", + new TileVisual() + { + AddImageQuery = false + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_AddImageQuery_True() + { + AssertVisual( + "", + new TileVisual() + { + AddImageQuery = true + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_BaseUri_Null() + { + AssertVisual( + "", + new TileVisual() + { + BaseUri = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_BaseUri_Value() + { + AssertVisual( + "", + new TileVisual() + { + BaseUri = new Uri("http://msn.com") + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Branding_Auto() + { + AssertVisual( + "", + new TileVisual() + { + Branding = TileBranding.Auto + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Branding_Name() + { + AssertVisual( + "", + new TileVisual() + { + Branding = TileBranding.Name + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Branding_Logo() + { + AssertVisual( + "", + new TileVisual() + { + Branding = TileBranding.Logo + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Branding_NameAndLogo() + { + AssertVisual( + "", + new TileVisual() + { + Branding = TileBranding.NameAndLogo + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Branding_None() + { + AssertVisual( + "", + new TileVisual() + { + Branding = TileBranding.None + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_ContentId_Null() + { + AssertVisual( + "", + new TileVisual() + { + ContentId = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_ContentId_Value() + { + AssertVisual( + "", + new TileVisual() + { + ContentId = "tsla" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_DisplayName_Null() + { + AssertVisual( + "", + new TileVisual() + { + DisplayName = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_DisplayName_Value() + { + AssertVisual( + "", + new TileVisual() + { + DisplayName = "My name" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Language_Null() + { + AssertVisual( + "", + new TileVisual() + { + Language = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Language_Value() + { + AssertVisual( + "", + new TileVisual() + { + Language = "en-US" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Arguments_Null() + { + AssertVisual( + "", + new TileVisual() + { + Arguments = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Arguments_EmptyString() + { + AssertVisual( + "", + new TileVisual() + { + Arguments = string.Empty + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_Arguments_Value() + { + AssertVisual( + "", + new TileVisual() + { + Arguments = "action=viewStory&story=53" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus1_NoMatchingText() + { + AssertVisual( + "AwesomeCool", + new TileVisual() + { + LockDetailedStatus1 = "Status 1", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveText() { Text = "Cool" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus1_MatchingText_InBinding() + { + AssertVisual( + "AwesomeCoolStatus 1Blah", + new TileVisual() + { + LockDetailedStatus1 = "Status 1", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveText() { Text = "Cool" }, + new AdaptiveText() { Text = "Status 1" }, + new AdaptiveText() { Text = "Blah" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus1_MatchingText_InSubgroup() + { + // The lockscreen only looks at ID's in the immediate binding children. So anything in the groups/subgroups are + // ignored. Thus, if text matches there, it still has to be placed as a hint. + AssertVisual( + "AwesomeStatus 1CoolBlah", + new TileVisual() + { + LockDetailedStatus1 = "Status 1", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveImage() + { + Source = "Fable.jpg" + }, + new AdaptiveText() { Text = "Status 1" }, + new AdaptiveText() { Text = "Cool" } + } + } + } + }, + new AdaptiveText() { Text = "Blah" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus2_NoMatchingText() + { + AssertVisual( + "AwesomeCool", + new TileVisual() + { + LockDetailedStatus2 = "Status 2", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveText() { Text = "Cool" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus2_MatchingText_InBinding() + { + AssertVisual( + "AwesomeCoolStatus 2Blah", + new TileVisual() + { + LockDetailedStatus2 = "Status 2", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveText() { Text = "Cool" }, + new AdaptiveText() { Text = "Status 2" }, + new AdaptiveText() { Text = "Blah" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus2_MatchingText_InSubgroup() + { + AssertVisual( + "AwesomeStatus 2CoolBlah", + new TileVisual() + { + LockDetailedStatus2 = "Status 2", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveImage() + { + Source = "Fable.jpg" + }, + new AdaptiveText() { Text = "Status 2" }, + new AdaptiveText() { Text = "Cool" } + } + } + } + }, + new AdaptiveText() { Text = "Blah" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus3_NoMatchingText() + { + AssertVisual( + "AwesomeCool", + new TileVisual() + { + LockDetailedStatus3 = "Status 3", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveText() { Text = "Cool" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus3_MatchingText_InBinding() + { + AssertVisual( + "AwesomeCoolStatus 3Blah", + new TileVisual() + { + LockDetailedStatus3 = "Status 3", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveText() { Text = "Cool" }, + new AdaptiveText() { Text = "Status 3" }, + new AdaptiveText() { Text = "Blah" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Visual_LockDetailedStatus3_MatchingText_InSubgroup() + { + AssertVisual( + "AwesomeStatus 3CoolBlah", + new TileVisual() + { + LockDetailedStatus3 = "Status 3", + + TileWide = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + Children = + { + new AdaptiveText() { Text = "Awesome" }, + new AdaptiveGroup() + { + Children = + { + new AdaptiveSubgroup() + { + Children = + { + new AdaptiveImage() + { + Source = "Fable.jpg" + }, + new AdaptiveText() { Text = "Status 3" }, + new AdaptiveText() { Text = "Cool" } + } + } + } + }, + new AdaptiveText() { Text = "Blah" } + } + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Default() + { + AssertBindingMedium("", new TileBinding()); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_AddImageQuery_False() + { + AssertBindingMedium( + "", + new TileBinding() + { + AddImageQuery = false + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_AddImageQuery_True() + { + AssertBindingMedium( + "", + new TileBinding() + { + AddImageQuery = true + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_BaseUri_Null() + { + AssertBindingMedium( + "", + new TileBinding() + { + BaseUri = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_BaseUri_Value() + { + AssertBindingMedium( + "", + new TileBinding() + { + BaseUri = new Uri("http://msn.com") + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Branding_Auto() + { + AssertBindingMedium( + "", + new TileBinding() + { + Branding = TileBranding.Auto + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Branding_None() + { + AssertBindingMedium( + "", + new TileBinding() + { + Branding = TileBranding.None + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Branding_Name() + { + AssertBindingMedium( + "", + new TileBinding() + { + Branding = TileBranding.Name + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Branding_Logo() + { + AssertBindingMedium( + "", + new TileBinding() + { + Branding = TileBranding.Logo + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Branding_NameAndLogo() + { + AssertBindingMedium( + "", + new TileBinding() + { + Branding = TileBranding.NameAndLogo + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_ContentId_Null() + { + AssertBindingMedium( + "", + new TileBinding() + { + ContentId = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_ContentId_Value() + { + AssertBindingMedium( + "", + new TileBinding() + { + ContentId = "myId" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_DisplayName_Null() + { + AssertBindingMedium( + "", + new TileBinding() + { + DisplayName = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_DisplayName_Value() + { + AssertBindingMedium( + "", + new TileBinding() + { + DisplayName = "My name" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Language_Null() + { + AssertBindingMedium( + "", + new TileBinding() + { + Language = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Language_Value() + { + AssertBindingMedium( + "", + new TileBinding() + { + Language = "en-US" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Arguments_Null() + { + AssertBindingMedium( + "", + new TileBinding() + { + Arguments = null + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Arguments_EmptyString() + { + AssertBindingMedium( + "", + new TileBinding() + { + Arguments = string.Empty + }); + } + + [TestMethod] + public void Test_Tile_Xml_Binding_Arguments_Value() + { + AssertBindingMedium( + "", + new TileBinding() + { + Arguments = "action=viewStory&storyId=52" + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Defaults() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_BackgroundImage_Value() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com/image.png" + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Overlay_Default() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + HintOverlay = 20, + Source = "Fable.jpg" + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Overlay_Min() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + HintOverlay = 0, + Source = "Fable.jpg" + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Overlay_Max() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + HintOverlay = 100, + Source = "Fable.jpg" + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Overlay_AboveDefault() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + HintOverlay = 40, + Source = "Fable.jpg" + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Overlay_BelowDefault() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + HintOverlay = 10, + Source = "Fable.jpg" + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_BackgroundImage_Overlay_BelowMin() + { + try + { + new TileBackgroundImage() + { + HintOverlay = -1, + Source = "Fable.jpg" + }; + } + catch + { + return; + } + + Assert.Fail("Exception should have been thrown."); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_Overlay_AboveMax() + { + try + { + new TileBackgroundImage() + { + HintOverlay = 101, + Source = "Fable.jpg" + }; + } + catch + { + return; + } + + Assert.Fail("Exception should have been thrown."); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_PeekImage_Value() + { + AssertBindingMedium( + "alt", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + AlternateText = "alt", + AddImageQuery = true + } + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_TextStacking_Top() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + TextStacking = TileTextStacking.Top + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_TextStacking_Center() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + TextStacking = TileTextStacking.Center + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_Root_TextStacking_Bottom() + { + AssertBindingMedium( + "", + new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + TextStacking = TileTextStacking.Bottom + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Defaults() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com" + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Source() + { + AssertBindingMediumAdaptive( + "MSN Image", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com", + AddImageQuery = true, + AlternateText = "MSN Image" + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Crop_None() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com", + HintCrop = TileBackgroundImageCrop.None + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Crop_Circle() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com", + HintCrop = TileBackgroundImageCrop.Circle + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Overlay_0() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com", + HintOverlay = 0 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Overlay_20() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com", + HintOverlay = 20 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_Overlay_80() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "http://msn.com", + HintOverlay = 80 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundImage_NoImageSource() + { + try + { + TileContent c = new TileContent() + { + Visual = new TileVisual() + { + TileMedium = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + // No source, which should throw exception + } + } + } + } + }; + + c.GetContent(); + } + catch (NullReferenceException) + { + return; + } + + Assert.Fail("Exception should have been thrown"); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Defaults() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com" + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Source() + { + AssertBindingMediumAdaptive( + "MSN Image", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + AddImageQuery = true, + AlternateText = "MSN Image" + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Crop_None() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + HintCrop = TilePeekImageCrop.None + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Crop_Circle() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + HintCrop = TilePeekImageCrop.Circle + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Overlay_0() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + HintOverlay = 0 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Overlay_20() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + HintOverlay = 20 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_Overlay_80() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + Source = "http://msn.com", + HintOverlay = 80 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_PeekImage_NoImageSource() + { + try + { + TileContent c = new TileContent() + { + Visual = new TileVisual() + { + TileMedium = new TileBinding() + { + Content = new TileBindingContentAdaptive() + { + PeekImage = new TilePeekImage() + { + // No source, which should throw exception when content retrieved + } + } + } + } + }; + + c.GetContent(); + } + catch (NullReferenceException) + { + return; + } + + Assert.Fail("Exception should have been thrown"); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Defaults() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "Background.jpg" + }, + + PeekImage = new TilePeekImage() + { + Source = "Peek.jpg" + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_0and0() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "Background.jpg", + HintOverlay = 0 + }, + + PeekImage = new TilePeekImage() + { + Source = "Peek.jpg", + HintOverlay = 0 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_20and20() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "Background.jpg", + HintOverlay = 20 + }, + + PeekImage = new TilePeekImage() + { + Source = "Peek.jpg", + HintOverlay = 20 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_20and30() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "Background.jpg", + HintOverlay = 20 + }, + + PeekImage = new TilePeekImage() + { + Source = "Peek.jpg", + HintOverlay = 30 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_30and20() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "Background.jpg", + HintOverlay = 30 + }, + + PeekImage = new TilePeekImage() + { + Source = "Peek.jpg", + HintOverlay = 20 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_0and20() + { + AssertBindingMediumAdaptive( + "", + new TileBindingContentAdaptive() + { + BackgroundImage = new TileBackgroundImage() + { + Source = "Background.jpg", + HintOverlay = 0 + }, + + PeekImage = new TilePeekImage() + { + Source = "Peek.jpg", + HintOverlay = 20 + } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Photos_Default() + { + TileBindingContentPhotos content = new TileBindingContentPhotos() + { + }; + + AssertBindingMedium("", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Photos_OneImage() + { + TileBindingContentPhotos content = new TileBindingContentPhotos() + { + Images = + { + new TileBasicImage() + { + Source = "http://msn.com/1.jpg", + AddImageQuery = true, + AlternateText = "alternate" + } + } + }; + + AssertBindingMedium("alternate", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Photos_TwoImages() + { + TileBindingContentPhotos content = new TileBindingContentPhotos() + { + Images = + { + new TileBasicImage() + { + Source = "Assets/1.jpg" + }, + new TileBasicImage() + { + Source = "Assets/2.jpg" + } + } + }; + + AssertBindingMedium("", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Photos_MaxImages() + { + TileBindingContentPhotos content = new TileBindingContentPhotos() + { + Images = + { + new TileBasicImage() { Source = "1.jpg" }, + new TileBasicImage() { Source = "2.jpg" }, + new TileBasicImage() { Source = "3.jpg" }, + new TileBasicImage() { Source = "4.jpg" }, + new TileBasicImage() { Source = "5.jpg" }, + new TileBasicImage() { Source = "6.jpg" }, + new TileBasicImage() { Source = "7.jpg" }, + new TileBasicImage() { Source = "8.jpg" }, + new TileBasicImage() { Source = "9.jpg" }, + new TileBasicImage() { Source = "10.jpg" }, + new TileBasicImage() { Source = "11.jpg" }, + new TileBasicImage() { Source = "12.jpg" } + } + }; + + AssertBindingMedium( + @" + + + + + + + + + + + + + ", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Photos_TooManyImages() + { + try + { + new TileBindingContentPhotos() + { + Images = + { + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage(), + new TileBasicImage() + } + }; + } + catch + { + return; + } + + Assert.Fail("Exception should have been thrown, adding more than 12 images isn't supported."); + } + + [TestMethod] + public void Test_Tile_Xml_Special_People_Defaults() + { + TileBindingContentPeople content = new TileBindingContentPeople(); + + AssertBindingMedium("", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_People_OneImage() + { + TileBindingContentPeople content = new TileBindingContentPeople() + { + Images = + { + new TileBasicImage() + { + Source = "http://msn.com/1.jpg", + AddImageQuery = true, + AlternateText = "alternate" + } + } + }; + + AssertBindingMedium("alternate", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_People_TwoImages() + { + TileBindingContentPeople content = new TileBindingContentPeople() + { + Images = + { + new TileBasicImage() { Source = "Assets/1.jpg" }, + new TileBasicImage() { Source = "Assets/2.jpg" } + } + }; + + AssertBindingMedium("", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_People_ManyImages() + { + string payload = ""; + + TileBindingContentPeople content = new TileBindingContentPeople(); + + // Add 30 images + for (int i = 1; i <= 30; i++) + { + string src = i + ".jpg"; + + content.Images.Add(new TileBasicImage() { Source = src }); + payload += $""; + } + + payload += ""; + + AssertBindingMedium(payload, new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Contact_Defaults() + { + TileBindingContentContact content = new TileBindingContentContact(); + + AssertBindingMedium("", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Contact_Text() + { + TileBindingContentContact content = new TileBindingContentContact() + { + Text = new TileBasicText() + { + Text = "Hello world", + Lang = "en-US" + } + }; + + AssertBindingMedium("Hello world", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Contact_Image() + { + TileBindingContentContact content = new TileBindingContentContact() + { + Image = new TileBasicImage() + { + Source = "http://msn.com/img.jpg", + AddImageQuery = true, + AlternateText = "John Smith" + } + }; + + AssertBindingMedium("John Smith", new TileBinding() + { + Content = content + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Contact_Both_Small() + { + TileBindingContentContact content = new TileBindingContentContact() + { + Text = new TileBasicText() + { + Text = "Hello world" + }, + + Image = new TileBasicImage() { Source = "Assets/img.jpg" } + }; + + // Small doesn't support the text, so it doesn't output the text element when rendered for small + AssertVisual("", new TileVisual() + { + TileSmall = new TileBinding() { Content = content } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Contact_Both_Medium() + { + TileBindingContentContact content = new TileBindingContentContact() + { + Text = new TileBasicText() + { + Text = "Hello world" + }, + + Image = new TileBasicImage() { Source = "Assets/img.jpg" } + }; + + // Text is written before the image element + AssertVisual("Hello world", new TileVisual() + { + TileMedium = new TileBinding() { Content = content } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Iconic_Small() + { + AssertVisual("", new TileVisual() + { + TileSmall = new TileBinding() { Content = new TileBindingContentIconic() } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Iconic_Medium() + { + AssertVisual("", new TileVisual() + { + TileMedium = new TileBinding() { Content = new TileBindingContentIconic() } + }); + } + + [TestMethod] + public void Test_Tile_Xml_Special_Iconic_Image() + { + AssertVisual("iconic", new TileVisual() + { + TileMedium = new TileBinding() + { + Content = new TileBindingContentIconic() + { + Icon = new TileBasicImage() + { + Source = "Assets/Iconic.png", + AlternateText = "iconic" + } + } + } + }); + } + + private static void AssertBindingMediumAdaptive(string expectedBindingXml, TileBindingContentAdaptive content) + { + AssertBindingMedium(expectedBindingXml, new TileBinding() { Content = content }); + } + + private static void AssertBindingMedium(string expectedBindingXml, TileBinding binding) + { + AssertVisual("" + expectedBindingXml + "", new TileVisual() + { + TileMedium = binding + }); + } + + private static void AssertVisual(string expectedVisualXml, TileVisual visual) + { + AssertPayload("" + expectedVisualXml + "", new TileContent() + { + Visual = visual + }); + } + + private static void AssertPayload(string expectedXml, TileContent tile) + { + AssertHelper.AssertTile(expectedXml, tile); + } + } +} diff --git a/components/Notifications/tests/Test_Toast_Xml.cs b/components/Notifications/tests/Test_Toast_Xml.cs new file mode 100644 index 000000000..96df801a3 --- /dev/null +++ b/components/Notifications/tests/Test_Toast_Xml.cs @@ -0,0 +1,2212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#nullable disable +namespace NotificationsExperiment.Tests +{ + [TestClass] + public class Test_Toast_Xml + { + [TestMethod] + public void Test_Toast_XML_Toast_Defaults() + { + AssertPayload("", new ToastContent()); + } + + [TestMethod] + public void Test_Toast_XML_Toast_Launch_Value() + { + var toast = new ToastContent() + { + Launch = "tacos" + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_ActivationType_Foreground() + { + var toast = new ToastContent() + { + ActivationType = ToastActivationType.Foreground + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_ActivationType_Background() + { + var toast = new ToastContent() + { + ActivationType = ToastActivationType.Background + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_ActivationType_Protocol() + { + var toast = new ToastContent() + { + ActivationType = ToastActivationType.Protocol + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_Scenario_Default() + { + var toast = new ToastContent() + { + Scenario = ToastScenario.Default + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_Scenarios() + { + AssertToastScenario(ToastScenario.Reminder, "reminder"); + AssertToastScenario(ToastScenario.Alarm, "alarm"); + AssertToastScenario(ToastScenario.IncomingCall, "incomingCall"); + } + + private void AssertToastScenario(ToastScenario scenario, string scenarioText) + { + var toast = new ToastContent() + { + Scenario = scenario + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_Duration_Short() + { + var toast = new ToastContent() + { + Duration = ToastDuration.Short + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_Duration_Long() + { + var toast = new ToastContent() + { + Duration = ToastDuration.Long + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_HintToastId() + { + var toast = new ToastContent() + { + HintToastId = "AppointmentReminder" + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_HintPeople_RemoteId() + { + var toast = new ToastContent() + { + HintPeople = new ToastPeople() + { + RemoteId = "1234" + } + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_HintPeople_EmailAddress() + { + var toast = new ToastContent() + { + HintPeople = new ToastPeople() + { + EmailAddress = "johndoe@mydomain.com" + } + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_HintPeople_PhoneNumber() + { + var toast = new ToastContent() + { + HintPeople = new ToastPeople() + { + PhoneNumber = "888-888-8888" + } + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_HintPeople_Precedence() + { + // Email should take precedence over phone number + var toast = new ToastContent() + { + HintPeople = new ToastPeople() + { + EmailAddress = "johndoe@mydomain.com", + PhoneNumber = "888-888-8888" + } + }; + + AssertPayload("", toast); + + // RemoteId should take precedence over phone number + toast.HintPeople = new ToastPeople() + { + RemoteId = "1234", + PhoneNumber = "888-888-8888" + }; + + AssertPayload("", toast); + + // RemoteId should take precedence over all + toast.HintPeople = new ToastPeople() + { + RemoteId = "1234", + PhoneNumber = "888-888-8888", + EmailAddress = "johndoe@mydomain.com" + }; + + AssertPayload("", toast); + } + + [TestMethod] + public void Test_Toast_XML_Toast_AdditionalProperties() + { + AssertPayload("", new ToastContent() + { + AdditionalProperties = + { + { "hint-tacos", "yummy://kind=beans,c=0" } + } + }); + + // Multiple + AssertPayload("", new ToastContent() + { + AdditionalProperties = + { + { "burrito", "true" }, + { "avacado", "definitely" } + } + }); + + // XML encoding + AssertPayload("", new ToastContent() + { + AdditionalProperties = + { + { "request", "eggs & beans" } + } + }); + } + + [TestMethod] + public void Test_ToastV2_Visual_Defaults() + { + AssertPayload("", new ToastContent()); + } + + [TestMethod] + public void Test_ToastV2_Visual_AddImageQuery_False() + { + var visual = new ToastVisual() + { + AddImageQuery = false + }; + + AssertVisualPayloadProperties(@"addImageQuery='false'", visual); + } + + [TestMethod] + public void Test_ToastV2_Visual_AddImageQuery_True() + { + var visual = new ToastVisual() + { + AddImageQuery = true + }; + + AssertVisualPayloadProperties(@"addImageQuery=""true""", visual); + } + + [TestMethod] + public void Test_ToastV2_Visual_BaseUri_Value() + { + var visual = new ToastVisual() + { + BaseUri = new Uri("http://msn.com") + }; + + AssertVisualPayloadProperties(@"baseUri=""http://msn.com/""", visual); + } + + [TestMethod] + public void Test_ToastV2_Visual_Language_Value() + { + var visual = new ToastVisual() + { + Language = "en-US" + }; + + AssertVisualPayloadProperties(@"lang=""en-US""", visual); + } + + [TestMethod] + public void Test_ToastV2_Visual_AdaptiveText_Defaults() + { + AssertAdaptiveText(@"", new AdaptiveText()); + } + + [TestMethod] + public void Test_ToastV2_Visual_AdaptiveText_All() + { + AssertAdaptiveText(@"Hi, I am a title", new AdaptiveText() + { + Text = "Hi, I am a title", + Language = "en-US", + HintAlign = AdaptiveTextAlign.Right, + HintMaxLines = 3, + HintMinLines = 2, + HintStyle = AdaptiveTextStyle.Header, + HintWrap = true + }); + } + + [TestMethod] + public void Test_ToastV2_Xml_Attribution() + { + var visual = new ToastVisual() + { + BindingGeneric = new ToastBindingGeneric() + { + Children = + { + new AdaptiveText() + { + Text = "My title" + }, + + new AdaptiveText() + { + Text = "My body 1" + } + }, + + Attribution = new ToastGenericAttributionText() + { + Text = "cnn.com" + } + } + }; + + AssertVisualPayload(@"My titleMy body 1cnn.com", visual); + } + + [TestMethod] + public void Test_ToastV2_Xml_Attribution_Lang() + { + var visual = new ToastVisual() + { + BindingGeneric = new ToastBindingGeneric() + { + Children = + { + new AdaptiveText() + { + Text = "My title" + }, + + new AdaptiveText() + { + Text = "My body 1" + } + }, + + Attribution = new ToastGenericAttributionText() + { + Text = "cnn.com", + Language = "en-US" + } + } + }; + + AssertVisualPayload(@"My titleMy body 1cnn.com", visual); + } + + [TestMethod] + public void Test_ToastV2_BindingGeneric_BaseUri() + { + AssertBindingGenericProperty("baseUri", "http://msn.com/images/", new ToastBindingGeneric() + { + BaseUri = new Uri("http://msn.com/images/", UriKind.Absolute) + }); + } + + [TestMethod] + public void Test_ToastV2_BindingGeneric_AddImageQuery() + { + AssertBindingGenericProperty("addImageQuery", "false", new ToastBindingGeneric() + { + AddImageQuery = false + }); + + AssertBindingGenericProperty("addImageQuery", "true", new ToastBindingGeneric() + { + AddImageQuery = true + }); + } + + [TestMethod] + public void Test_ToastV2_BindingGeneric_Language() + { + AssertBindingGenericProperty("lang", "en-US", new ToastBindingGeneric() + { + Language = "en-US" + }); + } + + [TestMethod] + public void Test_ToastV2_BindingGeneric_DefaultNullValues() + { + AssertBindingGenericPayload("", new ToastBindingGeneric() + { + AddImageQuery = null, + AppLogoOverride = null, + BaseUri = null, + Language = null, + HeroImage = null, + Attribution = null + }); + } + + private static void AssertBindingGenericProperty(string expectedPropertyName, string expectedPropertyValue, ToastBindingGeneric binding) + { + AssertBindingGenericPayload($"", binding); + } + + [TestMethod] + public void Test_ToastV2_BindingShoulderTap_BaseUri() + { + AssertBindingShoulderTapProperty("baseUri", "http://msn.com/images/", new ToastBindingShoulderTap() + { + BaseUri = new Uri("http://msn.com/images/", UriKind.Absolute) + }); + } + + [TestMethod] + public void Test_ToastV2_BindingShoulderTap_AddImageQuery() + { + AssertBindingShoulderTapProperty("addImageQuery", "false", new ToastBindingShoulderTap() + { + AddImageQuery = false + }); + + AssertBindingShoulderTapProperty("addImageQuery", "true", new ToastBindingShoulderTap() + { + AddImageQuery = true + }); + } + + [TestMethod] + public void Test_ToastV2_BindingShoulderTap_Language() + { + AssertBindingShoulderTapProperty("lang", "en-US", new ToastBindingShoulderTap() + { + Language = "en-US" + }); + } + + private static void AssertBindingShoulderTapProperty(string expectedPropertyName, string expectedPropertyValue, ToastBindingShoulderTap binding) + { + AssertBindingShoulderTapPayload($"", binding); + } + + [TestMethod] + public void Test_ToastV2_ShoulderTapImage() + { + AssertShoulderTapImagePayload("alt text", new ToastShoulderTapImage() + { + Source = "img.png", + AddImageQuery = true, + AlternateText = "alt text" + }); + + // Defaults shouldn't have anything assigned + AssertShoulderTapImagePayload("", new ToastShoulderTapImage() + { + Source = "img.png" + }); + } + + [TestMethod] + public void Test_ToastV2_ShoulderTapImage_SourceRequired() + { + Assert.ThrowsException(() => + { + AssertShoulderTapImagePayload("exception should be thrown", new ToastShoulderTapImage()); + }); + + Assert.ThrowsException(() => + { + new ToastShoulderTapImage() + { + Source = null + }; + }); + } + + private static void AssertShoulderTapImagePayload(string expectedImageXml, ToastShoulderTapImage image) + { + AssertBindingShoulderTapPayload($"{expectedImageXml}", new ToastBindingShoulderTap() + { + Image = image + }); + } + + [TestMethod] + public void Test_ToastV2_SpriteSheet() + { + AssertSpriteSheetProperties("spritesheet-src='sprite.png' spritesheet-height='80' spritesheet-fps='25' spritesheet-startingFrame='15'", new ToastSpriteSheet() + { + Source = "sprite.png", + FrameHeight = 80, + Fps = 25, + StartingFrame = 15 + }); + + // Defaults shouldn't have anything assigned + AssertSpriteSheetProperties("spritesheet-src='sprite.png'", new ToastSpriteSheet() + { + Source = "sprite.png" + }); + + // Can assign invalid values + AssertSpriteSheetProperties("spritesheet-src='sprite.png' spritesheet-height='0' spritesheet-fps='150' spritesheet-startingFrame='15'", new ToastSpriteSheet() + { + Source = "sprite.png", + FrameHeight = 0, + Fps = 150, + StartingFrame = 15 + }); + } + + [TestMethod] + public void Test_ToastV2_SpriteSheet_SourceRequired() + { + Assert.ThrowsException(() => + { + AssertSpriteSheetProperties("exception should be thrown", new ToastSpriteSheet()); + }); + + Assert.ThrowsException(() => + { + new ToastSpriteSheet() + { + Source = null + }; + }); + } + + private static void AssertSpriteSheetProperties(string expectedProperties, ToastSpriteSheet spriteSheet) + { + AssertShoulderTapImagePayload($"", new ToastShoulderTapImage() + { + Source = "img.png", + SpriteSheet = spriteSheet + }); + } + + private static void AssertBindingShoulderTapPayload(string expectedBindingXml, ToastBindingShoulderTap binding) + { + AssertVisualPayload("" + expectedBindingXml + "", new ToastVisual() + { + BindingGeneric = new ToastBindingGeneric(), + BindingShoulderTap = binding + }); + } + + [TestMethod] + public void Test_ToastV2_AppLogo_Crop_None() + { + var appLogo = new ToastGenericAppLogo() + { + HintCrop = ToastGenericAppLogoCrop.None, + Source = "img.png" + }; + + AssertAppLogoPayload(@"", appLogo); + } + + [TestMethod] + public void Test_ToastV2_AppLogo_Crop_Circle() + { + var appLogo = new ToastGenericAppLogo() + { + HintCrop = ToastGenericAppLogoCrop.Circle, + Source = "img.png" + }; + + AssertAppLogoPayload(@"", appLogo); + } + + [TestMethod] + public void Test_ToastV2_AppLogo_Source_Defaults() + { + var appLogo = new ToastGenericAppLogo() + { + Source = "http://xbox.com/Avatar.jpg" + }; + + AssertAppLogoPayload(@"", appLogo); + } + + [TestMethod] + public void Test_ToastV2_AppLogo_Source_Alt() + { + var appLogo = new ToastGenericAppLogo() + { + Source = "http://xbox.com/Avatar.jpg", + AlternateText = "alternate" + }; + + AssertAppLogoPayload(@"", appLogo); + } + + [TestMethod] + public void Test_ToastV2_AppLogo_Source_AddImageQuery_False() + { + var appLogo = new ToastGenericAppLogo() + { + Source = "http://xbox.com/Avatar.jpg", + AddImageQuery = false + }; + + AssertAppLogoPayload(@"", appLogo); + } + + [TestMethod] + public void Test_ToastV2_AppLogo_Source_AddImageQuery_True() + { + var appLogo = new ToastGenericAppLogo() + { + Source = "http://xbox.com/Avatar.jpg", + AddImageQuery = true + }; + + AssertAppLogoPayload(@"", appLogo); + } + + [TestMethod] + public void Test_ToastV2_Xml_HeroImage_Default() + { + var hero = new ToastGenericHeroImage(); + + try + { + AssertHeroImagePayload("", hero); + } + catch (NullReferenceException) + { + return; + } + + Assert.Fail("Exception should have been thrown since Source wasn't provided."); + } + + [TestMethod] + public void Test_ToastV2_Xml_HeroImage_WithSource() + { + var hero = new ToastGenericHeroImage() + { + Source = "http://food.com/peanuts.jpg" + }; + + AssertHeroImagePayload("", hero); + } + + [TestMethod] + public void Test_ToastV2_Xml_HeroImage_Alt() + { + var hero = new ToastGenericHeroImage() + { + Source = "http://food.com/peanuts.jpg", + AlternateText = "peanuts" + }; + + AssertHeroImagePayload("peanuts", hero); + } + + [TestMethod] + public void Test_ToastV2_Xml_HeroImage_AddImageQuery() + { + var hero = new ToastGenericHeroImage() + { + Source = "http://food.com/peanuts.jpg", + AddImageQuery = true + }; + + AssertHeroImagePayload("", hero); + } + + [TestMethod] + public void Test_ToastV2_Xml_HeroImage_AllProperties() + { + var hero = new ToastGenericHeroImage() + { + Source = "http://food.com/peanuts.jpg", + AddImageQuery = true, + AlternateText = "peanuts" + }; + + AssertHeroImagePayload("peanuts", hero); + } + + private static ToastContent GenerateFromVisual(ToastVisual visual) + { + return new ToastContent() + { + Visual = visual + }; + } + + /// + /// Used for testing properties of visual without needing to specify the Generic binding + /// + private static void AssertVisualPayloadProperties(string expectedVisualProperties, ToastVisual visual) + { + visual.BindingGeneric = new ToastBindingGeneric(); + + AssertVisualPayload("", visual); + } + + private static void AssertBindingGenericPayload(string expectedBindingXml, ToastBindingGeneric binding) + { + AssertVisualPayload("" + expectedBindingXml + "", new ToastVisual() + { + BindingGeneric = binding + }); + } + + private static void AssertAdaptiveText(string expectedAdaptiveTextXml, AdaptiveText text) + { + AssertBindingGenericPayload("" + expectedAdaptiveTextXml + "", new ToastBindingGeneric() + { + Children = + { + text + } + }); + } + + private static void AssertAppLogoPayload(string expectedAppLogoXml, ToastGenericAppLogo appLogo) + { + AssertVisualPayload(@"" + expectedAppLogoXml + "", new ToastVisual() + { + BindingGeneric = new ToastBindingGeneric() + { + AppLogoOverride = appLogo + } + }); + } + + private static void AssertHeroImagePayload(string expectedHeroXml, ToastGenericHeroImage heroImage) + { + AssertVisualPayload(@"" + expectedHeroXml + "", new ToastVisual() + { + BindingGeneric = new ToastBindingGeneric() + { + HeroImage = heroImage + } + }); + } + + [TestMethod] + public void Test_Toast_Xml_Audio_Defaults() + { + var audio = new ToastAudio(); + + AssertAudioPayload("