diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/docs/appPasswordsHelp.md b/Doc/appPasswordsHelp.md similarity index 100% rename from docs/appPasswordsHelp.md rename to Doc/appPasswordsHelp.md diff --git a/Images/shot01.png b/Images/shot01.png new file mode 100644 index 0000000..1edee51 Binary files /dev/null and b/Images/shot01.png differ diff --git a/Images/shot02.png b/Images/shot02.png new file mode 100644 index 0000000..6d50cb1 Binary files /dev/null and b/Images/shot02.png differ diff --git a/README.md b/README.md index 81149c8..9fdaf5d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,10 @@ -

- -

-

- Beeskie -

-

- A Windows app for Bluesky -

-

- - Store link - -

+# Beeskie v0.6.x (Beta) - master branch +![](Images/logo.png) +My RnD of Beeskie, modern uwp app for BlueSky social network. The main gool is to do src code Andromeda-compatible (see https://github.com/mediaexplorer74/Andromeda for details / my samples/dev kit)! + +## About (words of the author) +" ## Introduction Beeskie is a free and open source third-party app for Bluesky. Big kudos to the team that built the Bluesky APIs, which Beeskie relies on heavily. The APIs are extremely thorough and they're very friendly to third-party apps. @@ -23,4 +15,60 @@ The app is currently in public beta. You can write new posts, reply, repost, and ## How can I help? -Download the app from the store by clicking the badge above, follow [Beeskie on Bluesky](https://bsky.app/profile/beeskieapp.bsky.social), and send feedback to that handle! While I know there are many things missing still, it will be valuable if you tell me 3 things that you absolutely need ASAP in order to use the app on a more consistent basis. This will help me prioritize the features. Of course, you can let me know of other issues such as bugs. Lastly, you can also create an issue in this repo to submit any feedback. Thanks for your help! \ No newline at end of file +Download the app from the store by clicking the badge above, follow [Beeskie on Bluesky](https://bsky.app/profile/beeskieapp.bsky.social), and send feedback to that handle! While I know there are many things missing still, it will be valuable if you tell me 3 things that you absolutely need ASAP in order to use the app on a more consistent basis. This will help me prioritize the features. Of course, you can let me know of other issues such as bugs. Lastly, you can also create an issue in this repo to submit any feedback. Thanks for your help! + +Beeskie (Beta) on Microsoft Store: https://apps.microsoft.com/store/detail/9PCGNR7QHQGP?cid=github +" + - Daniel from Jenius Apps + +## Screenshots +![](Images/shot01.png) +![](Images/shot02.png) + + +## Tech/dev details +- Platforms: UWP only +- Targets: x64; x64; ARM +- OSes: Windows 11 (however, W10M Andromeda is good mobile case too))) +- Win. SDK used: 19041 +- Min. Win. OS build: 17134 (Hello, Microsoft WCOS!) + +## Status / my 2 cents +- Micro-research of scr code +- win sdk 22000 -> 17xxx +- Draft. Prototype / Pre-Pre-Pre-Alpha version. Still exploring modern-ui & mvvm "magic"... +- some common tools experiments / patches + +## Caution +- I noticed that src code uses some "dev telemetry". It's question of your "login-password" security, I think. I have no time to fix cut off that deal. +- Please use special "app password" for your own app tests. + +### Bluesky App Passwords + +App passwords are codes that Bluesky generates for you which you can use for third-party apps, such as Beeskie. It is **not** that same as your Bluesky account password. + +### How to start with BlueSky social network and how to generate an App Password + +- Register your account on the official website https://bsky.app. +- Login to the official website https://bsky.app. +- Open settings from the left sidebar menu. +- Click "Privacy and security". +- Click "App passwords". +- Click "Add App Password". Bluesky will generate a new code and display it on screen. +- Start Beeskie app. Copy that code and paste it into Beeskie in the app password field. +- Field "handle" means word construction like "youraccountname.bsky.social". However, your email can substitute this handle. + +## References +- https://github.com/jenius-apps/beeskie/ Original Beeskie project +- https://github.com/jenius-apps/ Jenius Apps, Beeskie's creators/dev team + +## Licensing +MIT License + +## .. +AS IS. No support. RnD only / DIY + +## . +[m][e] November 2024 + + diff --git a/Src/Clean-Bin-Obj-Folders.cmd b/Src/Clean-Bin-Obj-Folders.cmd new file mode 100644 index 0000000..21de54e --- /dev/null +++ b/Src/Clean-Bin-Obj-Folders.cmd @@ -0,0 +1 @@ +for /d /r . %%d in (bin,obj) do @if exist "%%d" rd /s /q "%%d" diff --git a/Src/JeniusApps.Common.Uwp/Authentication/WindowsMsalClient.cs b/Src/JeniusApps.Common.Uwp/Authentication/WindowsMsalClient.cs new file mode 100644 index 0000000..7eb60f5 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Authentication/WindowsMsalClient.cs @@ -0,0 +1,122 @@ +using JeniusApps.Common.Telemetry; +using Microsoft.Identity.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +#nullable enable + +namespace JeniusApps.Common.Authentication.Uwp; + +public class WindowsMsalClient : IMsalClient +{ + // Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#effective-audience + public const string ConsumerAuthority = "https://login.microsoftonline.com/consumers"; + public const string CommonAuthority = "https://login.microsoftonline.com/common"; + + private readonly ITelemetry _telemetry; + private readonly string _clientId; + private readonly IPublicClientApplication _msalSdkClient; + + public event EventHandler? InteractiveSignInCompleted; + + public WindowsMsalClient(ITelemetry telemetry, string clientId, string authorityUrl) + { + _telemetry = telemetry; + _clientId = clientId; + + _msalSdkClient = PublicClientApplicationBuilder + .Create(_clientId) + .WithAuthority(authorityUrl) + .WithBroker() // See note below. + .Build(); + + // ****** WithBroker notes ****** + // + // If using WithBroker(), no need for WithRedirectUri(). + // Instead, withBroker works only when we include special redirect URIs in Azure Portal: + // ms-appx-web://microsoft.aad.brokerplugin/ + // + // To find your SID, go to partner center > Product Identity. + } + + /// + public async Task GetTokenSilentAsync(string[] scopes) + { + try + { + var accounts = await _msalSdkClient.GetAccountsAsync(); + var firstAccount = accounts.FirstOrDefault(); + var authResult = await _msalSdkClient + .AcquireTokenSilent(scopes, firstAccount) + .ExecuteAsync(); + return authResult.AccessToken; + } + catch (MsalUiRequiredException) + { + // this is fine + } + catch (MsalException e) when (e.ErrorCode == "user_null") + { + // this is fine + } + catch (MsalException e) + { + _telemetry.TrackError(e, new Dictionary + { + { "trace", e.StackTrace }, + { "scopes", string.Join(",", scopes) } + }); + } + catch (HttpRequestException) + { + // no internet + } + + return ""; + } + + /// + public async Task RequestInteractiveSignIn(string[] scopes, string[]? extraScopes = null) + { + try + { + var builder = _msalSdkClient.AcquireTokenInteractive(scopes); + + if (extraScopes is not null) + { + builder = builder.WithExtraScopesToConsent(extraScopes); + } + + var authResult = await builder.ExecuteAsync(); + InteractiveSignInCompleted?.Invoke(this, authResult?.AccessToken); + } + catch (MsalException e) when (e.ErrorCode == "authentication_canceled") + { + InteractiveSignInCompleted?.Invoke(this, string.Empty); + } + catch (MsalException e) + { + InteractiveSignInCompleted?.Invoke(this, string.Empty); + + _telemetry.TrackError(e, new Dictionary + { + { "trace", e.StackTrace }, + { "scopes", string.Join(",", scopes) }, + { "extraScopes", string.Join(",", extraScopes ?? Array.Empty()) } + }); + } + } + + /// + public async Task SignOutAsync() + { + var accounts = await _msalSdkClient.GetAccountsAsync(); + foreach (var a in accounts) + { + await _msalSdkClient.RemoveAsync(a); + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/JeniusApps.Common.Uwp.csproj b/Src/JeniusApps.Common.Uwp/JeniusApps.Common.Uwp.csproj new file mode 100644 index 0000000..2536396 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/JeniusApps.Common.Uwp.csproj @@ -0,0 +1,201 @@ + + + + + latest + Debug + AnyCPU + {3354DE16-CD32-4C68-8268-297FA1CDC0E5} + Library + Properties + JeniusApps.Common + JeniusApps.Common.Uwp + en-US + UAP + 10.0.22621.0 + 10.0.17134.0 + 14 + 512 + 12.0 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM64 + true + bin\ARM64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM64 + bin\ARM64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + PackageReference + + + JeniusApps.Common.Uwp + 0.0.41-preview + Daniel Paulino + JeniusApps + A C# UWP class library of common components used to build apps. + true + true + bin\ + LICENSE + + + + True + + + + + + + + + + + + + + + + + + + + + + + + + + 4.56.0 + + + 6.0.0 + + + 7.0.0 + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 6.2.14 + + + 1.14.1 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + {815dcba0-659a-4cbc-84df-c763e2c4f45b} + JeniusApps.Common + + + + 14.0 + + + + \ No newline at end of file diff --git a/Src/JeniusApps.Common.Uwp/Properties/AssemblyInfo.cs b/Src/JeniusApps.Common.Uwp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0899b6a --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JeniusApps.Common.Uwp")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JeniusApps.Common.Uwp")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Src/JeniusApps.Common.Uwp/Properties/JeniusApps.Common.Uwp.rd.xml b/Src/JeniusApps.Common.Uwp/Properties/JeniusApps.Common.Uwp.rd.xml new file mode 100644 index 0000000..904ec4d --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Properties/JeniusApps.Common.Uwp.rd.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/Src/JeniusApps.Common.Uwp/Settings/LocalSettings.cs b/Src/JeniusApps.Common.Uwp/Settings/LocalSettings.cs new file mode 100644 index 0000000..bd34763 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Settings/LocalSettings.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Windows.Storage; + +#nullable enable + +namespace JeniusApps.Common.Settings.Uwp; + +public class LocalSettings : IUserSettings +{ + private readonly IReadOnlyDictionary _defaults; + + /// + public event EventHandler? SettingSet; + + public LocalSettings(IReadOnlyDictionary defaults) + { + _defaults = defaults; + } + + /// + public T? Get(string settingKey) + { + object result = ApplicationData.Current.LocalSettings.Values[settingKey]; + return result is null ? GetDefault(settingKey) : (T)result; + } + + /// + public void Set(string settingKey, T value) + { + ApplicationData.Current.LocalSettings.Values[settingKey] = value; + SettingSet?.Invoke(this, settingKey); + } + + /// + public T? Get(string settingKey, T defaultOverride) + { + object result = ApplicationData.Current.LocalSettings.Values[settingKey]; + return result is null ? defaultOverride : (T)result; + } + + /// + public T? GetAndDeserialize(string settingKey, JsonTypeInfo jsonTypeInfo) + { + object result = ApplicationData.Current.LocalSettings.Values[settingKey]; + if (result is string serialized) + { + try + { + return JsonSerializer.Deserialize(serialized, jsonTypeInfo); + } + catch { } + } + + return GetDefault(settingKey); + } + + /// + public void SetAndSerialize(string settingKey, T value, JsonTypeInfo jsonTypeInfo) + { + var serialized = JsonSerializer.Serialize(value, jsonTypeInfo); + Set(settingKey, serialized); + } + + private T? GetDefault(string settingKey) + { + return _defaults.ContainsKey(settingKey) + ? (T)_defaults[settingKey] + : default; + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/AssetReader.cs b/Src/JeniusApps.Common.Uwp/Tools/AssetReader.cs new file mode 100644 index 0000000..5008e89 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/AssetReader.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Windows.ApplicationModel; +using Windows.Storage; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp +{ + public class AssetReader : IAssetsReader + { + /// + public async Task ReadFileAsync(string relativePath) + { + StorageFolder currentFolder = Package.Current.InstalledLocation; + var splits = relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + StorageFile? file = null; + + for (int i = 0; i < splits.Length; i++) + { + if (splits.Length - 1 == i) + { + file = await currentFolder.GetFileAsync(splits[i]); + break; + } + + currentFolder = await currentFolder.GetFolderAsync(splits[i]); + } + + return await FileIO.ReadTextAsync(file); + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/DispatcherQueue.cs b/Src/JeniusApps.Common.Uwp/Tools/DispatcherQueue.cs new file mode 100644 index 0000000..8967034 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/DispatcherQueue.cs @@ -0,0 +1,21 @@ +using System; +using Windows.System; + +namespace JeniusApps.Common.Tools.Uwp +{ + public class WindowsDispatcherQueue : IDispatcherQueue + { + private readonly DispatcherQueue _dispatcherQueue; + + public WindowsDispatcherQueue() + { + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + } + + /// + public void TryEnqueue(Action action) + { + _dispatcherQueue.TryEnqueue(() => action()); + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/MicrosoftStoreUpdater.cs b/Src/JeniusApps.Common.Uwp/Tools/MicrosoftStoreUpdater.cs new file mode 100644 index 0000000..23d6ac0 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/MicrosoftStoreUpdater.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Windows.Foundation; +using System.Diagnostics.CodeAnalysis; +using Windows.Services.Store; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp; + +/// +/// Class for updating the app using Microsoft Store SDK. +/// +public sealed class MicrosoftStoreUpdater : IAppStoreUpdater +{ + private StoreContext? _context; + private IReadOnlyList _updates = []; + + /// + public event EventHandler? ProgressChanged; + + /// + public event EventHandler? UpdateAvailable; + + /// + public async Task TrySilentDownloadAsync() + { + var hasUpdates = await CheckForUpdatesAsync(); + + if (!hasUpdates || !_context.CanSilentlyDownloadStorePackageUpdates) + { + return false; + } + + StorePackageUpdateResult downloadResult = await _context.TrySilentDownloadStorePackageUpdatesAsync(_updates); + return downloadResult.OverallState is StorePackageUpdateState.Completed; + } + + /// + public async Task TrySilentDownloadAndInstallAsync() + { + var hasUpdates = await CheckForUpdatesAsync(); + + if (!hasUpdates || !_context.CanSilentlyDownloadStorePackageUpdates) + { + return false; + } + + StorePackageUpdateResult downloadResult = await _context.TrySilentDownloadAndInstallStorePackageUpdatesAsync(_updates); + return downloadResult.OverallState is StorePackageUpdateState.Completed; + } + + /// + [MemberNotNull(nameof(_context))] + public async Task CheckForUpdatesAsync() + { + _context ??= StoreContext.GetDefault(); + + try + { + _updates = await _context.GetAppAndOptionalStorePackageUpdatesAsync(); + } + catch (FileNotFoundException) + { + // This exception occurs if the app is not associated with the store. + return false; + } + + if (_updates is null) + { + return false; + } + + if (_updates.Count > 0) + { + UpdateAvailable?.Invoke(this, EventArgs.Empty); + } + + return _updates.Count > 0; + } + + /// + public async Task TryApplyUpdatesAsync() + { + if (_updates.Count == 0) + { + return null; + } + + _context ??= StoreContext.GetDefault(); + + IAsyncOperationWithProgress downloadOperation = + _context.RequestDownloadAndInstallStorePackageUpdatesAsync(_updates); + + downloadOperation.Progress = (asyncInfo, progress) => + { + ProgressChanged?.Invoke(null, progress.PackageDownloadProgress); + }; + + var result = await downloadOperation.AsTask(); + + return result.OverallState is StorePackageUpdateState.Completed; + } +} \ No newline at end of file diff --git a/Src/JeniusApps.Common.Uwp/Tools/Navigator.cs b/Src/JeniusApps.Common.Uwp/Tools/Navigator.cs new file mode 100644 index 0000000..31de5a8 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/Navigator.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media.Animation; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp +{ + public class Navigator : INavigator + { + private readonly IReadOnlyDictionary _pageTypeMap; + private object? _frame; + private INavigator? _innerNavigator; + + /// + public event EventHandler? PageNavigated; + + public Navigator(IReadOnlyDictionary pageTypeMap) + { + _pageTypeMap = pageTypeMap; + } + + /// + public void SetFrame(object frame) => _frame = frame; + + /// + public void NavigateTo(string pageKey, object? navArgs = null, PageTransition transition = PageTransition.None) + { + if (!_pageTypeMap.TryGetValue(pageKey, out Type pageType)) + { + return; + } + + if (_frame is Frame f) + { + f.Navigate(pageType, navArgs, ToTransitionInfo(transition)); + PageNavigated?.Invoke(this, pageKey); + } + } + + /// + public void GoBack(PageTransition transition = PageTransition.None, bool innerFrameGoBack = false) + { + if (_frame is Frame f && f.CanGoBack) + { + f.GoBack(ToTransitionInfo(transition)); + + if (innerFrameGoBack && _innerNavigator is { } innerNav) + { + innerNav.GoBack(); + } + } + } + + /// + public void SetInnerNavigator(INavigator inner) => _innerNavigator = inner; + + private NavigationTransitionInfo ToTransitionInfo(PageTransition transition) + { + return transition switch + { + PageTransition.None => new SuppressNavigationTransitionInfo(), + PageTransition.Drill => new DrillInNavigationTransitionInfo(), + PageTransition.SlideFromBottom => new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromBottom }, + PageTransition.SlideFromLeft => new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromLeft }, + PageTransition.SlideFromRight => new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }, + _ => new SuppressNavigationTransitionInfo() + }; + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/NavigatorFactory.cs b/Src/JeniusApps.Common.Uwp/Tools/NavigatorFactory.cs new file mode 100644 index 0000000..afdf18a --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/NavigatorFactory.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Windows.UI.Xaml.Controls; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp +{ + public class NavigatorFactory : INavigatorFactory + { + private readonly Dictionary _navigators = new(); + + /// + public INavigator GetOrCreate( + string navigatorName, + object? constructorParameter, + object? frame) + { + if (_navigators.TryGetValue(navigatorName, out INavigator navigator)) + { + if (frame is not null) + { + navigator.SetFrame(frame); + } + + return navigator; + } + + if (constructorParameter is IReadOnlyDictionary dictionary && + frame is Frame f) + { + var newNavigator = new Navigator(dictionary); + newNavigator.SetFrame(f); + _navigators.TryAdd(navigatorName, newNavigator); + return newNavigator; + } + else + { + throw new ArgumentException("The constructor parameter or the frame object provided are invalid."); + } + } + + /// + public INavigator? Get(string navigatorName) + { + if (_navigators.TryGetValue(navigatorName, out INavigator navigator)) + { + return navigator; + } + + return null; + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/ReswLocalizer.cs b/Src/JeniusApps.Common.Uwp/Tools/ReswLocalizer.cs new file mode 100644 index 0000000..6e988df --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/ReswLocalizer.cs @@ -0,0 +1,31 @@ +using Windows.ApplicationModel.Resources; + +namespace JeniusApps.Common.Tools.Uwp +{ + public class ReswLocalizer : ILocalizer + { + private readonly ResourceLoader _resourceLoader; + + public ReswLocalizer() + { + _resourceLoader = ResourceLoader.GetForCurrentView(); + } + + /// + public string GetString(string key) + { + if (string.IsNullOrEmpty(key)) + { + return string.Empty; + } + + return _resourceLoader.GetString(key); + } + + /// + public string GetString(string key, string formatParam) + { + return string.Format(_resourceLoader.GetString(key), formatParam); + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/StartupService.cs b/Src/JeniusApps.Common.Uwp/Tools/StartupService.cs new file mode 100644 index 0000000..0ec90b5 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/StartupService.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Windows.ApplicationModel; + +namespace JeniusApps.Common.Tools.Uwp; + +public class StartupService : IStartupService +{ + /// + public async Task GetStateAsync(string startupTaskId) + { + StartupTask startupTask = await StartupTask.GetAsync(startupTaskId); + return ToState(startupTask.State); + } + + /// + public async Task TryEnableOnUiThreadAsync(string startupTaskId) + { + StartupTask startupTask = await StartupTask.GetAsync(startupTaskId); + if (startupTask.State is + StartupTaskState.DisabledByUser or + StartupTaskState.DisabledByUser) + { + return false; + } + else if (startupTask.State is + StartupTaskState.Enabled or + StartupTaskState.EnabledByPolicy) + { + return true; + } + else if (startupTask.State is StartupTaskState.Disabled) + { + // Disabled but can be enabled + var newTaskState = await startupTask.RequestEnableAsync(); + return ToState(newTaskState) is StartupState.Enabled; + } + + return false; + } + + private StartupState ToState(StartupTaskState taskState) + { + return taskState switch + { + StartupTaskState.Disabled => StartupState.Disabled, + StartupTaskState.DisabledByUser => StartupState.DisabledByUser, + StartupTaskState.Enabled => StartupState.Enabled, + StartupTaskState.DisabledByPolicy => StartupState.Disallowed, + StartupTaskState.EnabledByPolicy => StartupState.Enabled, + _ => StartupState.Disallowed, + }; + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/SystemInfoProvider.cs b/Src/JeniusApps.Common.Uwp/Tools/SystemInfoProvider.cs new file mode 100644 index 0000000..51d540e --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/SystemInfoProvider.cs @@ -0,0 +1,71 @@ +using Microsoft.Toolkit.Uwp;//.Helpers; +using System; +using Windows.Storage; +using Windows.System.Power; +using Windows.System.Profile; +using Windows.UI.ViewManagement; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp; + +public class SystemInfoProvider : ISystemInfoProvider +{ + private bool? _isWin11; + + /// + public DateTime FirstUseDate() + { + //TODO + return DateTime.Now;//SystemInformation.Instance.FirstUseTime; + } + + /// + public string GetCulture() + { + //TODO + return "en-us";//SystemInformation.Instance.Culture.Name; + } + + /// + public bool IsCompact() + { + return ( ApplicationView.GetForCurrentView().ViewMode + == ApplicationViewMode.CompactOverlay ); + } + + /// + public string GetDeviceFamily() + { + return AnalyticsInfo.VersionInfo.DeviceFamily; + } + + /// + public bool IsFirstRun() + { + //TODO + return true;//SystemInformation.Instance.IsFirstRun; + } + + /// + public bool IsWin11() + { + _isWin11 ??= Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent( + "Windows.Foundation.UniversalApiContract", + 14); + + return _isWin11.Value; + } + + /// + public string LocalFolderPath() + { + return ApplicationData.Current.LocalFolder.Path; + } + + /// + public bool IsOnBatterySaver() + { + return PowerManager.EnergySaverStatus == EnergySaverStatus.On; + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/ToastService.cs b/Src/JeniusApps.Common.Uwp/Tools/ToastService.cs new file mode 100644 index 0000000..3f913e9 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/ToastService.cs @@ -0,0 +1,122 @@ +using Microsoft.Toolkit.Uwp.Notifications; +using System; +using Windows.UI.Notifications; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp; + +public class ToastService : IToastService +{ + private readonly Lazy _dismissButton; + + public ToastService() + { + _dismissButton = new Lazy(() => new ToastButtonDismiss()); + } + + /// + public void ClearScheduledToasts() + { + try + { + ToastNotifierCompat notifier = ToastNotificationManagerCompat.CreateToastNotifier(); + var scheduled = notifier.GetScheduledToastNotifications(); + + if (scheduled != null) + { + foreach (var toRemove in scheduled) + { + notifier.RemoveFromSchedule(toRemove); + } + } + } + catch + { + // Crash telemetry suggests that sometimes, the + // "notification platform" is unavailable, leading to a crash somehere here. + // We added the try-catch to try to mitigate the crash. + } + } + + /// + public void SendToast( + string title, + string message, + bool dismissVisible = false, + DateTime? scheduledDateTime = null, + string launchArg = "", + string tag = "", + Uri? audioUri = null, + bool audioSilent = false, + int minutesExpiration = 0) + { + if (scheduledDateTime is not null && + scheduledDateTime <= DateTime.Now) + { + return; + } + + var builder = new ToastContentBuilder() + .SetToastScenario(ToastScenario.Default) + .AddText(title) + .AddText(message) + .AddArgument(launchArg); + + if (audioUri is not null) + { + builder.AddAudio(audioUri, silent: audioSilent); + } + + if (dismissVisible) + { + builder.AddButton(_dismissButton.Value); + } + + if (scheduledDateTime is DateTime scheduledTime) + { + builder.Schedule(scheduledTime, toast => + { + if (!string.IsNullOrEmpty(tag)) + { + toast.Tag = tag; + } + + if (minutesExpiration > 0) + { + toast.ExpirationTime = DateTimeOffset.Now.AddMinutes(minutesExpiration); + } + }); + } + else + { + builder.Show(toast => + { + if (!string.IsNullOrEmpty(tag)) + { + toast.Tag = tag; + } + + if (minutesExpiration > 0) + { + toast.ExpirationTime = DateTimeOffset.Now.AddMinutes(minutesExpiration); + } + }); + } + } + + /// + public bool DoesToastExist(string toastTag) + { + var toasts = ToastNotificationManager.History.GetHistory(); + foreach (var toast in toasts) + { + if (toast.Tag == toastTag) + { + return true; + } + } + + return false; + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/UriLauncher.cs b/Src/JeniusApps.Common.Uwp/Tools/UriLauncher.cs new file mode 100644 index 0000000..a7dec03 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/UriLauncher.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Windows.System; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp +{ + public class UriLauncher : IUriLauncher + { + /// + public async Task LaunchUriAsync(Uri uri) + { + return await Launcher.LaunchUriAsync(uri); + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/WindowsClipboard.cs b/Src/JeniusApps.Common.Uwp/Tools/WindowsClipboard.cs new file mode 100644 index 0000000..1e4b20c --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/WindowsClipboard.cs @@ -0,0 +1,28 @@ +using Windows.ApplicationModel.DataTransfer; + +namespace JeniusApps.Common.Tools.Uwp +{ + public class WindowsClipboard : IClipboard + { + /// + public bool CopyToClipboard(string text) + { + try + { + DataPackage dataPackage = new() + { + RequestedOperation = DataPackageOperation.Copy + }; + + dataPackage.SetText(text); + Clipboard.SetContent(dataPackage); + } + catch + { + return false; + } + + return true; + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/Tools/WindowsMediaPlayer.cs b/Src/JeniusApps.Common.Uwp/Tools/WindowsMediaPlayer.cs new file mode 100644 index 0000000..7ccd522 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/Tools/WindowsMediaPlayer.cs @@ -0,0 +1,212 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; +using Windows.Media.Core; +using Windows.Media.Playback; +using Windows.Storage; + +#nullable enable + +namespace JeniusApps.Common.Tools.Uwp; + +public class WindowsMediaPlayer : IMediaPlayer +{ + private readonly MediaPlayer _player; + private readonly System.Timers.Timer _timer = new(); + private double _fadeInTargetVolume; + private double _fadeOutStartingVolume; + private long _fadeStart; + private long _fadeEnd; + private long _startEndDiff; + private bool _fadeIn; + private bool _disposeAfterFadeOut; + private CancellationTokenSource _fadeCts = new(); + + public event EventHandler? PositionChanged; + + public WindowsMediaPlayer(bool disableSystemControls = false) + { + var player = new MediaPlayer(); + if (disableSystemControls) + { + player.CommandManager.IsEnabled = false; + } + _player = player; + _player.PlaybackSession.PositionChanged += OnPlaybackPositionChanged; + _timer.Elapsed += OnFadeTimerTick; + _timer.Interval = 30; + } + + /// + public TimeSpan Duration => _player.PlaybackSession.NaturalDuration; + + /// + public double Volume + { + get => _player.Volume; + set + { + _fadeCts.Cancel(); + _player.Volume = value; + } + } + + /// + public void Play(double fadeInTargetVolume, double fadeDuration) + { + _fadeCts.Cancel(); + + if (fadeInTargetVolume <= 0 || fadeDuration <= 0) + { + _player.Play(); + return; + } + + _fadeCts = new CancellationTokenSource(); + _fadeIn = true; + var now = DateTime.Now; + _fadeStart = now.Ticks; + _fadeEnd = now.AddMilliseconds(fadeDuration).Ticks; + _startEndDiff = _fadeEnd - _fadeStart; + _fadeInTargetVolume = fadeInTargetVolume; + _player.Volume = 0; + _player.Play(); + _timer.Start(); + } + + /// + public void Pause(double fadeOutDuration, bool disposeAfterFadeOut = false) + { + _fadeCts.Cancel(); + + if (fadeOutDuration <= 0) + { + _player.Pause(); + return; + } + + _disposeAfterFadeOut = disposeAfterFadeOut; + _fadeCts = new CancellationTokenSource(); + _fadeIn = false; + var now = DateTime.Now; + _fadeStart = now.Ticks; + _fadeEnd = now.AddMilliseconds(fadeOutDuration).Ticks; + _startEndDiff = _fadeEnd - _fadeStart; + _fadeOutStartingVolume = _player.Volume; + _timer.Start(); + } + + public void Play() + { + _fadeCts.Cancel(); + _player.Play(); + } + + public void Pause() + { + _fadeCts.Cancel(); + _player.Pause(); + } + + /// + public void Dispose() + { + _fadeCts.Cancel(); + _player.Dispose(); + } + + /// + public bool SetUriSource(Uri uriSource, bool enableGaplessLoop = false) + { + try + { + var mediaSource = MediaSource.CreateFromUri(uriSource); + AssignSource(mediaSource, enableGaplessLoop); + } + catch + { + return false; + } + + return true; + } + + /// + public async Task SetSourceAsync(string pathToFile, bool enableGaplessLoop = false) + { + try + { + StorageFile file = await StorageFile.GetFileFromPathAsync(pathToFile); + var mediaSource = MediaSource.CreateFromStorageFile(file); + AssignSource(mediaSource, enableGaplessLoop); + } + catch + { + return false; + } + + return true; + } + + private void AssignSource(MediaSource source, bool enableGaplessLoop) + { + _player.Source = enableGaplessLoop + ? LoopEnabledPlaybackList(source) + : source; + } + + private MediaPlaybackList LoopEnabledPlaybackList(MediaSource source) + { + // This code here (combined with a wav source file) allows for gapless playback! + var item = new MediaPlaybackItem(source); + var playbackList = new MediaPlaybackList() { AutoRepeatEnabled = true }; + playbackList.Items.Add(item); + return playbackList; + } + + private void OnPlaybackPositionChanged(MediaPlaybackSession sender, object args) + { + if (sender is null) + { + return; + } + + PositionChanged?.Invoke(sender, sender.Position); + } + + private void OnFadeTimerTick(object sender, ElapsedEventArgs e) + { + if (_fadeCts.IsCancellationRequested) + { + _timer.Stop(); + return; + } + + var currentTicks = e.SignalTime.Ticks - _fadeStart; + double percent = (double)currentTicks / _startEndDiff; + + if (percent >= 1) + { + _timer.Stop(); + _player.Volume = _fadeIn ? _fadeInTargetVolume : 0; + + if (!_fadeIn) + { + _player.Pause(); + _player.Volume = _fadeOutStartingVolume; + + if (_disposeAfterFadeOut) + { + _player.Dispose(); + } + } + } + else + { + _player.Volume = _fadeIn + ? _fadeInTargetVolume * percent + : _fadeOutStartingVolume * (1 - percent); + } + } +} diff --git a/Src/JeniusApps.Common.Uwp/UI/UIExtensions.cs b/Src/JeniusApps.Common.Uwp/UI/UIExtensions.cs new file mode 100644 index 0000000..6411158 --- /dev/null +++ b/Src/JeniusApps.Common.Uwp/UI/UIExtensions.cs @@ -0,0 +1,99 @@ +using Windows.UI.Xaml; + +#nullable enable + +namespace JeniusApps.Common.UI.Uwp +{ + /// + /// Extension methods designed to be used with x:Bind. + /// + public static class UIExtensions + { + /// + /// Inverts the value. + /// + public static bool Not(this bool value) => !value; + + /// + /// Inverts the value and converts the result to visibility. + /// + public static Visibility InvertBoolToVis(this bool value) + { + return value + ? Visibility.Collapsed + : Visibility.Visible; + } + + /// + /// Returns visible if string is null or empty. + /// Collapsed, otherwise. + /// + public static Visibility VisibleIfEmpty(this string s) + { + return string.IsNullOrEmpty(s) + ? Visibility.Visible + : Visibility.Collapsed; + } + + /// + /// Returns collapsed if string is null or empty. + /// Visible, otherwise. + /// + public static Visibility CollapsedIfEmpty(this string s) + { + return string.IsNullOrEmpty(s) + ? Visibility.Collapsed + : Visibility.Visible; + } + + /// + /// Returns collapsed if object is null. + /// Visible, otherwise. + /// + public static Visibility CollapsedIfNull(this object? obj) + { + return obj is null + ? Visibility.Collapsed + : Visibility.Visible; + } + + /// + /// Returns visible if object is null. + /// Collapsed, otherwise. + /// + public static Visibility VisibleIfNull(this object? obj) + { + return obj is null + ? Visibility.Visible + : Visibility.Collapsed; + } + + public static Visibility VisibleIfAll(this bool a, bool b) + { + return a && b + ? Visibility.Visible + : Visibility.Collapsed; + } + + public static Visibility VisibleIfAny(this bool a, bool b) + { + return a || b + ? Visibility.Visible + : Visibility.Collapsed; + } + + public static Visibility CollapsedIfAll(this bool a, bool b) + { + return a && b + ? Visibility.Collapsed + : Visibility.Visible; + } + + public static Visibility CollapsedIfAny(this bool a, bool b) + { + return a || b + ? Visibility.Collapsed + : Visibility.Visible; + } + } +} diff --git a/Src/JeniusApps.Common/Authentication/IMsalClient.cs b/Src/JeniusApps.Common/Authentication/IMsalClient.cs new file mode 100644 index 0000000..dcc7c5c --- /dev/null +++ b/Src/JeniusApps.Common/Authentication/IMsalClient.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; + +namespace JeniusApps.Common.Authentication; + +/// +/// Interface for signing in to a +/// Microsoft account and retrieving a Microsoft Graph token. +/// +public interface IMsalClient +{ + /// + /// Fires when sign in process completes. + /// String is the access token if successful. Null or empty + /// otherwise. + /// + event EventHandler? InteractiveSignInCompleted; + + /// + /// Attempts to sign in silently and retrieve a token + /// for the given scopes. + /// Returns null if silent auth was unsuccessful. + /// + /// A token if sign in was successful, and null if not. + Task GetTokenSilentAsync(string[] scopes); + + /// + /// Attempts to sign in and retrieve at token. User will be prompted. + /// Result will be communicated via . + /// + Task RequestInteractiveSignIn(string[] scopes, string[]? extraScopes = null); + + /// + /// Signs out the user. + /// + Task SignOutAsync(); +} diff --git a/Src/JeniusApps.Common/JeniusApps.Common.csproj b/Src/JeniusApps.Common/JeniusApps.Common.csproj new file mode 100644 index 0000000..0c1efba --- /dev/null +++ b/Src/JeniusApps.Common/JeniusApps.Common.csproj @@ -0,0 +1,41 @@ + + + + latest + netstandard2.0 + JeniusApps.Common + JeniusApps.Common + JeniusApps.Common + true + true + 0.0.41-preview + jeniusapps,apps,common + https://github.com/jenius-apps/common + Daniel Paulino + JeniusApps + © Jenius Apps + + A .NET Standard 2.0 library of common components used to build apps. + true + true + snupkg + true + 12.0 + enable + LICENSE + + + + + True + + + + + + + + + + + diff --git a/Src/JeniusApps.Common/Models/MenuItem.cs b/Src/JeniusApps.Common/Models/MenuItem.cs new file mode 100644 index 0000000..e0aa173 --- /dev/null +++ b/Src/JeniusApps.Common/Models/MenuItem.cs @@ -0,0 +1,78 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace JeniusApps.Common.Models +{ + /// + /// Object that represents the menu items. + /// + public class MenuItem : ObservableObject + { + private bool _isSelected; + + /// + /// Default constructor. + /// + /// The command to trigger upon being selected. + /// The text to display on UI. + /// The glyph code to display on UI. + /// A developer-facing tag to help programmaticaly identify this item. + /// Text to display in a tooltip. + /// Subtitle text to display in a tooltip. + public MenuItem( + IRelayCommand asyncRelayCommand, + string text, + string glyph, + string? tag = null, + string? tooltipText = null, + string? tooltipSubtitle = null) + { + ActionCommand = asyncRelayCommand; + Text = text; + Glyph = glyph; + Tag = tag; + ToolTipText = tooltipText ?? text; + ToolTipSubtitle = tooltipSubtitle ?? string.Empty; + } + + /// + /// String used by developer to help identify this menu item + /// programmatically. Not designed to be shown in UI. + /// + public string? Tag { get; } + + /// + /// Determines if the item is selected. + /// + public bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + + /// + /// The command to trigger upon being selected. + /// + public IRelayCommand ActionCommand { get; } + + /// + /// The menu's text. + /// + public string Text { get; } = string.Empty; + + /// + /// The menu's icon. + /// + public string Glyph { get; } = string.Empty; + + /// + /// Text to display in a tooltip. + /// + public string ToolTipText { get; } = string.Empty; + + /// + /// Subtitle text to display in a tooltip. + /// + public string ToolTipSubtitle { get; } = string.Empty; + } +} diff --git a/Src/JeniusApps.Common/Settings/IUserSettings.cs b/Src/JeniusApps.Common/Settings/IUserSettings.cs new file mode 100644 index 0000000..47f9b02 --- /dev/null +++ b/Src/JeniusApps.Common/Settings/IUserSettings.cs @@ -0,0 +1,64 @@ +using System; +using System.Text.Json.Serialization.Metadata; + +namespace JeniusApps.Common.Settings +{ + /// + /// Interface for storing + /// and retrieving user settings. + /// + public interface IUserSettings + { + /// + /// Raised when a settings is set. + /// String is the key of the setting. + /// + event EventHandler? SettingSet; + + /// + /// Saves settings into persistent local + /// storage. + /// + /// Type of the value. + /// The settings key to use. + /// The value to save. + void Set(string settingKey, T value); + + /// + /// Retrieves the value for the desired settings key. + /// + /// Type of the value. + /// The settings key to use. + /// The desired value or returns the default value. + T? Get(string settingKey); + + /// + /// Retrieves the value for the desired settings key. + /// + /// Type of the value. + /// The settings key to use. + /// The default override to use if the setting has no value. + /// The desired value or returns the default override. + T? Get(string settingKey, T defaultOverride); + + /// + /// Retrieves the value for the desired settings key + /// and performs json deserialization on the stored value. + /// + /// Type of the value. + /// The settings key. + /// The instance to deserialize values. + /// The desired value or returns the default. + T? GetAndDeserialize(string settingKey, JsonTypeInfo jsonTypeInfo); + + /// + /// Saves settings into persistent local storage + /// after serializing the object. + /// + /// Type of the value. + /// The settings key. + /// The value to save. + /// The instance to serialize values. + void SetAndSerialize(string settingKey, T value, JsonTypeInfo jsonTypeInfo); + } +} diff --git a/Src/JeniusApps.Common/Telemetry/AppInsightsTelemetry.cs b/Src/JeniusApps.Common/Telemetry/AppInsightsTelemetry.cs new file mode 100644 index 0000000..b75c4d3 --- /dev/null +++ b/Src/JeniusApps.Common/Telemetry/AppInsightsTelemetry.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; + +namespace JeniusApps.Common.Telemetry; + +/// +/// Class for capturing telemetry using +/// Microsoft Application Insights. +/// +public class AppInsightsTelemetry : ITelemetry +{ + private readonly TelemetryClient _tc; + private bool _isEnabled = true; + + /// + /// Initializes class. + /// + /// The instrumentation key your AppInsights instance. + /// Determines if events will be tracked. + /// Optional context to add to the telemetry client. + public AppInsightsTelemetry( + string apiKey, + bool isEnabled = true, + TelemetryContext? context = null) + { + _isEnabled = isEnabled; + + var configuration = new TelemetryConfiguration + { + ConnectionString = $"InstrumentationKey={apiKey}" + + }; + _tc = new TelemetryClient(configuration); + + if (context is not null) + { + _tc.Context.Component.Version = context.Component.Version; + _tc.Context.Device.Id = context.Device.Id; + _tc.Context.Device.OperatingSystem = context.Device.OperatingSystem; + _tc.Context.Location.Ip = context.Location.Ip; + _tc.Context.Session.Id = context.Session.Id; + _tc.Context.Session.IsFirst = context.Session.IsFirst; + _tc.Context.User.Id = context.User.Id; + _tc.Context.User.AuthenticatedUserId = context.User.AuthenticatedUserId; + + foreach (var property in context.GlobalProperties) + { + _tc.Context.GlobalProperties.Add(property.Key, property.Value); + } + } + } + + /// + public async Task FlushAsync() + { + await _tc.FlushAsync(default).ConfigureAwait(false); + } + + /// + public void SetEnabled(bool isEnabled) => _isEnabled = isEnabled; + + /// + public void TrackError( + Exception e, + IDictionary? properties = null, + IDictionary? metrics = null) + { + _tc.TrackException(e, properties, metrics); + } + + /// + public void TrackEvent(string eventName, + IDictionary? properties = null, + IDictionary? metrics = null) + { + if (!_isEnabled) + { + return; + } + + _tc.TrackEvent(eventName, properties, metrics); + } + + /// + public void TrackPageView(string page) + { + if (!_isEnabled) + { + return; + } + + _tc.TrackPageView(page); + } +} diff --git a/Src/JeniusApps.Common/Telemetry/ITelemetry.cs b/Src/JeniusApps.Common/Telemetry/ITelemetry.cs new file mode 100644 index 0000000..232299f --- /dev/null +++ b/Src/JeniusApps.Common/Telemetry/ITelemetry.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace JeniusApps.Common.Telemetry; + +/// +/// Telemetry interface. +/// +public interface ITelemetry +{ + /// + /// Tracks exceptions. + /// + /// The exception to forward. + /// Optional properties associated with the exception. + /// Optional metrics associated with the exception. + void TrackError( + Exception e, + IDictionary? properties = null, + IDictionary? metrics = null); + + /// + /// Tracks the given event and its properties. + /// + /// Name of event. + /// Optional properties associated with the event. + /// Optional metrics associated with the event. + void TrackEvent( + string eventName, + IDictionary? properties = null, + IDictionary? metrics = null); + + /// + /// Sets if usage telemetry is enabled or not. + /// + /// If true, telemetry is enabled. If falsed, disabled. + void SetEnabled(bool isEnabled); + + /// + /// Used to flush data and to avoid lost telemetry. + /// Recommended to be used when application is shutting down + /// or suspending. + /// + Task FlushAsync(); + + /// + /// Tracks the page view event. + /// + /// Name of the page. + void TrackPageView(string page); +} diff --git a/Src/JeniusApps.Common/Tools/IAppStoreUpdater.cs b/Src/JeniusApps.Common/Tools/IAppStoreUpdater.cs new file mode 100644 index 0000000..edad54c --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IAppStoreUpdater.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; + +namespace JeniusApps.Common.Tools; + +/// +/// Interface for handling app store updates +/// from within your application. +/// +public interface IAppStoreUpdater +{ + /// + /// Raised when an update is detected. + /// + event EventHandler? UpdateAvailable; + + /// + /// Raised when an update is in progress. The double + /// payload is the percent progress of the update. + /// + event EventHandler? ProgressChanged; + + /// + /// Checks if updates exist for the current app. + /// If updates exist, the update information will be cached, + /// but it does not perform the updates. + /// + /// True if updates are available, false otherwise. + public Task CheckForUpdatesAsync(); + + /// + /// Attempts to apply updates that were previously + /// cached using . + /// If the cache is empty, no operation is performed. + /// + public Task TryApplyUpdatesAsync(); + + /// + /// Attempts to silently download and install the update + /// in the background. + /// + /// + /// If the update was downloaded previously, then we skip to the install step. + /// The installation step will restart the app. + /// + /// Returns false if the download was not completed successfully. + Task TrySilentDownloadAndInstallAsync(); + + /// + /// Attemps to silently download the update in the background, but + /// the install step will not be triggered. + /// + /// Returns false if the download was not completed successfully. + Task TrySilentDownloadAsync(); +} diff --git a/Src/JeniusApps.Common/Tools/IAssetsReader.cs b/Src/JeniusApps.Common/Tools/IAssetsReader.cs new file mode 100644 index 0000000..120c132 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IAssetsReader.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace JeniusApps.Common.Tools +{ + /// + /// Reads data from the installed package location. + /// + public interface IAssetsReader + { + /// + /// Reads content from file at given relative path. + /// + /// + /// The case sensitive relative path of the file to read in the installed package location. + /// E.g. 'Assets/data.json' + /// + /// The string content of the given file. + Task ReadFileAsync(string relativePath); + } +} diff --git a/Src/JeniusApps.Common/Tools/IClipboard.cs b/Src/JeniusApps.Common/Tools/IClipboard.cs new file mode 100644 index 0000000..5e32b2c --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IClipboard.cs @@ -0,0 +1,15 @@ +namespace JeniusApps.Common.Tools +{ + /// + /// Interface for clipboard functionality. + /// + public interface IClipboard + { + /// + /// Copies the given text to the system clipboard. + /// + /// The text to copy. + /// True if the copy was successful. False, otherwise. + bool CopyToClipboard(string text); + } +} diff --git a/Src/JeniusApps.Common/Tools/IDispatcherQueue.cs b/Src/JeniusApps.Common/Tools/IDispatcherQueue.cs new file mode 100644 index 0000000..e80c102 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IDispatcherQueue.cs @@ -0,0 +1,18 @@ +using System; + +namespace JeniusApps.Common.Tools +{ + /// + /// Wrapper interface around a dispatcher queue + /// that the client can use to run code + /// on the UI thread. + /// + public interface IDispatcherQueue + { + /// + /// Schedules the given action to be run + /// on the UI thread. + /// + void TryEnqueue(Action action); + } +} diff --git a/Src/JeniusApps.Common/Tools/IExperimentationService.cs b/Src/JeniusApps.Common/Tools/IExperimentationService.cs new file mode 100644 index 0000000..5f8e3bd --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IExperimentationService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace JeniusApps.Common.Tools; + +/// +/// Interface for an experimentation service. +/// +public interface IExperimentationService +{ + /// + /// Returns the local state of all active experiments. + /// + /// + /// A dictionary for each experiment and a bool that represents if they + /// should be enabled or not. + /// + IReadOnlyDictionary GetAllExperiments(); + + /// + /// Determines if the experiment should be enabled. + /// + /// The unique name of the experiment. + /// Returns true if the experiment should be enabled. + bool IsEnabled(string experiment); +} \ No newline at end of file diff --git a/Src/JeniusApps.Common/Tools/ILocalizer.cs b/Src/JeniusApps.Common/Tools/ILocalizer.cs new file mode 100644 index 0000000..8f8c5c2 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/ILocalizer.cs @@ -0,0 +1,22 @@ +namespace JeniusApps.Common.Tools +{ + /// + /// Interfaces for retrieving localized strings. + /// + public interface ILocalizer + { + /// + /// Retrieves the localized string for the given key. + /// + /// The resource key for the string. + string GetString(string key); + + /// + /// Retrieves the localized string for the given key and uses + /// the given parameter to format the localized string. + /// + /// The resource key for the string. + /// The parameter to use to format the localized string. + string GetString(string key, string formatParam); + } +} diff --git a/Src/JeniusApps.Common/Tools/IMediaPlayer.cs b/Src/JeniusApps.Common/Tools/IMediaPlayer.cs new file mode 100644 index 0000000..255f510 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IMediaPlayer.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; + +namespace JeniusApps.Common.Tools; + +/// +/// An abstraction over the system media player. +/// +public interface IMediaPlayer +{ + /// + /// Raised when the position of the active playback item has changed. + /// + event EventHandler PositionChanged; + + /// + /// Media player's volume. + /// + double Volume { get; set; } + + /// + /// Duration of the current playback item. + /// + TimeSpan Duration { get; } + + /// + /// Pauses the media player. + /// + void Pause(); + + /// + /// Plays the media player. + /// + void Play(); + + /// + /// Pauses the media player. + /// + void Pause(double fadeDuration, bool disposeAfterFadeOut = false); + + /// + /// Plays the media player. + /// + void Play(double fadeInTargetVolume, double fadeDuration); + + /// + /// Sets the media player's source using the given file path. + /// + /// Path of the source file. + /// If true, the media source will be configured for gapless playback loop. + /// True if setting the source was successful. False, otherwise. + Task SetSourceAsync(string pathToFile, bool enableGaplessLoop = false); + + /// + /// Sets the media player's source using the given URI. + /// + /// The URI to load into the media player. + /// If true, the media source will be configured for gapless playback loop. + /// True if setting the source was successful. False, otherwise. + bool SetUriSource(Uri uriSource, bool enableGaplessLoop = false); + + /// + /// Releases resources associated with the media player. + /// + void Dispose(); +} diff --git a/Src/JeniusApps.Common/Tools/INavigator.cs b/Src/JeniusApps.Common/Tools/INavigator.cs new file mode 100644 index 0000000..10b9f2c --- /dev/null +++ b/Src/JeniusApps.Common/Tools/INavigator.cs @@ -0,0 +1,79 @@ +using System; + +namespace JeniusApps.Common.Tools +{ + /// + /// Interface that aids with page navigation. + /// + public interface INavigator + { + /// + /// Raised when a navigation was performed successfully. + /// The string payload is the pageKey. + /// + event EventHandler? PageNavigated; + + /// + /// Initializes the frame that will be used for navigation. + /// + /// The frame to navigate. + void SetFrame(object frame); + + /// + /// Sets an optional inner navigator that can be manipulated. + /// + /// The navigator representing an inner content frame. + void SetInnerNavigator(INavigator inner); + + /// + /// Navigates the frame to the given page. + /// + /// The page to naviate to. + /// Optional. An object that will be passed to the destination page. + /// Optional. Specifies the transition animation to use. + void NavigateTo( + string pageKey, + object? navArgs = null, + PageTransition transition = PageTransition.None); + + /// + /// Safely goes back one level in the frame's navigation stack. + /// + /// Optional. The transition to use when navigating back. + /// Optional. If true, a GoBack command will be executed on the inner frame as well. + void GoBack( + PageTransition transition = PageTransition.None, + bool innerFrameGoBack = false); + } + + /// + /// Enums that represent supported page transitions. + /// + public enum PageTransition + { + /// + /// Represents no page transition animation. + /// + None, + + /// + /// Represents a drill in or out transition animation. + /// + Drill, + + /// + /// Represents a slide from bottom transition animation. + /// + SlideFromBottom, + + /// + /// Represents a slide from left transition animation. + /// + SlideFromLeft, + + /// + /// Represents a slide from right transition animation. + /// + SlideFromRight, + } +} diff --git a/Src/JeniusApps.Common/Tools/INavigatorFactory.cs b/Src/JeniusApps.Common/Tools/INavigatorFactory.cs new file mode 100644 index 0000000..5fb947a --- /dev/null +++ b/Src/JeniusApps.Common/Tools/INavigatorFactory.cs @@ -0,0 +1,24 @@ +namespace JeniusApps.Common.Tools +{ + /// + /// Interface for creating INavigator instances. + /// + public interface INavigatorFactory + { + /// + /// Creates a new INavigator instance using the given parameters. + /// + /// Key name for the navigator. Can be used to retrieve the existing instance after it's been created. + /// The parameter to be supplied to the constructor of the INavigator instance if construction is needed. + /// The frame object to be associated with the INavigator instance. Will always override the previous frame object. + /// Returns the newly created INavigator instance. + INavigator GetOrCreate(string navigatorName, object? constructorParameter, object? frame); + + /// + /// Retrieves an INavigator instance with the given navigator key name. + /// + /// The key name of the INavigator to fetch. + /// Returns the requested INavigator instance or null if it doesn't exist. + INavigator? Get(string navigatorName); + } +} diff --git a/Src/JeniusApps.Common/Tools/IStartupService.cs b/Src/JeniusApps.Common/Tools/IStartupService.cs new file mode 100644 index 0000000..00cbd4f --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IStartupService.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; + +namespace JeniusApps.Common.Tools; + +/// +/// Interface for handling startup-related logic. +/// +public interface IStartupService +{ + /// + /// Gets the current state of the startup task. + /// + /// The ID associated with the app's startup. + /// + Task GetStateAsync(string startupTaskId); + + /// + /// Opens system dialog requesting permission to turn on the given startup task. + /// Must be called from the UI thread. + /// + /// The ID associated with the app's startup. + /// True if the state is enabled, false otherwise. + Task TryEnableOnUiThreadAsync(string startupTaskId); +} + +/// +/// State related to the app's startup task. +/// +public enum StartupState +{ + /// + /// Startup is disabled but it can be enabled. + /// + Disabled, + + /// + /// Startup is disabled and must be enabled manually from Windows Settings. + /// + DisabledByUser, + + /// + /// Not allowed due to group policy or the device does not support it. + /// + Disallowed, + + /// + /// Startup is already enabled. + /// + Enabled, +} diff --git a/Src/JeniusApps.Common/Tools/ISystemInfoProvider.cs b/Src/JeniusApps.Common/Tools/ISystemInfoProvider.cs new file mode 100644 index 0000000..03435fb --- /dev/null +++ b/Src/JeniusApps.Common/Tools/ISystemInfoProvider.cs @@ -0,0 +1,55 @@ +using System; + +namespace JeniusApps.Common.Tools +{ + /// + /// Series of methods that retrieve information about the system. + /// + public interface ISystemInfoProvider + { + /// + /// Retrieves the culture name + /// in en-US format. + /// + string GetCulture(); + + /// + /// Returns string representing the device family. + /// + string GetDeviceFamily(); + + /// + /// Returns true if the current + /// session is the first time this app + /// was run since being installed. + /// + bool IsFirstRun(); + + /// + /// Returns true if the app is currently in compact mode. + /// + bool IsCompact(); + + /// + /// Returns true if the system is capable of using + /// the built-in fluent system icons. + /// + bool IsWin11(); + + /// + /// Returns the date time when the app was first used. + /// + /// DateTime when the app was first used. + DateTime FirstUseDate(); + + /// + /// Returns the local folder path for application data. + /// + string LocalFolderPath(); + + /// + /// Returns true if the device's battery saver is active. + /// + bool IsOnBatterySaver(); + } +} diff --git a/Src/JeniusApps.Common/Tools/IToastService.cs b/Src/JeniusApps.Common/Tools/IToastService.cs new file mode 100644 index 0000000..81943c6 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IToastService.cs @@ -0,0 +1,46 @@ +using System; + +namespace JeniusApps.Common.Tools; + +/// +/// Service for sending toasts. +/// +public interface IToastService +{ + /// + /// Clears the scheduled toasts. + /// + void ClearScheduledToasts(); + + /// + /// Pops the toast based on the given time. + /// If no time is provided, the toast will pop immediately. + /// + /// Title of the toast. + /// Message body of the toast. + /// Adds dismiss button if true. + /// The time when the notification will be triggered. + /// Arguments that will be added to the toast. This is passed onto the app foreground when the toast is clicked. + /// A unique ID assigned to the toast that can be used for searching the toast later. + /// URI for an audio file that will be used as the notification sound. + /// If true, the notification will not make a sound. Only used when an audioUri is provided. + /// Defines how long in minutes that the toast will remain active. After expiration, the toast disappears. A value of 0 minutes no expiration. + void SendToast( + string title, + string message, + bool dismissVisible = false, + DateTime? scheduledDateTime = null, + string launchArg = "", + string tag = "", + Uri? audioUri = null, + bool audioSilent = false, + int minutesExpiration = 0); + + /// + /// Determines if the toast exists already. + /// + /// A unique ID assigned to the toast that can be used for searching the toast later. + /// True if the toast already exists. + bool DoesToastExist(string toastTag); + +} diff --git a/Src/JeniusApps.Common/Tools/IUriLauncher.cs b/Src/JeniusApps.Common/Tools/IUriLauncher.cs new file mode 100644 index 0000000..d1236f5 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/IUriLauncher.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace JeniusApps.Common.Tools +{ + /// + /// Interface for launching URIs. + /// + public interface IUriLauncher + { + /// + /// Launches the give URI. + /// + /// The URI to launch. + /// True if launch was successful. False, otherwise. + Task LaunchUriAsync(Uri uri); + } +} diff --git a/Src/JeniusApps.Common/Tools/LocalExperimentationService.cs b/Src/JeniusApps.Common/Tools/LocalExperimentationService.cs new file mode 100644 index 0000000..e746803 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/LocalExperimentationService.cs @@ -0,0 +1,61 @@ +using JeniusApps.Common.Settings; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace JeniusApps.Common.Tools; + +/// +/// Implementation of an experimentation service that uses local settings. +/// +public class LocalExperimentationService : IExperimentationService +{ + private readonly IReadOnlyList _activeExperiments; + private readonly IUserSettings _userSettings; + private readonly Random _rand = new(); + private readonly ConcurrentDictionary _cachedResults = new(); + private readonly object _dictionaryLock = new(); + + /// + /// Constructor. + /// + /// List of active experimentation keys. + /// Required user settings service. + public LocalExperimentationService( + IReadOnlyList activeExperiments, + IUserSettings userSettings) + { + _activeExperiments = activeExperiments; + _userSettings = userSettings; + } + + /// + public IReadOnlyDictionary GetAllExperiments() + { + var result = new Dictionary(); + foreach (var experiment in _activeExperiments) + { + result[experiment] = IsEnabled(experiment); + } + + return result; + } + + /// + public bool IsEnabled(string experiment) + { + lock (_dictionaryLock) + { + if (_cachedResults.TryGetValue(experiment, out bool value)) + { + return value; + } + + var key = $"experimentation-{experiment}"; + var newOrStoredValue = _userSettings.Get(key, _rand.Next(0, 2) == 0); + _cachedResults.TryAdd(experiment, newOrStoredValue); + _userSettings.Set(key, newOrStoredValue); + return newOrStoredValue; + } + } +} diff --git a/Src/JeniusApps.Common/Tools/SystemInfoExtensions.cs b/Src/JeniusApps.Common/Tools/SystemInfoExtensions.cs new file mode 100644 index 0000000..0c0bcf7 --- /dev/null +++ b/Src/JeniusApps.Common/Tools/SystemInfoExtensions.cs @@ -0,0 +1,21 @@ +namespace JeniusApps.Common.Tools; + +/// +/// Extension methods for . +/// +public static class SystemInfoExtensions +{ + /// + /// Returns true if the system is desktop. + /// + /// The to use. + /// True if the system is desktop. + public static bool IsDesktop(this ISystemInfoProvider systemInfoProvider) => systemInfoProvider.GetDeviceFamily() == "Windows.Desktop"; + + /// + /// Returns true if the system is Xbox. + /// + /// The to use. + /// True if the system is Xbox. + public static bool IsXbox(this ISystemInfoProvider systemInfoProvider) => systemInfoProvider.GetDeviceFamily() == "Windows.Xbox"; +} \ No newline at end of file diff --git a/builds/release.yml b/builds/release.yml deleted file mode 100644 index 553acdb..0000000 --- a/builds/release.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Universal Windows Platform -# Build a Universal Windows Platform project using Visual Studio. -# Add steps that test and distribute an app, save build artifacts, and more: -# https://aka.ms/yaml - -trigger: none - -pr: none - -pool: - vmImage: 'windows-2022' - -variables: - - group: beeskie-variables - - name: solution - value: '**/*.sln' - - name: buildPlatform - value: 'x64|ARM64' - - name: buildConfiguration - value: 'Release' - - name: appxPackageDir - value: '$(build.artifactStagingDirectory)\AppxPackages\\' - -steps: - -- task: PowerShell@2 - inputs: - targetType: 'inline' - script: | - [xml]$xmlDoc = Get-Content $(Build.SourcesDirectory)\src\BlueskyClient.Uwp\Package.appxmanifest - $xmlDoc.Package.Identity.Name="$(BeeskieIdentityName)" - $xmlDoc.Save('$(Build.SourcesDirectory)\src\BlueskyClient.Uwp\Package.appxmanifest') - failOnStderr: true - -- task: PowerShell@2 - inputs: - targetType: 'inline' - script: | - [xml]$xmlDoc = Get-Content $(Build.SourcesDirectory)\src\BlueskyClient.Uwp\appsettings.resw - $xmlDoc.SelectSingleNode("//root/data[@name='TelemetryApiKey']").Value = "$(TelemetryApiKey)" - $xmlDoc.Save('$(Build.SourcesDirectory)\src\BlueskyClient.Uwp\appsettings.resw') - failOnStderr: true - -- task: NuGetToolInstaller@1 - -- task: NuGetCommand@2 - inputs: - restoreSolution: '$(solution)' - nugetConfigPath: 'src\nuget.config' - feedsToUse: 'config' - -- task: VSBuild@1 - inputs: - platform: 'x64' - solution: '**/BlueskyClient.Uwp.csproj' - configuration: '$(buildConfiguration)' - msbuildArgs: '/p:AppxBundlePlatforms="$(buildPlatform)" /p:AppxPackageDir="$(appxPackageDir)" /p:AppxBundle=Always /p:UapAppxPackageBuildMode=CI' - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: drop' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' diff --git a/images/logo.png b/images/logo.png index d3f4f4c..ca85368 100644 Binary files a/images/logo.png and b/images/logo.png differ diff --git a/images/storeBadge.png b/images/storeBadge.png deleted file mode 100644 index 4d78d31..0000000 Binary files a/images/storeBadge.png and /dev/null differ diff --git a/privacypolicy.md b/privacypolicy.md deleted file mode 100644 index 3278660..0000000 --- a/privacypolicy.md +++ /dev/null @@ -1,21 +0,0 @@ -# Beeskie - Privacy Policy - -## Information sent to Bluesky - -Beeskie is a third-party client for Bluesky. As such, this app interacts with the APIs provided by Bluesky. For example, in order for Beeskie to display your timeline, we use your provided credentials to talk to Bluesky and retrieve your timeline. When you make a new post or if you `like` something, we send that data to Bluesky via their API. - -Your data is not used in any other way, other than to accomplish operations and interactions with Bluesky. - -## How We Collect and Use Other Information - -### Telemetry -Beeskie collects anonymous, aggregate telemetry. You can view Beeskie's open source repository on GitHub to see all the areas where the app tracks telemetry. Examples include the number of Beeskie is launched per day. This information is not personally identifiable. Telemetry is used to understand what parts of Beeskie is being used or not used to help guide future work. This data is not used for marketing or sales. This data is not sent to any third party other than the telemetry service itself. - -## How to Contact Us -Contact us at jenius_apps@outlook.com if you have any questions regarding this privacy policy. - -## Changes to Our Privacy Policy -The publisher Jenius Apps may modify or update this Privacy Policy from time to time to reflect changes in our app, and so you should review this page periodically. When we change the policy in a material manner we will let you know and update the ‘last modified’ date at the bottom of this page. - ---- -This privacy policy was last modified on **November 19th, 2024**. diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs index c3f03ec..def905a 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.Actor.cs @@ -12,9 +12,9 @@ namespace Bluesky.NET.ApiClients; partial class BlueskyApiClient { - public async Task GetAuthorAsync(string accessToken, string identifier) + public async Task GetAuthorAsync(string accessToken, string handle) { - var timelineUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.ProfilePath}?actor={identifier}"; + var timelineUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.ProfilePath}?actor={handle}"; HttpRequestMessage message = new(HttpMethod.Get, timelineUrl); message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); diff --git a/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs b/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs index 0911423..3ff9cd1 100644 --- a/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs +++ b/src/Bluesky.NET/ApiClients/BlueskyApiClient.cs @@ -23,13 +23,13 @@ public partial class BlueskyApiClient : IBlueskyApiClient } /// - public async Task AuthenticateAsync(string identifer, string appPassword) + public async Task AuthenticateAsync(string userHandle, string appPassword) { var authUrl = $"{UrlConstants.BlueskyBaseUrl}/{UrlConstants.AuthPath}"; var requestBody = new AuthRequestBody { - Identifier = identifer, + Identifier = userHandle, Password = appPassword }; diff --git a/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs b/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs index a4e7a65..d2ab753 100644 --- a/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs +++ b/src/Bluesky.NET/ApiClients/IBlueskyApiClient.cs @@ -11,14 +11,14 @@ public interface IBlueskyApiClient /// Retrieves authenticated tokens that can be used /// for other API calls that require auth. /// - /// The user's handle or email address or DID. + /// The user's handle or email address. /// An app password provided by the user. /// An . - Task AuthenticateAsync(string identifier, string appPassword); + Task AuthenticateAsync(string userHandle, string appPassword); Task> GetTimelineAsync(string accesstoken); Task RefreshAsync(string refreshToken); - Task GetAuthorAsync(string accessToken, string identifier); + Task GetAuthorAsync(string accessToken, string handle); Task> GetNotificationsAsync(string accessToken); Task> GetPostsAsync(string accessToken, IReadOnlyList atUriList); diff --git a/src/Bluesky.NET/Bluesky.NET.csproj b/src/Bluesky.NET/Bluesky.NET.csproj index d7861be..779823a 100644 --- a/src/Bluesky.NET/Bluesky.NET.csproj +++ b/src/Bluesky.NET/Bluesky.NET.csproj @@ -1,8 +1,9 @@  + latest netstandard2.0 - enable + enable diff --git a/src/Bluesky.NET/Models/AuthResponse.cs b/src/Bluesky.NET/Models/AuthResponse.cs index 53dc21c..83683a7 100644 --- a/src/Bluesky.NET/Models/AuthResponse.cs +++ b/src/Bluesky.NET/Models/AuthResponse.cs @@ -2,7 +2,6 @@ public class AuthResponse { - public string? Did { get; init; } public bool Success { get; set; } public string? Handle { get; set; } public string? Email { get; set; } diff --git a/src/BlueskyClient.Uwp/App.Configuration.xaml.cs b/src/BlueskyClient.Uwp/App.Configuration.xaml.cs index f9be405..ea96ca5 100644 --- a/src/BlueskyClient.Uwp/App.Configuration.xaml.cs +++ b/src/BlueskyClient.Uwp/App.Configuration.xaml.cs @@ -17,7 +17,7 @@ using JeniusApps.Common.Tools.Uwp; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Toolkit.Uwp.Helpers; +//using Microsoft.Toolkit.Uwp.Helpers; using System; using System.Collections.Generic; using Windows.Storage; @@ -39,7 +39,8 @@ public static IServiceProvider Services if (serviceProvider is null) { - ThrowHelper.ThrowInvalidOperationException("The service provider is not initialized"); + ThrowHelper.ThrowInvalidOperationException( + "The service provider is not initialized"); } return serviceProvider; @@ -64,7 +65,8 @@ private static IServiceProvider ConfigureServices() return new AppInsightsTelemetry(apiKey, context: context); }); - collection.AddKeyedSingleton(NavigationConstants.RootNavigatorKey, (serviceProvider, key) => + collection.AddKeyedSingleton( + NavigationConstants.RootNavigatorKey, (serviceProvider, key) => { return new Navigator(new Dictionary { @@ -73,7 +75,8 @@ private static IServiceProvider ConfigureServices() }); }); - collection.AddKeyedSingleton(NavigationConstants.ContentNavigatorKey, (serviceProvider, key) => + collection.AddKeyedSingleton( + NavigationConstants.ContentNavigatorKey, (serviceProvider, key) => { return new Navigator(new Dictionary { @@ -87,7 +90,8 @@ private static IServiceProvider ConfigureServices() { return new SignInPageViewModel( serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredKeyedService(NavigationConstants.RootNavigatorKey), + serviceProvider.GetRequiredKeyedService( + NavigationConstants.RootNavigatorKey), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()); }); @@ -97,15 +101,18 @@ private static IServiceProvider ConfigureServices() return new ShellPageViewModel( serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredKeyedService(NavigationConstants.ContentNavigatorKey), - serviceProvider.GetRequiredKeyedService(NavigationConstants.RootNavigatorKey), + serviceProvider.GetRequiredKeyedService( + NavigationConstants.ContentNavigatorKey), + serviceProvider.GetRequiredKeyedService( + NavigationConstants.RootNavigatorKey), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()); }); - collection.AddSingleton(_ => new LocalSettings(UserSettingsConstants.Defaults)); + collection.AddSingleton(_ => + new LocalSettings(UserSettingsConstants.Defaults)); IServiceProvider provider = collection.BuildServiceProvider(); return provider; @@ -138,21 +145,24 @@ private static IServiceProvider ConfigureServices() { var context = new TelemetryContext(); context.Session.Id = Guid.NewGuid().ToString(); - context.Component.Version = SystemInformation.Instance.ApplicationVersion.ToFormattedString(); - context.GlobalProperties.Add("isFirstRun", SystemInformation.Instance.IsFirstRun.ToString()); + context.Component.Version = "0.6"; + //SystemInformation.Instance.ApplicationVersion.ToFormattedString(); + context.GlobalProperties.Add("isFirstRun", + /*SystemInformation.Instance.IsFirstRun.ToString()*/"true"); - if (ApplicationData.Current.LocalSettings.Values[UserSettingsConstants.LocalUserIdKey] is string { Length: > 0 } id) + if (ApplicationData.Current.LocalSettings + .Values[UserSettingsConstants.LocalUserIdKey] is string { Length: > 0 } id) { context.User.Id = id; } else { string userId = Guid.NewGuid().ToString(); - ApplicationData.Current.LocalSettings.Values[UserSettingsConstants.LocalUserIdKey] = userId; + ApplicationData.Current.LocalSettings + .Values[UserSettingsConstants.LocalUserIdKey] = userId; context.User.Id = userId; } - // Ref: https://learn.microsoft.com/en-us/answers/questions/1563897/uwp-and-winui-how-to-check-my-os-version-through-c ulong version = ulong.Parse(AnalyticsInfo.VersionInfo.DeviceFamilyVersion); ulong major = (version & 0xFFFF000000000000L) >> 48; ulong minor = (version & 0x0000FFFF00000000L) >> 32; diff --git a/src/BlueskyClient.Uwp/App.xaml.cs b/src/BlueskyClient.Uwp/App.xaml.cs index 96559c6..477efc4 100644 --- a/src/BlueskyClient.Uwp/App.xaml.cs +++ b/src/BlueskyClient.Uwp/App.xaml.cs @@ -60,9 +60,9 @@ private Task ActivateAsync(LaunchActivatedEventArgs args) if (rootFrame.Content is null) { - var storedHandle = Services.GetRequiredService().Get(UserSettingsConstants.SignedInDIDKey); + var storedHandle = Services.GetRequiredService().Get(UserSettingsConstants.LastUsedUserHandleKey); - if (string.IsNullOrEmpty(storedHandle)) + if (string.IsNullOrEmpty(storedHandle) || storedHandle?.Contains("@") is true) { rootFrame.Navigate(typeof(SignInPage)); } diff --git a/src/BlueskyClient.Uwp/BlueskyClient.Uwp.csproj b/src/BlueskyClient.Uwp/BlueskyClient.Uwp.csproj index 0a122e2..7b0d09a 100644 --- a/src/BlueskyClient.Uwp/BlueskyClient.Uwp.csproj +++ b/src/BlueskyClient.Uwp/BlueskyClient.Uwp.csproj @@ -2,6 +2,7 @@ + latest Debug x86 {2F0DEF6B-3D93-4DB8-905B-19E3983A0BBD} @@ -12,7 +13,7 @@ en-US UAP 10.0.22621.0 - 10.0.17763.0 + 10.0.17134.0 14 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} @@ -232,23 +233,11 @@ 0.0.1 - - 0.0.1 - - - 8.1.240916 - - - 8.1.240916 - - - 0.0.41-preview - 6.2.14 - 2.8.6 + 2.7.0 @@ -306,6 +295,14 @@ {1ef2e316-2ad5-434f-b6bb-5cfa8013b874} BlueskyClient + + {3354de16-cd32-4c68-8268-297fa1cdc0e5} + JeniusApps.Common.Uwp + + + {8edc679c-837d-50fc-2f4a-4322078ac527} + JeniusApps.Common + diff --git a/src/BlueskyClient.Uwp/Controls/PostEmbeds.xaml b/src/BlueskyClient.Uwp/Controls/PostEmbeds.xaml index aee9e1a..a8808b2 100644 --- a/src/BlueskyClient.Uwp/Controls/PostEmbeds.xaml +++ b/src/BlueskyClient.Uwp/Controls/PostEmbeds.xaml @@ -45,7 +45,7 @@ Padding="0" x:Load="{x:Bind IsExternalUrl}" Click="OnExternalUrlClicked"> - + + Version="0.6.7.0" /> - + Beeskie @@ -45,11 +45,16 @@ + + + + + \ No newline at end of file diff --git a/src/BlueskyClient.Uwp/Tools/SecureCredentialStorage.cs b/src/BlueskyClient.Uwp/Tools/SecureCredentialStorage.cs index 7207fda..408f1a5 100644 --- a/src/BlueskyClient.Uwp/Tools/SecureCredentialStorage.cs +++ b/src/BlueskyClient.Uwp/Tools/SecureCredentialStorage.cs @@ -24,11 +24,6 @@ public bool SetCredential(string key, string credential) public string? GetCredential(string key) { - if (string.IsNullOrEmpty(key)) - { - return null; - } - try { PasswordVault vault = new(); diff --git a/src/BlueskyClient.Uwp/Views/ShellPage.xaml b/src/BlueskyClient.Uwp/Views/ShellPage.xaml index c17c83a..e23e366 100644 --- a/src/BlueskyClient.Uwp/Views/ShellPage.xaml +++ b/src/BlueskyClient.Uwp/Views/ShellPage.xaml @@ -5,6 +5,7 @@ xmlns:apiModels="using:Bluesky.NET.Models" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ex="using:JeniusApps.Common.UI.Uwp" + xmlns:local="using:BlueskyClient.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="using:JeniusApps.Common.Models" xmlns:winui="using:Microsoft.UI.Xaml.Controls" @@ -77,7 +78,7 @@ + ProfilePicture="{x:Bind ViewModel.CurrentUser.Avatar, Mode=OneWay}" /> $"Beeskie {SystemInformation.Instance.ApplicationVersion.ToFormattedString().TrimEnd('0').TrimEnd('.')}"; + public string DisplayTitle + { + get + { + return $"Beeskie 0.6"; + //{SystemInformation.Instance.ApplicationVersion.ToFormattedString() + // .TrimEnd('0').TrimEnd('.')}"; + } + } protected async override void OnNavigatedTo(NavigationEventArgs e) { App.Services.GetRequiredService().TrackPageView(nameof(ShellPage)); App.Services.GetRequiredKeyedService(NavigationConstants.ContentNavigatorKey).SetFrame(ContentFrame); - await ViewModel.InitializeAsync(e.Parameter as ShellPageNavigationArgs ?? new()).ConfigureAwait(false); + await ViewModel.InitializeAsync(e.Parameter as string).ConfigureAwait(false); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/BlueskyClient.sln b/src/BlueskyClient.sln index 9cba762..cd2145f 100644 --- a/src/BlueskyClient.sln +++ b/src/BlueskyClient.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueskyClient.Uwp", "Bluesk EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bluesky.NET", "Bluesky.NET\Bluesky.NET.csproj", "{0C16ABBF-64F4-4880-9DFD-5EC48247C0B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JeniusApps.Common", "JeniusApps.Common\JeniusApps.Common.csproj", "{8EDC679C-837D-50FC-2F4A-4322078AC527}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JeniusApps.Common.Uwp", "JeniusApps.Common.Uwp\JeniusApps.Common.Uwp.csproj", "{3354DE16-CD32-4C68-8268-297FA1CDC0E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,6 +97,46 @@ Global {0C16ABBF-64F4-4880-9DFD-5EC48247C0B2}.Release|x64.Build.0 = Release|Any CPU {0C16ABBF-64F4-4880-9DFD-5EC48247C0B2}.Release|x86.ActiveCfg = Release|Any CPU {0C16ABBF-64F4-4880-9DFD-5EC48247C0B2}.Release|x86.Build.0 = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|ARM.ActiveCfg = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|ARM.Build.0 = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|ARM64.Build.0 = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|x64.ActiveCfg = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|x64.Build.0 = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|x86.ActiveCfg = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Debug|x86.Build.0 = Debug|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|Any CPU.Build.0 = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|ARM.ActiveCfg = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|ARM.Build.0 = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|ARM64.ActiveCfg = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|ARM64.Build.0 = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|x64.ActiveCfg = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|x64.Build.0 = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|x86.ActiveCfg = Release|Any CPU + {8EDC679C-837D-50FC-2F4A-4322078AC527}.Release|x86.Build.0 = Release|Any CPU + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|ARM.ActiveCfg = Debug|ARM + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|ARM.Build.0 = Debug|ARM + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|ARM64.Build.0 = Debug|ARM64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|x64.ActiveCfg = Debug|x64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|x64.Build.0 = Debug|x64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|x86.ActiveCfg = Debug|x86 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Debug|x86.Build.0 = Debug|x86 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|Any CPU.Build.0 = Release|Any CPU + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|ARM.ActiveCfg = Release|ARM + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|ARM.Build.0 = Release|ARM + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|ARM64.ActiveCfg = Release|ARM64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|ARM64.Build.0 = Release|ARM64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|x64.ActiveCfg = Release|x64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|x64.Build.0 = Release|x64 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|x86.ActiveCfg = Release|x86 + {3354DE16-CD32-4C68-8268-297FA1CDC0E5}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/BlueskyClient/BlueskyClient.csproj b/src/BlueskyClient/BlueskyClient.csproj index 0f55818..94262ce 100644 --- a/src/BlueskyClient/BlueskyClient.csproj +++ b/src/BlueskyClient/BlueskyClient.csproj @@ -1,14 +1,14 @@  + latest netstandard2.0 enable - - - + + all @@ -19,6 +19,7 @@ + diff --git a/src/BlueskyClient/Caches/ProfileCache.cs b/src/BlueskyClient/Caches/ProfileCache.cs index 48e089d..642fea8 100644 --- a/src/BlueskyClient/Caches/ProfileCache.cs +++ b/src/BlueskyClient/Caches/ProfileCache.cs @@ -28,9 +28,9 @@ public ProfileCache( _telemetry = telemetry; } - public async Task GetItemAsync(string identifier) + public async Task GetItemAsync(string handle) { - if (_cache.TryGetValue(identifier, out CachedItem cachedResult) && + if (_cache.TryGetValue(handle, out CachedItem cachedResult) && DateTime.Now < cachedResult.ExpirationTime) { return cachedResult.Data; @@ -49,7 +49,7 @@ public ProfileCache( try { - author = await _apiClient.GetAuthorAsync(accessToken, identifier); + author = await _apiClient.GetAuthorAsync(accessToken, handle); } catch (Exception e) { @@ -73,7 +73,7 @@ public ProfileCache( ExpirationTime = DateTime.Now.AddHours(UrlConstants.OnlineDataHoursToLive) }; - _cache.AddOrUpdate(identifier, newCachedItem, (key, item) => newCachedItem); + _cache.AddOrUpdate(handle, newCachedItem, (key, item) => newCachedItem); return author; } diff --git a/src/BlueskyClient/Constants/UserSettingsConstants.cs b/src/BlueskyClient/Constants/UserSettingsConstants.cs index 22eec05..09928a8 100644 --- a/src/BlueskyClient/Constants/UserSettingsConstants.cs +++ b/src/BlueskyClient/Constants/UserSettingsConstants.cs @@ -5,14 +5,9 @@ namespace BlueskyClient.Constants; public sealed class UserSettingsConstants { /// - /// Remembers the signed-in DID identifier. + /// Remembers the last user handle that signed in. /// - public const string SignedInDIDKey = "SignedInDID"; - - /// - /// Remembers the handle or email used by the user to sign in. - /// - public const string LastUsedUserIdentifierInputKey = "LastUsedUserIdentifierInputKey"; + public const string LastUsedUserHandleKey = "LastUsedUserHandle"; /// /// Anonymous ID referencing the local user, for telemetry purposes. @@ -24,7 +19,6 @@ public sealed class UserSettingsConstants /// public static IReadOnlyDictionary Defaults { get; } = new Dictionary() { - { LastUsedUserIdentifierInputKey, string.Empty }, - { SignedInDIDKey, string.Empty }, + { LastUsedUserHandleKey, string.Empty }, }; } diff --git a/src/BlueskyClient/Models/ShellPageNavigationArgs.cs b/src/BlueskyClient/Models/ShellPageNavigationArgs.cs deleted file mode 100644 index 05456d2..0000000 --- a/src/BlueskyClient/Models/ShellPageNavigationArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace BlueskyClient.Models; - -public class ShellPageNavigationArgs -{ - public bool AlreadySignedIn { get; init; } -} diff --git a/src/BlueskyClient/Services/AuthenticationService.cs b/src/BlueskyClient/Services/AuthenticationService.cs index b4745b8..db20ab6 100644 --- a/src/BlueskyClient/Services/AuthenticationService.cs +++ b/src/BlueskyClient/Services/AuthenticationService.cs @@ -1,8 +1,6 @@ using Bluesky.NET.ApiClients; using Bluesky.NET.Models; -using BlueskyClient.Constants; using BlueskyClient.Tools; -using JeniusApps.Common.Settings; using System; using System.Threading.Tasks; @@ -13,59 +11,63 @@ public sealed class AuthenticationService : IAuthenticationService private const int TokenHoursToLive = 2; // based on decoded JWT, TTL is 2 hours. private readonly IBlueskyApiClient _apiClient; private readonly ISecureCredentialStorage _secureCredentialStorage; - private readonly IUserSettings _userSettings; private string? _accesToken; private string? _refreshToken; private DateTime? _expirationTime; + private string? _signedInUsername; public AuthenticationService( IBlueskyApiClient blueskyApiClient, - ISecureCredentialStorage secureCredentialStorage, - IUserSettings userSettings) + ISecureCredentialStorage secureCredentialStorage) { _apiClient = blueskyApiClient; _secureCredentialStorage = secureCredentialStorage; - _userSettings = userSettings; } /// - public async Task<(bool, string)> TrySilentSignInAsync() + public async Task<(bool, string)> TrySilentSignInAsync(string storedUserHandle) { #if DEBUG //return (false, "debugReturnFalse"); #endif - string? storedUserHandle = _userSettings.Get(UserSettingsConstants.SignedInDIDKey) ?? string.Empty; - string? storedRefreshToken = _secureCredentialStorage.GetCredential(storedUserHandle); - if (storedRefreshToken is not { Length: > 0 }) + + var storedRefreshToken = _secureCredentialStorage.GetCredential(storedUserHandle); + if (storedRefreshToken is not { Length: > 0 } oldRefreshToken) { return (false, "emptyStoredRefreshToken"); } - var authResponse = await _apiClient.RefreshAsync(storedRefreshToken); - UpdateStoredToken(authResponse); + var authResponse = await _apiClient.RefreshAsync(oldRefreshToken); + if (authResponse?.Success is true) + { + _signedInUsername = storedUserHandle; + } + + UpdateStoredToken(storedUserHandle, authResponse); return (authResponse?.Success is true, authResponse?.ErrorMessage ?? string.Empty); } /// - public async Task SignInAsync(string rawUserHandleOrEmail, string rawPassword) + public async Task SignInAsync(string rawUserHandle, string rawPassword) { - var userHandleOrEmail = rawUserHandleOrEmail.Trim().TrimStart('@'); + var handle = rawUserHandle.Trim(); var password = rawPassword.Trim(); - if (string.IsNullOrEmpty(userHandleOrEmail) || string.IsNullOrEmpty(password)) + if (string.IsNullOrEmpty(handle) || string.IsNullOrEmpty(password)) { return null; } - var result = await _apiClient.AuthenticateAsync(userHandleOrEmail, password); - UpdateStoredToken(result); - if (result is { Success: true, Did: string { Length: > 0 } did }) + var result = await _apiClient.AuthenticateAsync(handle, password); + if (result?.Success is true) { - _userSettings.Set(UserSettingsConstants.SignedInDIDKey, did); + _signedInUsername = handle; } + UpdateStoredToken(handle, result); + return result; } @@ -80,7 +82,7 @@ public AuthenticationService( if (DateTime.Now >= _expirationTime.Value && _refreshToken is string refreshToken) { var authResponse = await _apiClient.RefreshAsync(refreshToken); - UpdateStoredToken(authResponse); + UpdateStoredToken(_signedInUsername, authResponse); } if (DateTime.Now < _expirationTime.Value && _accesToken is string token) @@ -92,18 +94,18 @@ public AuthenticationService( return null; } - private void UpdateStoredToken(AuthResponse? response) + private void UpdateStoredToken(string? userHandle, AuthResponse? response) { - if (response is { - Success: true, - Did: string { Length: > 0 } did, - AccessJwt: string { Length: > 0 } accessToken, - RefreshJwt: string { Length: > 0 } refreshToken }) + if (response is { Success: true, AccessJwt: string { Length: > 0 } accessToken, RefreshJwt: string { Length: > 0 } refreshToken }) { _accesToken = accessToken; _refreshToken = refreshToken; _expirationTime = DateTime.Now.AddHours(TokenHoursToLive); - _secureCredentialStorage.SetCredential(did, refreshToken); + + if (userHandle is not null) + { + _secureCredentialStorage.SetCredential(userHandle, refreshToken); + } } } } diff --git a/src/BlueskyClient/Services/IAuthenticationService.cs b/src/BlueskyClient/Services/IAuthenticationService.cs index 2fbcffe..cd239ca 100644 --- a/src/BlueskyClient/Services/IAuthenticationService.cs +++ b/src/BlueskyClient/Services/IAuthenticationService.cs @@ -7,5 +7,5 @@ public interface IAuthenticationService { Task SignInAsync(string rawUserHandle, string rawPassword); Task TryGetFreshTokenAsync(); - Task<(bool, string)> TrySilentSignInAsync(); + Task<(bool, string)> TrySilentSignInAsync(string storedUserHandle); } \ No newline at end of file diff --git a/src/BlueskyClient/Services/PostSubmissionService.cs b/src/BlueskyClient/Services/PostSubmissionService.cs index 1f4647c..411a5eb 100644 --- a/src/BlueskyClient/Services/PostSubmissionService.cs +++ b/src/BlueskyClient/Services/PostSubmissionService.cs @@ -41,7 +41,7 @@ public PostSubmissionService( } var token = await _authenticationService.TryGetFreshTokenAsync(); - var handle = _userSettings.Get(UserSettingsConstants.SignedInDIDKey); + var handle = _userSettings.Get(UserSettingsConstants.LastUsedUserHandleKey); if (token is null || handle is null) { @@ -109,7 +109,7 @@ public async Task LikeOrRepostAsync(RecordType recordType, string targetUr } var token = await _authenticationService.TryGetFreshTokenAsync(); - var handle = _userSettings.Get(UserSettingsConstants.SignedInDIDKey); + var handle = _userSettings.Get(UserSettingsConstants.LastUsedUserHandleKey); if (token is null || handle is null) { @@ -162,7 +162,7 @@ public async Task LikeOrRepostAsync(RecordType recordType, string targetUr } var token = await _authenticationService.TryGetFreshTokenAsync(); - var handle = _userSettings.Get(UserSettingsConstants.SignedInDIDKey); + var handle = _userSettings.Get(UserSettingsConstants.LastUsedUserHandleKey); if (token is null || handle is null) { diff --git a/src/BlueskyClient/Services/ProfileService.cs b/src/BlueskyClient/Services/ProfileService.cs index a4ed2f3..d4ce5ed 100644 --- a/src/BlueskyClient/Services/ProfileService.cs +++ b/src/BlueskyClient/Services/ProfileService.cs @@ -3,7 +3,9 @@ using BlueskyClient.Caches; using BlueskyClient.Constants; using JeniusApps.Common.Settings; +using System; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; namespace BlueskyClient.Services; @@ -29,13 +31,13 @@ public ProfileService( public async Task GetCurrentUserAsync() { - string? identifier = _userSettings.Get(UserSettingsConstants.SignedInDIDKey); - if (identifier is null) + var handle = _userSettings.Get(UserSettingsConstants.LastUsedUserHandleKey); + if (handle is null) { return null; } - return await _profileCache.GetItemAsync(identifier); + return await _profileCache.GetItemAsync(handle); } public async Task> GetProfileFeedAsync(string handle) diff --git a/src/BlueskyClient/ViewModels/ShellPageViewModel.cs b/src/BlueskyClient/ViewModels/ShellPageViewModel.cs index 3cec82d..c101ac8 100644 --- a/src/BlueskyClient/ViewModels/ShellPageViewModel.cs +++ b/src/BlueskyClient/ViewModels/ShellPageViewModel.cs @@ -1,7 +1,5 @@ using Bluesky.NET.Models; using BlueskyClient.Constants; -using BlueskyClient.Extensions; -using BlueskyClient.Models; using BlueskyClient.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -63,39 +61,37 @@ public ShellPageViewModel( private int _imageViewerIndex; [ObservableProperty] - [NotifyPropertyChangedFor(nameof(SafeAvatarUrl))] private Author? _currentUser; - public string SafeAvatarUrl => CurrentUser.SafeAvatarUrl(); - - public async Task InitializeAsync(ShellPageNavigationArgs args) + public async Task InitializeAsync(string? rawStoredHandle = null) { - bool shouldAbortToSignInPage; + rawStoredHandle ??= _userSettings.Get(UserSettingsConstants.LastUsedUserHandleKey); - if (args.AlreadySignedIn) - { - string? testToken = await _authenticationService.TryGetFreshTokenAsync(); - shouldAbortToSignInPage = string.IsNullOrEmpty(testToken); - } - else + if (rawStoredHandle is not { Length: > 0 } storedHandle) { - (bool signInSuccessful, string errorMessage) = await _authenticationService.TrySilentSignInAsync(); - shouldAbortToSignInPage = !signInSuccessful; - - if (signInSuccessful) + _telemetry.TrackEvent(TelemetryConstants.AuthFailFromShellPage, new Dictionary { - _telemetry.TrackEvent(TelemetryConstants.AuthSuccessFromShellPage); - } + { "errorMessage", "emptyStoredHandle" } + }); + await _dialogService.OpenSignInRequiredAsync(); + _rootNavigator.NavigateTo(NavigationConstants.SignInPage); + return; } - if (shouldAbortToSignInPage) + (bool signInSuccessful, string errorMessage) = await _authenticationService.TrySilentSignInAsync(storedHandle); + if (!signInSuccessful) { - _telemetry.TrackEvent(TelemetryConstants.AuthFailFromShellPage); + _telemetry.TrackEvent(TelemetryConstants.AuthFailFromShellPage, new Dictionary + { + { "errorMessage", errorMessage } + }); await _dialogService.OpenSignInRequiredAsync(); _rootNavigator.NavigateTo(NavigationConstants.SignInPage); return; } + _telemetry.TrackEvent(TelemetryConstants.AuthSuccessFromShellPage); + _imageViewerService.ImageViewerRequested += OnImageViewerRequested; Task profileTask = _profileService.GetCurrentUserAsync(); diff --git a/src/BlueskyClient/ViewModels/SignInPageViewModel.cs b/src/BlueskyClient/ViewModels/SignInPageViewModel.cs index 3a45012..e612d24 100644 --- a/src/BlueskyClient/ViewModels/SignInPageViewModel.cs +++ b/src/BlueskyClient/ViewModels/SignInPageViewModel.cs @@ -1,5 +1,4 @@ using BlueskyClient.Constants; -using BlueskyClient.Models; using BlueskyClient.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -29,7 +28,7 @@ public SignInPageViewModel( _userSettings = userSettings; _telemetry = telemetry; - UserHandleInput = userSettings.Get(UserSettingsConstants.LastUsedUserIdentifierInputKey) ?? string.Empty; + UserHandleInput = userSettings.Get(UserSettingsConstants.LastUsedUserHandleKey) ?? string.Empty; } [ObservableProperty] @@ -63,14 +62,9 @@ private async Task SignInAsync() ? string.Empty : result?.ErrorMessage ?? "Null response"; - if (result?.Success is true) + if (result is { Success: true, Handle: string { Length: > 0 } handle }) { - _userSettings.Set(UserSettingsConstants.LastUsedUserIdentifierInputKey, UserHandleInput); - - _navigator.NavigateTo(NavigationConstants.ShellPage, new ShellPageNavigationArgs - { - AlreadySignedIn = true - }); + OnSuccessfulSignIn(handle); } SigningIn = false; @@ -84,7 +78,13 @@ private async Task SignInAsync() }); } - private void OnSuccessfulSignIn() + private void OnSuccessfulSignIn(string userHandle) { + if (userHandle.Trim() is { Length: > 0 } handle) + { + _userSettings.Set(UserSettingsConstants.LastUsedUserHandleKey, handle); + } + + _navigator.NavigateTo(NavigationConstants.ShellPage); } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props deleted file mode 100644 index 9c1e33a..0000000 --- a/src/Directory.Build.props +++ /dev/null @@ -1,21 +0,0 @@ - - - 12.0 - - - 7 - - - 7-all - - - strict - - \ No newline at end of file diff --git a/src/nuget.config b/src/nuget.config deleted file mode 100644 index ca070b8..0000000 --- a/src/nuget.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - -