Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feat/1246-allow-re…
Browse files Browse the repository at this point in the history
…ordering-example-sentences
  • Loading branch information
myieye committed Jan 13, 2025
2 parents 3c62972 + 7595e0b commit b591027
Show file tree
Hide file tree
Showing 31 changed files with 2,568 additions and 63 deletions.
33 changes: 23 additions & 10 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ internal void CompleteExemplars(WritingSystems writingSystems)
}
}

public Task<WritingSystem> CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem)
public async Task<WritingSystem> CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem)
{
await validators.ValidateAndThrow(writingSystem);
var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems;
if (exitingWs.Any(ws => ws.Id == writingSystem.WsId))
{
Expand Down Expand Up @@ -194,7 +195,7 @@ public Task<WritingSystem> CreateWritingSystem(WritingSystemType type, WritingSy
WritingSystemType.Vernacular => WritingSystemContainer.CurrentVernacularWritingSystems.Count,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
} - 1;
return Task.FromResult(FromLcmWritingSystem(ws, index, type));
return FromLcmWritingSystem(ws, index, type);
}

public async Task<WritingSystem> UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput<WritingSystem> update)
Expand All @@ -220,6 +221,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",

public async Task<WritingSystem> UpdateWritingSystem(WritingSystem before, WritingSystem after)
{
await validators.ValidateAndThrow(after);
await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
"Revert WritingSystem",
async () =>
Expand All @@ -246,8 +248,9 @@ public IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech()
? FromLcmPartOfSpeech(partOfSpeech) : null);
}

public Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)
public async Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)
{
await validators.ValidateAndThrow(partOfSpeech);
IPartOfSpeech? lcmPartOfSpeech = null;
if (partOfSpeech.Id == default) partOfSpeech.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Part of Speech",
Expand All @@ -259,7 +262,7 @@ public Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)
.Create(partOfSpeech.Id, Cache.LangProject.PartsOfSpeechOA);
UpdateLcmMultiString(lcmPartOfSpeech.Name, partOfSpeech.Name);
});
return Task.FromResult(FromLcmPartOfSpeech(lcmPartOfSpeech ?? throw new InvalidOperationException("Part of speech was not created")));
return FromLcmPartOfSpeech(lcmPartOfSpeech ?? throw new InvalidOperationException("Part of speech was not created"));
}

public Task<PartOfSpeech> UpdatePartOfSpeech(Guid id, UpdateObjectInput<PartOfSpeech> update)
Expand All @@ -278,6 +281,7 @@ public Task<PartOfSpeech> UpdatePartOfSpeech(Guid id, UpdateObjectInput<PartOfSp

public async Task<PartOfSpeech> UpdatePartOfSpeech(PartOfSpeech before, PartOfSpeech after)
{
await validators.ValidateAndThrow(after);
await PartOfSpeechSync.Sync(before, after, this);
return await GetPartOfSpeech(after.Id) ?? throw new NullReferenceException($"unable to find part of speech with id {after.Id}");
}
Expand All @@ -301,7 +305,7 @@ internal SemanticDomain FromLcmSemanticDomain(ICmSemanticDomain semanticDomain)
Id = semanticDomain.Guid,
Name = FromLcmMultiString(semanticDomain.Name),
Code = semanticDomain.Abbreviation.UiString ?? "",
Predefined = true, // TODO: Look up in a GUID list of predefined data
Predefined = CanonicalGuidsSemanticDomain.CanonicalSemDomGuids.Contains(semanticDomain.Guid),
};
}

Expand All @@ -323,6 +327,7 @@ public IAsyncEnumerable<SemanticDomain> GetSemanticDomains()

public async Task<SemanticDomain> CreateSemanticDomain(SemanticDomain semanticDomain)
{
await validators.ValidateAndThrow(semanticDomain);
if (semanticDomain.Id == Guid.Empty) semanticDomain.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Semantic Domain",
"Remove semantic domain",
Expand Down Expand Up @@ -355,6 +360,7 @@ public Task<SemanticDomain> UpdateSemanticDomain(Guid id, UpdateObjectInput<Sema

public async Task<SemanticDomain> UpdateSemanticDomain(SemanticDomain before, SemanticDomain after)
{
await validators.ValidateAndThrow(after);
await SemanticDomainSync.Sync(before, after, this);
return await GetSemanticDomain(after.Id) ?? throw new NullReferenceException($"unable to find semantic domain with id {after.Id}");
}
Expand Down Expand Up @@ -429,6 +435,7 @@ public Task<ComplexFormType> UpdateComplexFormType(Guid id, UpdateObjectInput<Co

public async Task<ComplexFormType> UpdateComplexFormType(ComplexFormType before, ComplexFormType after)
{
await validators.ValidateAndThrow(after);
await ComplexFormTypeSync.Sync(before, after, this);
return ToComplexFormType(ComplexFormTypesFlattened.Single(c => c.Guid == after.Id));
}
Expand Down Expand Up @@ -460,7 +467,7 @@ private PartOfSpeech FromLcmPartOfSpeech(IPartOfSpeech lcmPos)
Id = lcmPos.Guid,
Name = FromLcmMultiString(lcmPos.Name),
// TODO: Abreviation = FromLcmMultiString(partOfSpeech.Abreviation),
Predefined = true, // NOTE: the !string.IsNullOrEmpty(lcmPos.CatalogSourceId) check doesn't work if the PoS originated in CRDT
Predefined = CanonicalGuidsPartOfSpeech.CanonicalPosGuids.Contains(lcmPos.Guid),
};
}

Expand Down Expand Up @@ -660,6 +667,7 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
public async Task<Entry> CreateEntry(Entry entry)
{
entry.Id = entry.Id == default ? Guid.NewGuid() : entry.Id;
await validators.ValidateAndThrow(entry);
try
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Entry",
Expand Down Expand Up @@ -852,6 +860,7 @@ public Task<Entry> UpdateEntry(Guid id, UpdateObjectInput<Entry> update)

public async Task<Entry> UpdateEntry(Entry before, Entry after)
{
await validators.ValidateAndThrow(after);
await Cache.DoUsingNewOrCurrentUOW("Update Entry",
"Revert entry",
async () =>
Expand Down Expand Up @@ -981,16 +990,17 @@ private void ApplySenseToLexSense(Sense sense, ILexSense lexSense)
return Task.FromResult(lcmSense is null ? null : FromLexSense(lcmSense));
}

public Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
await validators.ValidateAndThrow(sense);
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Sense",
"Remove sense",
Cache.ServiceLocator.ActionHandler,
() => CreateSense(lexEntry, sense, between));
return Task.FromResult(FromLexSense(SenseRepository.GetObject(sense.Id)));
return FromLexSense(SenseRepository.GetObject(sense.Id));
}

public Task<Sense> UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sense> update)
Expand All @@ -1010,6 +1020,7 @@ public Task<Sense> UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sen

public async Task<Sense> UpdateSense(Guid entryId, Sense before, Sense after)
{
await validators.ValidateAndThrow(after);
await Cache.DoUsingNewOrCurrentUOW("Update Sense",
"Revert Sense",
async () =>
Expand Down Expand Up @@ -1092,16 +1103,17 @@ internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleS
lexExampleSentence.Reference.get_WritingSystem(0));
}

public Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)
public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)
{
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
throw new InvalidOperationException("Sense not found");
await validators.ValidateAndThrow(exampleSentence);
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Example Sentence",
"Remove example sentence",
Cache.ServiceLocator.ActionHandler,
() => CreateExampleSentence(lexSense, exampleSentence, between));
return Task.FromResult(FromLexExampleSentence(senseId, ExampleSentenceRepository.GetObject(exampleSentence.Id)));
return FromLexExampleSentence(senseId, ExampleSentenceRepository.GetObject(exampleSentence.Id));
}

public Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
Expand All @@ -1127,6 +1139,7 @@ public async Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
ExampleSentence before,
ExampleSentence after)
{
await validators.ValidateAndThrow(after);
await Cache.DoUsingNewOrCurrentUOW("Update Example Sentence",
"Revert Example Sentence",
async () =>
Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/FwLiteMaui/FwLiteMaui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
<PackageReference Include="System.Collections" Version="4.3.0" />
<PackageReference Include="System.IO" Version="4.3.0" />
Expand Down
5 changes: 5 additions & 0 deletions backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics;
using System.Reflection;
using FwLiteShared;
using FwLiteShared.Auth;
using LcmCrdt;
Expand Down Expand Up @@ -78,6 +80,9 @@ public static void AddFwLiteMauiServices(this IServiceCollection services,
});

var defaultDataPath = IsPortableApp ? Directory.GetCurrentDirectory() : FileSystem.AppDataDirectory;
//when launching from a notification, the current directory may be C:\Windows\System32, so we'll use the path of the executable instead
if (defaultDataPath.StartsWith("C:\\Windows\\System32", StringComparison.OrdinalIgnoreCase))
defaultDataPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetExecutingAssembly().Location) ?? ".";
var baseDataPath = Path.GetFullPath(configuration.GetSection("FwLiteMaui").GetValue<string>("BaseDataDir") ??
defaultDataPath);
logging.AddFilter("FwLiteShared.Auth.LoggerAdapter", LogLevel.Warning);
Expand Down
111 changes: 107 additions & 4 deletions backend/FwLite/FwLiteMaui/Platforms/Windows/AppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Buffers;
using System.Net.Http.Json;
using System.Text.Json;
using Windows.Management.Deployment;
using Windows.Networking.Connectivity;
using LexCore.Entities;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Uwp.Notifications;

namespace FwLiteMaui;

Expand All @@ -16,6 +19,10 @@ public class AppUpdateService(
private const string FwliteUpdateUrlEnvVar = "FWLITE_UPDATE_URL";
private const string ForceUpdateCheckEnvVar = "FWLITE_FORCE_UPDATE_CHECK";
private const string PreventUpdateCheckEnvVar = "FWLITE_PREVENT_UPDATE";
private const string NotificationIdKey = "notificationId";
private const string ActionKey = "action";
private const string ResultRefKey = "resultRef";
private static readonly Dictionary<string, TaskCompletionSource<string?>> NotificationCompletionSources = new();

private static readonly SearchValues<string> ValidPositiveEnvVarValues =
SearchValues.Create(["1", "true", "yes"], StringComparison.OrdinalIgnoreCase);
Expand All @@ -25,6 +32,16 @@ public class AppUpdateService(

public void Initialize(IServiceProvider services)
{
ToastNotificationManagerCompat.OnActivated += toastArgs =>
{
ToastArguments args = ToastArguments.Parse(toastArgs.Argument);
HandleNotificationAction(args.Get(ActionKey), args.Get(NotificationIdKey), args);
};
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
//don't check for updates if the user already clicked on a notification
return;
}
_ = Task.Run(TryUpdate);
}

Expand All @@ -39,19 +56,90 @@ private async Task TryUpdate()
if (!ShouldCheckForUpdate()) return;
var response = await ShouldUpdate();
if (!response.Update) return;

if (ShouldPromptBeforeUpdate() && !await RequestPermissionToUpdate(response.Release))
{
return;
}
await ApplyUpdate(response.Release);
}

private async Task ApplyUpdate(FwLiteRelease latestRelease)
private async Task Test()
{
logger.LogInformation("Testing update notifications");
var fwLiteRelease = new FwLiteRelease("1.0.0.0", "https://test.com");
if (!await RequestPermissionToUpdate(fwLiteRelease))
{
logger.LogInformation("User declined update");
return;
}

await ApplyUpdate(fwLiteRelease);
}

private void ShowUpdateAvailableNotification(FwLiteRelease latestRelease)
{
new ToastContentBuilder().AddText("FieldWorks Lite Update Available").AddText($"Version {latestRelease.Version} will be installed after FieldWorks Lite is closed").Show();
}

private async Task<bool> RequestPermissionToUpdate(FwLiteRelease latestRelease)
{
var notificationId = $"update-{Guid.NewGuid()}";
var tcs = new TaskCompletionSource<string?>();
NotificationCompletionSources.Add(notificationId, tcs);
new ToastContentBuilder()
.AddText("FieldWorks Lite Update")
.AddText("A new version of FieldWorks Lite is available")
.AddText($"Version {latestRelease.Version} would you like to download and install this update?")
.AddArgument(NotificationIdKey, notificationId)
.AddButton(new ToastButton()
.SetContent("Download & Install")
.AddArgument(ActionKey, "download")
.AddArgument("release", JsonSerializer.Serialize(latestRelease)))
.AddArgument(ResultRefKey, "release")
.Show(toast =>
{
toast.Tag = "update";
});
var taskResult = await tcs.Task;
return taskResult != null;
}

private void HandleNotificationAction(string action, string notificationId, ToastArguments args)
{
var result = args.Get(args.Get(ResultRefKey));
if (!NotificationCompletionSources.TryGetValue(notificationId, out var tcs))
{
if (action == "download")
{
var release = JsonSerializer.Deserialize<FwLiteRelease>(result);
if (release == null)
{
logger.LogError("Invalid release {Release} for notification {NotificationId}", result, notificationId);
return;
}
_ = Task.Run(() => ApplyUpdate(release, true));
}
else
{
logger.LogError("Unknown action {Action} for notification {NotificationId}", action, notificationId);
}
return;
}

tcs.SetResult(result);
NotificationCompletionSources.Remove(notificationId);
}

private async Task ApplyUpdate(FwLiteRelease latestRelease, bool quitOnUpdate = false)
{
logger.LogInformation("New version available: {Version}, Current version: {CurrentVersion}", latestRelease.Version, AppVersion.Version);
logger.LogInformation("Installing new version: {Version}, Current version: {CurrentVersion}", latestRelease.Version, AppVersion.Version);
var packageManager = new PackageManager();
var asyncOperation = packageManager.AddPackageByUriAsync(new Uri(latestRelease.Url),
new AddPackageOptions()
{
DeferRegistrationWhenPackagesAreInUse = true,
ForceUpdateFromAnyVersion = true
ForceUpdateFromAnyVersion = true,
ForceAppShutdown = quitOnUpdate
});
asyncOperation.Progress = (info, progressInfo) =>
{
Expand All @@ -70,6 +158,7 @@ private async Task ApplyUpdate(FwLiteRelease latestRelease)
}

logger.LogInformation("Update downloaded, will install on next restart");
ShowUpdateAvailableNotification(latestRelease);
}

private async Task<ShouldUpdateResponse> ShouldUpdate()
Expand Down Expand Up @@ -124,4 +213,18 @@ private bool ShouldCheckForUpdate()
preferences.Set(LastUpdateCheck, DateTime.UtcNow);
return true;
}

private bool ShouldPromptBeforeUpdate()
{
return IsOnMeteredConnection();
}

private bool IsOnMeteredConnection()
{
var profile = NetworkInformation.GetInternetConnectionProfile();
if (profile == null) return false;
var cost = profile.GetConnectionCost();
return cost.NetworkCostType != NetworkCostType.Unrestricted;

}
}
21 changes: 20 additions & 1 deletion backend/FwLite/FwLiteMaui/Platforms/Windows/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
IgnorableNamespaces="uap rescap com desktop">

<Identity Name="FwLiteMaui"
Publisher="CN=&quot;Summer Institute of Linguistics, Inc.&quot;, O=&quot;Summer Institute of Linguistics, Inc.&quot;, L=Dallas, S=Texas, C=US"
Expand Down Expand Up @@ -37,6 +39,23 @@
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
<Extensions>

<!--Specify which CLSID to activate when toast clicked-->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="49f2053c-31cc-4eb9-8a65-84491e543d56"/>
</desktop:Extension>

<!--Register COM CLSID LocalServer32 registry key-->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="$targetnametoken$.exe" Arguments="-ToastActivated"
DisplayName="Toast activator">
<com:Class Id="49f2053c-31cc-4eb9-8a65-84491e543d56" DisplayName="Toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>

Expand Down
Loading

0 comments on commit b591027

Please sign in to comment.