From 8f5527408b0284f0e5bd71dca700d9c6a27bf7e8 Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Fri, 5 Nov 2021 14:11:22 -0700 Subject: [PATCH 1/4] Added a way to offer the user to rate the app. --- .../DevToys/Api/Core/IMarketingService.cs | 27 ++ src/dev/impl/DevToys/App.xaml.cs | 17 +- src/dev/impl/DevToys/Core/MarketingService.cs | 303 ++++++++++++++++++ .../impl/DevToys/Core/Threading/AsyncLazy.cs | 57 ++++ src/dev/impl/DevToys/DevToys.csproj | 4 + src/dev/impl/DevToys/LanguageManager.cs | 15 + src/dev/impl/DevToys/Models/MarketingState.cs | 34 ++ .../impl/DevToys/Strings/cs-CZ/MainPage.resw | 9 + .../impl/DevToys/Strings/en-US/MainPage.resw | 9 + .../impl/DevToys/Strings/fr-FR/MainPage.resw | 9 + .../impl/DevToys/Strings/ru-RU/MainPage.resw | 9 + .../DevToys/ViewModels/MainPageViewModel.cs | 18 +- .../Base64EncoderDecoderToolViewModel.cs | 12 +- .../GuidGeneratorToolViewModel.cs | 13 +- .../HashGeneratorToolViewModel.cs | 12 +- .../JsonFormatterToolViewModel.cs | 12 +- .../Tools/JsonYaml/JsonYamlToolViewModel.cs | 12 +- .../JwtDecoderEncoderToolViewModel.cs | 15 + .../MarkdownPreviewToolViewModel.cs | 12 +- .../Tools/RegEx/RegExToolViewModel.cs | 12 +- .../StringUtilitiesToolViewModel.cs | 13 +- 21 files changed, 603 insertions(+), 21 deletions(-) create mode 100644 src/dev/impl/DevToys/Api/Core/IMarketingService.cs create mode 100644 src/dev/impl/DevToys/Core/MarketingService.cs create mode 100644 src/dev/impl/DevToys/Core/Threading/AsyncLazy.cs create mode 100644 src/dev/impl/DevToys/Models/MarketingState.cs diff --git a/src/dev/impl/DevToys/Api/Core/IMarketingService.cs b/src/dev/impl/DevToys/Api/Core/IMarketingService.cs new file mode 100644 index 0000000000..9b20578169 --- /dev/null +++ b/src/dev/impl/DevToys/Api/Core/IMarketingService.cs @@ -0,0 +1,27 @@ +#nullable enable + +using System.Threading.Tasks; + +namespace DevToys.Api.Core +{ + /// + /// Provides a service that help to generate positive review of the DevToys app. + /// + /// + /// This service should be called when the app started, crashed, successfuly performed a task. + /// By monitoring these events, the service will try to decide of the most ideal moment + /// for proposing to the user to share constructive feedback to the developer. + /// + public interface IMarketingService + { + Task NotifyAppEncounteredAProblemAsync(); + + void NotifyToolSuccessfullyWorked(); + + void NotifyAppJustUpdated(); + + void NotifyAppStarted(); + + void NotifySmartDetectionWorked(); + } +} diff --git a/src/dev/impl/DevToys/App.xaml.cs b/src/dev/impl/DevToys/App.xaml.cs index 0e6a30705f..2caec4c7ae 100644 --- a/src/dev/impl/DevToys/App.xaml.cs +++ b/src/dev/impl/DevToys/App.xaml.cs @@ -30,8 +30,9 @@ namespace DevToys sealed partial class App : Application, IDisposable { private readonly Task _mefComposer; - private readonly Lazy> _themeListener; - private readonly Lazy> _settingsProvider; + private readonly AsyncLazy _themeListener; + private readonly AsyncLazy _settingsProvider; + private readonly AsyncLazy _marketingService; private bool _isDisposed; @@ -56,8 +57,9 @@ public App() UnhandledException += OnUnhandledException; // Importing it in a Lazy because we can't import it before a Window is created. - _themeListener = new Lazy>(async () => (await _mefComposer).ExportProvider.GetExport()); - _settingsProvider = new Lazy>(async () => (await _mefComposer).ExportProvider.GetExport()); + _themeListener = new AsyncLazy(async () => (await _mefComposer).ExportProvider.GetExport()); + _settingsProvider = new AsyncLazy(async () => (await _mefComposer).ExportProvider.GetExport()); + _marketingService = new AsyncLazy(async () => (await _mefComposer).ExportProvider.GetExport()); InitializeComponent(); Suspending += OnSuspending; @@ -179,9 +181,10 @@ private void OnSuspending(object sender, SuspendingEventArgs e) deferral.Complete(); } - private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e) + private async void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e) { Logger.LogFault("Unhandled problem", e.Exception); + await (await _marketingService.GetValueAsync()).NotifyAppEncounteredAProblemAsync(); } private async Task EnsureWindowIsInitializedAsync() @@ -210,7 +213,7 @@ LanguageDefinition languageDefinition LanguageManager.Instance.SetCurrentCulture(languageDefinition); // Apply the app color theme. - (await _themeListener.Value).ApplyDesiredColorTheme(); + (await _themeListener.GetValueAsync()).ApplyDesiredColorTheme(); // Change the text editor font if the current font isn't available on the system. ValidateDefaultTextEditorFontAsync().Forget(); @@ -240,7 +243,7 @@ private async Task ValidateDefaultTextEditorFontAsync() { await TaskScheduler.Default; - ISettingsProvider settingsProvider = await _settingsProvider.Value; + ISettingsProvider settingsProvider = await _settingsProvider.GetValueAsync(); string currentFont = settingsProvider.GetSetting(PredefinedSettings.TextEditorFont); string[] systemFonts = CanvasTextFormat.GetSystemFontFamilies(); diff --git a/src/dev/impl/DevToys/Core/MarketingService.cs b/src/dev/impl/DevToys/Core/MarketingService.cs new file mode 100644 index 0000000000..6de10a24c5 --- /dev/null +++ b/src/dev/impl/DevToys/Core/MarketingService.cs @@ -0,0 +1,303 @@ +#nullable enable + +using DevToys.Api.Core; +using DevToys.Core.Threading; +using DevToys.Models; +using Newtonsoft.Json; +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Windows.Services.Store; +using Windows.Storage; + +namespace DevToys.Core +{ + [Export(typeof(IMarketingService))] + [Shared] + internal sealed class MarketingService : IMarketingService, IDisposable + { + private const string StoredFileName = "marketingInfo.json"; + + private readonly INotificationService _notificationService; + private readonly DisposableSempahore _semaphore = new DisposableSempahore(); + private readonly object _lock = new object(); + + private bool _rateOfferInProgress; + private AsyncLazy _marketingState; + + [ImportingConstructor] + public MarketingService(INotificationService notificationService) + { + _notificationService = notificationService; + _marketingState = new AsyncLazy(LoadStateAsync); + } + + public void Dispose() + { + _semaphore.Dispose(); + } + + public async Task NotifyAppEncounteredAProblemAsync() + { + await UpdateMarketingStateAsync(state => + { + state.LastProblemEncounteredDate = DateTime.Now; + state.StartSinceLastProblemEncounteredCount = 0; + }); + } + + public void NotifyAppJustUpdated() + { + UpdateMarketingStateAsync(state => + { + state.LastUpdateDate = DateTime.Now; + }).ContinueWith(_ => + { + TryOfferUserToRateApp(); + }); + } + + public void NotifyAppStarted() + { + UpdateMarketingStateAsync(state => + { + state.StartSinceLastProblemEncounteredCount++; + }).ContinueWith(_ => + { + TryOfferUserToRateApp(); + }); + } + + public void NotifySmartDetectionWorked() + { + UpdateMarketingStateAsync(state => + { + state.SmartDetectionCount++; + }).ContinueWith(_ => + { + TryOfferUserToRateApp(); + }); + } + + public void NotifyToolSuccessfullyWorked() + { + UpdateMarketingStateAsync(state => + { + state.ToolSuccessfulyWorkedCount++; + }).ContinueWith(_ => + { + TryOfferUserToRateApp(); + }); + } + + private void TryOfferUserToRateApp() + { + lock (_lock) + { + if (_rateOfferInProgress) + { + return; + } + + if (_marketingState.IsValueCreated + && DetermineWhetherAppRatingShouldBeOffered(_marketingState.GetValueAsync().Result)) + { + _rateOfferInProgress = true; + + _notificationService.ShowInAppNotification( + LanguageManager.Instance.MainPage.NotificationRateAppTitle, + LanguageManager.Instance.MainPage.NotificationRateAppActionableActionText, + () => + { + RateAsync().ForgetSafely(); + }, + LanguageManager.Instance.MainPage.NotificationRateAppMessage); + } + } + } + + private async Task RateAsync() + { + await UpdateMarketingStateAsync(state => + { + state.AppRatingOfferCount++; + state.LastAppRatingOfferDate = DateTime.Now; + }); + + StoreContext storeContext = StoreContext.GetDefault(); + + StoreRateAndReviewResult result = await ThreadHelper.RunOnUIThreadAsync(async () => + { + return await storeContext.RequestRateAndReviewAppAsync(); + }).ConfigureAwait(false); + + if (result.Status == StoreRateAndReviewStatus.Succeeded) + { + await UpdateMarketingStateAsync(state => + { + state.AppGotRated = true; + }); + } + + lock (_lock) + { + _rateOfferInProgress = false; + } + } + + private bool DetermineWhetherAppRatingShouldBeOffered(MarketingState state) + { + // The user already rated the app. Let's not offer him to rate it again. + if (state.AppGotRated) + { + return false; + } + + // We already offered the user to rate the app many times. + // It's very unlikely that he will rate it at this point. Let's stop asking. + if (state.AppRatingOfferCount >= 10) + { + return false; + } + + // If it's been less than 8 days since the last time the app crashed or that the app + // has been installed on the machine. Let's not ask the user to rate the app. + if (DateTime.Now - state.LastProblemEncounteredDate < TimeSpan.FromDays(8)) + { + return false; + } + + // If the app have been started less than 4 times since the last crash or since the app + // got installed on the machine, let's not ask the user to rate the app. + if (state.StartSinceLastProblemEncounteredCount < 4) + { + return false; + } + + // The app got updated 2 days ago. Potentially, we introduced some instability (not necessarily crash, + // but maybe visual issues, inconsistencies...etc). + // Let's make sure we don't offer the user to rate the app as soon as it got updated, just in case + // if the app is completely broken. + if (DateTime.Now - state.LastUpdateDate < TimeSpan.FromDays(2)) + { + return false; + } + + // Let's make sure we don't offer to rate the app more than once within 2 days. + if (state.AppRatingOfferCount > 0 && DateTime.Now - state.LastAppRatingOfferDate < TimeSpan.FromDays(2)) + { + return false; + } + + // If we already offered to rate the app more than 2 times, let's make sure we + // don't offer it again before the next 5 days. + if (state.AppRatingOfferCount > 2 && DateTime.Now - state.LastAppRatingOfferDate < TimeSpan.FromDays(5)) + { + return false; + } + + // If we already offered to rate the app more than 5 times, let's make sure we + // don't offer it again before the next 10 days. + if (state.AppRatingOfferCount > 5 && DateTime.Now - state.LastAppRatingOfferDate < TimeSpan.FromDays(10)) + { + return false; + } + + // If we already offered to rate the app more than 7 times, let's make sure we + // don't offer it again before the next 60 days. + if (state.AppRatingOfferCount > 7 && DateTime.Now - state.LastAppRatingOfferDate < TimeSpan.FromDays(60)) + { + return false; + } + + // Smart Detection has been used at least twice. Let's offer the use to rate the app. + if (state.SmartDetectionCount > 3) + { + return true; + } + + // The user used tools at least 10 times already. Let's offer the use to rate the app. + if (state.ToolSuccessfulyWorkedCount > 10) + { + return true; + } + + return false; + } + + private async Task UpdateMarketingStateAsync(Action updateAction) + { + await TaskScheduler.Default; + + MarketingState state = await _marketingState.GetValueAsync(); + + using (await _semaphore.WaitAsync(CancellationToken.None)) + { + updateAction(state); + } + + await SaveStateAsync(state); + } + + private async Task SaveStateAsync(MarketingState state) + { + await TaskScheduler.Default; + + try + { + using (await _semaphore.WaitAsync(CancellationToken.None)) + { + StorageFolder localCacheFolder = ApplicationData.Current.LocalCacheFolder; + + StorageFile file = await localCacheFolder.CreateFileAsync(StoredFileName, CreationCollisionOption.ReplaceExisting); + + string fileContent + = JsonConvert.SerializeObject( + state, + Formatting.Indented); + + await FileIO.WriteTextAsync(file, fileContent); + } + } + catch (Exception) + { + } + } + + private async Task LoadStateAsync() + { + await TaskScheduler.Default; + + try + { + using (await _semaphore.WaitAsync(CancellationToken.None)) + { + StorageFolder localCacheFolder = ApplicationData.Current.LocalCacheFolder; + + IStorageItem? file = await localCacheFolder.TryGetItemAsync(StoredFileName); + + if (file is not null && file is StorageFile storageFile) + { + string fileContent = await FileIO.ReadTextAsync(storageFile); + var result = JsonConvert.DeserializeObject(fileContent); + if (result is not null) + { + return result; + } + } + } + } + catch (Exception) + { + } + + return new MarketingState + { + LastAppRatingOfferDate = DateTime.Now, + LastProblemEncounteredDate = DateTime.Now, + LastUpdateDate = DateTime.Now + }; + } + } +} diff --git a/src/dev/impl/DevToys/Core/Threading/AsyncLazy.cs b/src/dev/impl/DevToys/Core/Threading/AsyncLazy.cs new file mode 100644 index 0000000000..a07837e9e2 --- /dev/null +++ b/src/dev/impl/DevToys/Core/Threading/AsyncLazy.cs @@ -0,0 +1,57 @@ +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DevToys.Core.Threading +{ + internal sealed class AsyncLazy + { + private static Func> FromFuncT(Func valueFunc) => () => Task.FromResult(valueFunc()); + + private readonly Lazy> _innerLazy; + + public bool IsValueCreated => _innerLazy.IsValueCreated && _innerLazy.Value.IsCompletedSuccessfully; + + public AsyncLazy(T value) + { + _innerLazy = new Lazy>(Task.FromResult(value)); + } + + public AsyncLazy(Func valueFactory) + { + _innerLazy = new Lazy>(FromFuncT(valueFactory)); + } + + public AsyncLazy(Func valueFactory, bool isThreadSafe) + { + _innerLazy = new Lazy>(FromFuncT(valueFactory), isThreadSafe); + } + + public AsyncLazy(Func valueFactory, LazyThreadSafetyMode mode) + { + _innerLazy = new Lazy>(FromFuncT(valueFactory), mode); + } + + public AsyncLazy(Func> valueFactory) + { + _innerLazy = new Lazy>(valueFactory); + } + + public AsyncLazy(Func> valueFactory, bool isThreadSafe) + { + _innerLazy = new Lazy>(valueFactory, isThreadSafe); + } + + public AsyncLazy(Func> valueFactory, LazyThreadSafetyMode mode) + { + _innerLazy = new Lazy>(valueFactory, mode); + } + + public async Task GetValueAsync() + { + return IsValueCreated? await _innerLazy.Value : _innerLazy.Value.Result; + } + } +} diff --git a/src/dev/impl/DevToys/DevToys.csproj b/src/dev/impl/DevToys/DevToys.csproj index 62c69ab297..a6577c905c 100644 --- a/src/dev/impl/DevToys/DevToys.csproj +++ b/src/dev/impl/DevToys/DevToys.csproj @@ -151,6 +151,7 @@ + @@ -182,6 +183,7 @@ + @@ -189,6 +191,7 @@ + @@ -201,6 +204,7 @@ + CodeEditor.xaml diff --git a/src/dev/impl/DevToys/LanguageManager.cs b/src/dev/impl/DevToys/LanguageManager.cs index c1e05aa832..b5eaa482fd 100644 --- a/src/dev/impl/DevToys/LanguageManager.cs +++ b/src/dev/impl/DevToys/LanguageManager.cs @@ -569,6 +569,21 @@ public class MainPageStrings : ObservableObject /// public string ExitCompactOverlayTooltip => _resources.GetString("ExitCompactOverlayTooltip"); + /// + /// Gets the resource NotificationRateAppActionableActionText. + /// + public string NotificationRateAppActionableActionText => _resources.GetString("NotificationRateAppActionableActionText"); + + /// + /// Gets the resource NotificationRateAppMessage. + /// + public string NotificationRateAppMessage => _resources.GetString("NotificationRateAppMessage"); + + /// + /// Gets the resource NotificationRateAppTitle. + /// + public string NotificationRateAppTitle => _resources.GetString("NotificationRateAppTitle"); + /// /// Gets the resource NotificationReleaseNoteActionableActionText. /// diff --git a/src/dev/impl/DevToys/Models/MarketingState.cs b/src/dev/impl/DevToys/Models/MarketingState.cs new file mode 100644 index 0000000000..7084ca03f7 --- /dev/null +++ b/src/dev/impl/DevToys/Models/MarketingState.cs @@ -0,0 +1,34 @@ +#nullable enable + +using Newtonsoft.Json; +using System; + +namespace DevToys.Models +{ + internal sealed class MarketingState + { + [JsonProperty] + internal DateTime LastProblemEncounteredDate { get; set; } + + [JsonProperty] + internal int StartSinceLastProblemEncounteredCount { get; set; } + + [JsonProperty] + internal int SmartDetectionCount { get; set; } + + [JsonProperty] + internal DateTime LastUpdateDate { get; set; } + + [JsonProperty] + internal int ToolSuccessfulyWorkedCount { get; set; } + + [JsonProperty] + internal DateTime LastAppRatingOfferDate { get; set; } + + [JsonProperty] + internal int AppRatingOfferCount { get; set; } + + [JsonProperty] + internal bool AppGotRated { get; set; } + } +} diff --git a/src/dev/impl/DevToys/Strings/cs-CZ/MainPage.resw b/src/dev/impl/DevToys/Strings/cs-CZ/MainPage.resw index aee2836753..3cdc97903c 100644 --- a/src/dev/impl/DevToys/Strings/cs-CZ/MainPage.resw +++ b/src/dev/impl/DevToys/Strings/cs-CZ/MainPage.resw @@ -123,6 +123,15 @@ Zpět na plné zobrazení (Ctrl+Down) + + rate us now... + + + Enjoying DevToys? Please consider rating us! + + + Um... hi! 😅 + zjistit více... diff --git a/src/dev/impl/DevToys/Strings/en-US/MainPage.resw b/src/dev/impl/DevToys/Strings/en-US/MainPage.resw index 54b2459bec..c9fc56044b 100644 --- a/src/dev/impl/DevToys/Strings/en-US/MainPage.resw +++ b/src/dev/impl/DevToys/Strings/en-US/MainPage.resw @@ -123,6 +123,15 @@ Back to full view (Ctrl+Down) + + rate us now... + + + Enjoying DevToys? Please consider rating us! + + + Um... hi! 😅 + read more... diff --git a/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw b/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw index 4933fc84fa..b85e038c29 100644 --- a/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw +++ b/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw @@ -123,6 +123,15 @@ Retour en vue normal (Ctrl+Down) + + rate us now... + + + Enjoying DevToys? Please consider rating us! + + + Um... hi! 😅 + en savoir plus... diff --git a/src/dev/impl/DevToys/Strings/ru-RU/MainPage.resw b/src/dev/impl/DevToys/Strings/ru-RU/MainPage.resw index 1a17f0afc2..b7b4f1b814 100644 --- a/src/dev/impl/DevToys/Strings/ru-RU/MainPage.resw +++ b/src/dev/impl/DevToys/Strings/ru-RU/MainPage.resw @@ -123,6 +123,15 @@ Вернуться к полному представлению (CTRL + СТРЕЛКА ВНИЗ) + + rate us now... + + + Enjoying DevToys? Please consider rating us! + + + Um... hi! 😅 + подробнее... diff --git a/src/dev/impl/DevToys/ViewModels/MainPageViewModel.cs b/src/dev/impl/DevToys/ViewModels/MainPageViewModel.cs index f20b99e467..acfa2208d1 100644 --- a/src/dev/impl/DevToys/ViewModels/MainPageViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/MainPageViewModel.cs @@ -40,6 +40,7 @@ public sealed class MainPageViewModel : ObservableRecipient private readonly IUriActivationProtocolService _launchProtocolService; private readonly ISettingsProvider _settingsProvider; private readonly INotificationService _notificationService; + private readonly IMarketingService _marketingService; private readonly List _allToolsMenuitems = new(); private readonly DisposableSempahore _sempahore = new(); private readonly Lazy _firstUpdateMenuTask; @@ -192,13 +193,15 @@ public MainPageViewModel( IToolProviderFactory toolProviderFactory, IUriActivationProtocolService launchProtocolService, ISettingsProvider settingsProvider, - INotificationService notificationService) + INotificationService notificationService, + IMarketingService marketingService) { _clipboard = clipboard; _toolProviderFactory = toolProviderFactory; _launchProtocolService = launchProtocolService; _settingsProvider = settingsProvider; _notificationService = notificationService; + _marketingService = marketingService; TitleBar = titleBar; OpenToolInNewWindowCommand = new AsyncRelayCommand(ExecuteOpenToolInNewWindowCommandAsync); @@ -279,6 +282,7 @@ internal async Task OnNavigatedToAsync(NavigationParameter parameters) } } + _marketingService.NotifyAppStarted(); ShowReleaseNoteAsync().Forget(); ShowAvailableUpdateAsync().Forget(); @@ -464,6 +468,10 @@ MatchedToolProviderViewData[] newRecommendedTools else { _pasteInFirstSelectedToolIsAllowed = true; + if (newRecommendedTools.Length > 0) + { + _marketingService.NotifySmartDetectionWorked(); + } } using (await _sempahore.WaitAsync(CancellationToken.None).ConfigureAwait(false)) @@ -508,6 +516,8 @@ private async Task ShowReleaseNoteAsync() Windows.System.Launcher.LaunchUriAsync(new Uri("https://github.com/veler/DevToys/releases")).AsTask().Forget(); }, await AssetsHelper.GetReleaseNoteAsync()); + + _marketingService.NotifyAppJustUpdated(); } _settingsProvider.SetSetting(PredefinedSettings.FirstTimeStart, false); @@ -519,11 +529,7 @@ private async Task ShowAvailableUpdateAsync() // Make sure we work in background. await TaskScheduler.Default; - // Get the current app's package for the current user. - var packageManager = new PackageManager(); - var currentPackage = packageManager.FindPackageForUser(string.Empty, Package.Current.Id.FullName); - - PackageUpdateAvailabilityResult result = await currentPackage.CheckUpdateAvailabilityAsync(); + PackageUpdateAvailabilityResult result = await Package.Current.CheckUpdateAvailabilityAsync(); if (result.Availability == PackageUpdateAvailability.Required || result.Availability == PackageUpdateAvailability.Available) { diff --git a/src/dev/impl/DevToys/ViewModels/Tools/Base64EncoderDecoder/Base64EncoderDecoderToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/Base64EncoderDecoder/Base64EncoderDecoderToolViewModel.cs index bb796905f2..9551bc725c 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/Base64EncoderDecoder/Base64EncoderDecoderToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/Base64EncoderDecoder/Base64EncoderDecoderToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core; @@ -39,6 +40,7 @@ private static readonly SettingDefinition Encoder private const string DefaultConversion = "Encode"; internal const string DecodeConversion = "Decode"; + private readonly IMarketingService _marketingService; private readonly ISettingsProvider _settingsProvider; private readonly Queue _conversionQueue = new(); @@ -46,6 +48,7 @@ private static readonly SettingDefinition Encoder private string? _outputValue; private bool _conversionInProgress; private bool _setPropertyInProgress; + private bool _toolSuccessfullyWorked; public Type View { get; } = typeof(Base64EncoderDecoderToolPage); @@ -116,9 +119,10 @@ internal string EncodingMode } [ImportingConstructor] - public Base64EncoderDecoderToolViewModel(ISettingsProvider settingsProvider) + public Base64EncoderDecoderToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService) { _settingsProvider = settingsProvider; + _marketingService = marketingService; } private void QueueConversionCalculation() @@ -153,6 +157,12 @@ private async Task TreatQueueAsync() ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => { OutputValue = conversionResult; + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/GuidGenerator/GuidGeneratorToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/GuidGenerator/GuidGeneratorToolViewModel.cs index 47699b30ef..6f9ebf7e67 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/GuidGenerator/GuidGeneratorToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/GuidGenerator/GuidGeneratorToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.UI.Controls; @@ -42,8 +43,11 @@ private static readonly SettingDefinition GuidsToGenerate isRoaming: true, defaultValue: 1); + private readonly IMarketingService _marketingService; private readonly ISettingsProvider _settingsProvider; + private string _output = string.Empty; + private bool _toolSuccessfullyWorked; public Type View { get; } = typeof(GuidGeneratoToolPage); @@ -97,9 +101,10 @@ internal string Output internal ICustomTextBox? OutputTextBox { private get; set; } [ImportingConstructor] - public GuidGeneratorToolViewModel(ISettingsProvider settingsProvider) + public GuidGeneratorToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService) { _settingsProvider = settingsProvider; + _marketingService = marketingService; GenerateCommand = new RelayCommand(ExecuteGenerateCommand); } @@ -134,6 +139,12 @@ private void ExecuteGenerateCommand() Output += newGuids.ToString(); OutputTextBox?.ScrollToBottom(); + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } } #endregion diff --git a/src/dev/impl/DevToys/ViewModels/Tools/HashGenerator/HashGeneratorToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/HashGenerator/HashGeneratorToolViewModel.cs index c656d98cea..2d0636545a 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/HashGenerator/HashGeneratorToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/HashGenerator/HashGeneratorToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core; @@ -28,9 +29,11 @@ private static readonly SettingDefinition Uppercase isRoaming: true, defaultValue: false); + private readonly IMarketingService _marketingService; private readonly ISettingsProvider _settingsProvider; private readonly Queue _hashCalculationQueue = new(); + private bool _toolSuccessfullyWorked; private bool _calculationInProgress; private string? _input; private string? _md5; @@ -91,9 +94,10 @@ internal string? SHA512 } [ImportingConstructor] - public HashGeneratorToolViewModel(ISettingsProvider settingsProvider) + public HashGeneratorToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService) { _settingsProvider = settingsProvider; + _marketingService = marketingService; } private void QueueHashCalculation() @@ -128,6 +132,12 @@ private async Task TreatQueueAsync() SHA1 = sha1CalculationTask.Result; SHA256 = sha256CalculationTask.Result; SHA512 = sha512CalculationTask.Result; + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/JsonFormatter/JsonFormatterToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/JsonFormatter/JsonFormatterToolViewModel.cs index e5d40771fe..857421e99f 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/JsonFormatter/JsonFormatterToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/JsonFormatter/JsonFormatterToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core.Threading; @@ -28,8 +29,10 @@ private static readonly SettingDefinition Indentation isRoaming: true, defaultValue: Models.Indentation.TwoSpaces); + private readonly IMarketingService _marketingService; private readonly Queue _formattingQueue = new(); + private bool _toolSuccessfullyWorked; private bool _formattingInProgress; private string? _inputValue; private string? _outputValue; @@ -92,9 +95,10 @@ internal string? OutputValue internal ISettingsProvider SettingsProvider { get; } [ImportingConstructor] - public JsonFormatterToolViewModel(ISettingsProvider settingsProvider) + public JsonFormatterToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService) { SettingsProvider = settingsProvider; + _marketingService = marketingService; } private void QueueFormatting() @@ -122,6 +126,12 @@ private async Task TreatQueueAsync() ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => { OutputValue = result; + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/JsonYaml/JsonYamlToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/JsonYaml/JsonYamlToolViewModel.cs index 3f0fef0007..1b97e3db3e 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/JsonYaml/JsonYamlToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/JsonYaml/JsonYamlToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core; @@ -46,8 +47,10 @@ private static readonly SettingDefinition Indentation private const string TwoSpaceIndentation = "TwoSpaces"; private const string FourSpaceIndentation = "FourSpaces"; + private readonly IMarketingService _marketingService; private readonly Queue _conversionQueue = new(); + private bool _toolSuccessfullyWorked; private bool _conversionInProgress; private bool _setPropertyInProgress; private string? _inputValue; @@ -163,9 +166,10 @@ internal string? OutputValueLanguage internal ISettingsProvider SettingsProvider { get; } [ImportingConstructor] - public JsonYamlToolViewModel(ISettingsProvider settingsProvider) + public JsonYamlToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService) { SettingsProvider = settingsProvider; + _marketingService = marketingService; InputValueLanguage = "json"; OutputValueLanguage = "yaml"; } @@ -203,6 +207,12 @@ private async Task TreatQueueAsync() ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => { OutputValue = result; + + if (success && !_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs index 572c3df21a..a9e03bbb7c 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Tools; using DevToys.Core.Threading; using DevToys.Helpers; @@ -17,8 +18,10 @@ namespace DevToys.ViewModels.Tools.JwtDecoderEncoder [Export(typeof(JwtDecoderEncoderToolViewModel))] public sealed class JwtDecoderEncoderToolViewModel : ObservableRecipient, IToolViewModel { + private readonly IMarketingService _marketingService; private readonly Queue _decodingQueue = new(); + private bool _toolSuccessfullyWorked; private bool _decodingInProgress; private string? _jwtToken; private string? _jwtHeader; @@ -59,6 +62,12 @@ internal string? JwtPayload set => SetProperty(ref _jwtPayload, value); } + [ImportingConstructor] + public JwtDecoderEncoderToolViewModel(IMarketingService marketingService) + { + _marketingService = marketingService; + } + private void QueueConversion() { _decodingQueue.Enqueue(JwtToken ?? string.Empty); @@ -83,6 +92,12 @@ private async Task TreatQueueAsync() { JwtHeader = header; JwtPayload = payload; + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/MarkdownPreview/MarkdownPreviewToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/MarkdownPreview/MarkdownPreviewToolViewModel.cs index a51848dc04..e431b1ec13 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/MarkdownPreview/MarkdownPreviewToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/MarkdownPreview/MarkdownPreviewToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Core.Theme; using DevToys.Api.Tools; @@ -30,9 +31,11 @@ private static readonly SettingDefinition ThemeSetting isRoaming: true, defaultValue: null); + private readonly IMarketingService _marketingService; private readonly IThemeListener _themeListener; private readonly Queue _workQueue = new(); + private bool _toolSuccessfullyWorked; private bool _workInProgress; private string? _inputValue; @@ -78,8 +81,9 @@ internal ApplicationTheme Theme internal ISettingsProvider SettingsProvider { get; } [ImportingConstructor] - public MarkdownPreviewToolViewModel(ISettingsProvider settingsProvider, IThemeListener themeListener) + public MarkdownPreviewToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService, IThemeListener themeListener) { + _marketingService = marketingService; _themeListener = themeListener; SettingsProvider = settingsProvider; @@ -113,6 +117,12 @@ private async Task TreatQueueAsync() ThreadHelper.RunOnUIThreadAsync(() => { Messenger.Send(new NavigateToMarkdownPreviewHtmlMessage(html)); + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/RegEx/RegExToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/RegEx/RegExToolViewModel.cs index 4c9cfab272..0a6f34d128 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/RegEx/RegExToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/RegEx/RegExToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core.Threading; @@ -60,8 +61,10 @@ private static readonly SettingDefinition EcmaScriptSetting isRoaming: true, defaultValue: false); + private readonly IMarketingService _marketingService; private readonly Queue<(string pattern, string text)> _regExMatchingQueue = new(); + private bool _toolSuccessfullyWorked; private bool _calculationInProgress; private string? _regularExpression; private string? _text; @@ -193,9 +196,10 @@ internal string? Text internal ICustomTextBox? MatchTextBox { private get; set; } [ImportingConstructor] - public RegExToolViewModel(ISettingsProvider settingsProvider) + public RegExToolViewModel(ISettingsProvider settingsProvider, IMarketingService marketingService) { SettingsProvider = settingsProvider; + _marketingService = marketingService; } private void QueueRegExMatch() @@ -248,6 +252,12 @@ private async Task TreatQueueAsync() () => { MatchTextBox?.SetHighlights(spans); + + if (spans.Count > 0 && !_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }).ForgetSafely(); } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/StringUtilities/StringUtilitiesToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/StringUtilities/StringUtilitiesToolViewModel.cs index 1efc9eed95..ecf65fa9a2 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/StringUtilities/StringUtilitiesToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/StringUtilities/StringUtilitiesToolViewModel.cs @@ -1,5 +1,6 @@ #nullable enable +using DevToys.Api.Core; using DevToys.Api.Tools; using DevToys.Core.Threading; using DevToys.Views.Tools.StringUtilities; @@ -20,6 +21,9 @@ public sealed class StringUtilitiesToolViewModel : ObservableRecipient, IToolVie { private static readonly object _lockObject = new(); + private readonly IMarketingService _marketingService; + + private bool _toolSuccessfullyWorked; private bool _forbidBackup; private string? _text; private string? _textBackup; @@ -135,8 +139,9 @@ internal string? CharacterDistribution } [ImportingConstructor] - public StringUtilitiesToolViewModel() + public StringUtilitiesToolViewModel(IMarketingService marketingService) { + _marketingService = marketingService; OriginalCaseCommand = new RelayCommand(ExecuteOriginalCaseCommand, CanExecuteOriginalCaseCommand); SentenceCaseCommand = new RelayCommand(ExecuteSentenceCaseCommand); LowerCaseCommand = new RelayCommand(ExecuteLowerCaseCommand); @@ -787,6 +792,12 @@ await ThreadHelper.RunOnUIThreadAsync( Bytes = bytes; WordDistribution = wordDistribution; CharacterDistribution = characterDistribution; + + if (!_toolSuccessfullyWorked) + { + _toolSuccessfullyWorked = true; + _marketingService.NotifyToolSuccessfullyWorked(); + } }); } From 6c3fff419407b2eb90b730927a9cbfbd94855406 Mon Sep 17 00:00:00 2001 From: Etienne BAUDOUX Date: Fri, 5 Nov 2021 18:10:20 -0700 Subject: [PATCH 2/4] adjusted marketing service --- src/dev/impl/DevToys/Core/MarketingService.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/dev/impl/DevToys/Core/MarketingService.cs b/src/dev/impl/DevToys/Core/MarketingService.cs index 6de10a24c5..51d6eaec41 100644 --- a/src/dev/impl/DevToys/Core/MarketingService.cs +++ b/src/dev/impl/DevToys/Core/MarketingService.cs @@ -52,10 +52,7 @@ public void NotifyAppJustUpdated() UpdateMarketingStateAsync(state => { state.LastUpdateDate = DateTime.Now; - }).ContinueWith(_ => - { - TryOfferUserToRateApp(); - }); + }).ForgetSafely(); } public void NotifyAppStarted() @@ -63,10 +60,7 @@ public void NotifyAppStarted() UpdateMarketingStateAsync(state => { state.StartSinceLastProblemEncounteredCount++; - }).ContinueWith(_ => - { - TryOfferUserToRateApp(); - }); + }).ForgetSafely(); } public void NotifySmartDetectionWorked() @@ -77,7 +71,7 @@ public void NotifySmartDetectionWorked() }).ContinueWith(_ => { TryOfferUserToRateApp(); - }); + }).ForgetSafely(); } public void NotifyToolSuccessfullyWorked() @@ -88,7 +82,7 @@ public void NotifyToolSuccessfullyWorked() }).ContinueWith(_ => { TryOfferUserToRateApp(); - }); + }).ForgetSafely(); } private void TryOfferUserToRateApp() From bdc15cf6f006caefd7e02305d49a7aa93c18483e Mon Sep 17 00:00:00 2001 From: Etienne BAUDOUX Date: Fri, 5 Nov 2021 18:12:21 -0700 Subject: [PATCH 3/4] updated Readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 31454df126..964ad34b21 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ DevToys helps in everyday tasks like formatting JSON, comparing text, testing Re Many tools are available. - Base 64 Encoder/Decoder - Hash Generator (MD5, SHA1, SHA256, SHA512) +- Guid Generator - JWT Decoder - Json Formatter - RegExp Tester @@ -67,6 +68,7 @@ For example, `start devtoys:?tool=jsonyaml` will open DevToys and start on the ` Here is the list of tool name you can use: - `base64` - Base64 Encoder/Decoder - `hash` - Hash Generator +- `guid` - Guid Generator - `jsonformat` Json Formatter - `jsonyaml` - Json <> Yaml - `jwt` - JWT Decoder From 52f5ba779ef274523e6d2133bcfb21aec70d9638 Mon Sep 17 00:00:00 2001 From: Etienne BAUDOUX Date: Fri, 5 Nov 2021 18:25:53 -0700 Subject: [PATCH 4/4] update french language --- src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw b/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw index b85e038c29..4beebd8465 100644 --- a/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw +++ b/src/dev/impl/DevToys/Strings/fr-FR/MainPage.resw @@ -124,13 +124,13 @@ Retour en vue normal (Ctrl+Down) - rate us now... + évaluez maintenant... - Enjoying DevToys? Please consider rating us! + Vous aimez DevToys? N'hésitez pas à évaluer l'app! - Um... hi! 😅 + Hum... bonjour! 😅 en savoir plus...