Skip to content

Commit

Permalink
New User Feedback Dialog UI (#683)
Browse files Browse the repository at this point in the history
* Front-end Initial Implementation

* Localize UI and add independent header poster

* Submit result based on state, not check state

* Add handler to share feedback on error dialog

@bagusnl will implement the back-end for the submission system.
+ this commit allows all Control type of elements to be assigned with InputSystemCursorShape.Hand

* Allow Title text box to be collapsed

* Implement user feedback backend for Sentry

* Adjust feedback content

sentry no support newline reee

* Fix parsing error on id_ID locale

* Add callback input on ShowAsync to process the result

* Add docs to the class

* Add docs to ``UserFeedbackResult``

* username and email feedback (jank)

* Localize

* Make CodeQA happy

* Fix dumass bagel putting the wrong variable

Co-authored-by: Kemal Setya Adhi <30566970+neon-nyan@users.noreply.github.com>

* Disable general feedback button
Sentry cannot submit non-exception feedback at the moment, need to find alternative for a more general feedback platform later(tm)

* Add ENABLEUSERFEEDBACK constant

+ The user feedback function will only be enabled once the constant is defined

* Forgor QA

Gawd Damn

* Enable user feedback only on exception dialog

* Disable feedback button once sent

* Remove unused code

* Avoid Click event being triggered on invalid sender

* CodeQA

---------

Co-authored-by: Bagus Nur Listiyono <dzakibagus@gmail.com>
  • Loading branch information
neon-nyan and bagusnl authored Feb 9, 2025
1 parent a739c34 commit 53f206d
Show file tree
Hide file tree
Showing 22 changed files with 1,410 additions and 55 deletions.
1 change: 1 addition & 0 deletions CollapseLauncher/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/ImageEx/ImageEx.xaml" />
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/CommunityToolkit.Labs/DataTable/DataColumn.xaml" />
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/CommunityToolkit.Labs/MarkdownTextBlock/MarkdownTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/UserFeedbackDialog/UserFeedbackDialog.xaml" />
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions CollapseLauncher/Classes/EventsManagement/EventsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,31 @@ public enum ErrorType { Unhandled, GameError, Connection, Warning, DiskCrc }
internal static class ErrorSender
{
private static readonly ErrorSenderInvoker Invoker = new();
public static Exception Exception;
public static string ExceptionContent;
public static ErrorType ExceptionType;
public static string ExceptionTitle;
public static string ExceptionSubtitle;
public static Guid SentryErrorId;

public static void SendException(Exception e, ErrorType eT = ErrorType.Unhandled, bool isSendToSentry = true)
{
// Reset previous Sentry ID
SentryErrorId = Guid.Empty;
Exception = e;
var sentryGuid = Guid.Empty;
if (isSendToSentry)
SentryHelper.ExceptionHandler(e, eT == ErrorType.Unhandled ?
sentryGuid = SentryHelper.ExceptionHandler(e, eT == ErrorType.Unhandled ?
SentryHelper.ExceptionType.UnhandledOther : SentryHelper.ExceptionType.Handled);
SentryErrorId = sentryGuid;
Invoker.SendException(e, eT);
}
public static void SendWarning(Exception e, ErrorType eT = ErrorType.Warning) =>
Invoker.SendException(e, eT);
Invoker.SendException(Exception = e, eT);
public static void SendExceptionWithoutPage(Exception e, ErrorType eT = ErrorType.Unhandled)
{
SentryHelper.ExceptionHandler(e, eT == ErrorType.Unhandled ? SentryHelper.ExceptionType.UnhandledOther : SentryHelper.ExceptionType.Handled);
Exception = e;
ExceptionContent = e!.ToString();
ExceptionType = eT;
SetPageTitle(eT);
Expand Down
1 change: 1 addition & 0 deletions CollapseLauncher/CollapseLauncher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
This decoder supports lots of newest format, including AV1, HEVC and MPEG-DASH Contained video.
- USENEWZIPDECOMPRESS : Use sharpcompress for decompressing .zip game package files
- USEVELOPACK : Use Velopack as the update manager
- ENABLEUSERFEEDBACK : Enable user feedback feature
-->
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<!-- !! IMPORTANT !!-->
Expand Down
5 changes: 5 additions & 0 deletions CollapseLauncher/XAMLs/MainApp/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@
<customcontrol:ContentDialogCollapse x:Name="ContentDialog"
Grid.Row="0"
x:FieldModifier="internal" />
<Grid x:Name="OverlayRootGrid"
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Grid x:Name="TitleBarFrameGrid"
Grid.Row="0">
<Grid x:Name="AppTitleBar"
Expand Down
183 changes: 160 additions & 23 deletions CollapseLauncher/XAMLs/MainApp/Pages/Dialogs/SimpleDialogs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using CollapseLauncher.Helper.Animation;
using CollapseLauncher.Helper.Metadata;
using CollapseLauncher.InstallManager.Base;
using CollapseLauncher.XAMLs.Theme.CustomControls.UserFeedbackDialog;
using CommunityToolkit.WinUI;
using Hi3Helper;
using Hi3Helper.SentryHelper;
Expand Down Expand Up @@ -1182,7 +1183,7 @@ public static Task<ContentDialogResult> Dialog_GenericWarning()
ContentDialogTheme.Warning);
}

public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu()
public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(bool isUserFeedbackSent = false)
{
Button? copyButton = null;

Expand All @@ -1196,22 +1197,23 @@ public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(
.WithHorizontalAlignment(HorizontalAlignment.Stretch)
.WithVerticalAlignment(VerticalAlignment.Stretch)
.WithRows(GridLength.Auto, new GridLength(1, GridUnitType.Star),
GridLength.Auto);
GridLength.Auto)
.WithColumns(GridLength.Auto, new GridLength(1, GridUnitType.Star));

_ = rootGrid.AddElementToGridRow(new TextBlock
_ = rootGrid.AddElementToGridRowColumn(new TextBlock
{
Text = subtitle,
TextWrapping = TextWrapping.Wrap,
FontWeight = FontWeights.Medium
}, 0);
_ = rootGrid.AddElementToGridRow(new TextBox
}, 0, 0, 0, 2);
_ = rootGrid.AddElementToGridRowColumn(new TextBox
{
IsReadOnly = true,
TextWrapping = TextWrapping.Wrap,
MaxHeight = 300,
AcceptsReturn = true,
Text = exceptionContent
}, 1).WithMargin(0d, 8d)
}, 1, 0, 0, 2).WithMargin(0d, 8d)
.WithHorizontalAlignment(HorizontalAlignment.Stretch)
.WithVerticalAlignment(VerticalAlignment.Stretch);

Expand All @@ -1220,18 +1222,47 @@ public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(
"",
"FontAwesomeSolid",
"AccentButtonStyle"
), 2)
.WithHorizontalAlignment(HorizontalAlignment.Center);
).WithHorizontalAlignment(
HorizontalAlignment.Left
), 2);
copyButton.Click += CopyTextToClipboard;

var btnText = isUserFeedbackSent ? Lang._Misc.ExceptionFeedbackBtn_FeedbackSent :
ErrorSender.SentryErrorId == Guid.Empty
? Lang._Misc.ExceptionFeedbackBtn_Unavailable
: Lang._Misc.ExceptionFeedbackBtn;

Button submitFeedbackButton = rootGrid.AddElementToGridRowColumn(CollapseUIExt.CreateButtonWithIcon<Button>(
btnText,
"\ue594",
"FontAwesomeSolid",
"TransparentDefaultButtonStyle",
14,
10
).WithMargin(8,0,0,0).WithHorizontalAlignment(HorizontalAlignment.Right),
2, 1);

if (ErrorSender.SentryErrorId == Guid.Empty || isUserFeedbackSent)
{
submitFeedbackButton.IsEnabled = false;
}

submitFeedbackButton.Click += SubmitFeedbackButton_Click;
// TODO: Change button content after feedback is submitted

ContentDialogResult result = await SpawnDialog(title, rootGrid, null,
Lang._UnhandledExceptionPage.GoBackPageBtn1,
null,
null,
ContentDialogButton.Close,
ContentDialogTheme.Error);
ContentDialogTheme.Error,
OnLoadedDialog
);

return result;

void OnLoadedDialog(object? sender, RoutedEventArgs e)
=> submitFeedbackButton.SetTag(sender);
}
catch (Exception ex)
{
Expand All @@ -1247,6 +1278,77 @@ public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(
}
}

// ReSharper disable once AsyncVoidMethod
private static async void SubmitFeedbackButton_Click(object sender, RoutedEventArgs e)
{
bool isFeedbackSent = false;
if (sender is not Button { Tag: ContentDialog contentDialog })
{
return;
}

try
{
contentDialog.Hide();

var userTemplate = Lang._Misc.ExceptionFeedbackTemplate_User;
var emailTemplate = Lang._Misc.ExceptionFeedbackTemplate_Email;

string exceptionContent = $"""
{userTemplate}
{emailTemplate}
{Lang._Misc.ExceptionFeedbackTemplate_Message}
------------------------------------
""";
string exceptionTitle = $"{Lang._Misc.ExceptionFeedbackTitle} {ErrorSender.ExceptionTitle}";

UserFeedbackDialog feedbackDialog = new UserFeedbackDialog(contentDialog.XamlRoot)
{
Title = exceptionTitle,
IsTitleReadOnly = true,
Message = exceptionContent
};
UserFeedbackResult? feedbackResult = await feedbackDialog.ShowAsync();
// TODO: (Optional) Implement generic user feedback pathway (preferably when SentryErrorId is null
// Using https://paste.mozilla.org/
// API Documentation: https://docs.dpaste.org/api/
// Though im not sure since user will still need to paste the link to us 🤷

if (feedbackResult is null)
{
return;
}

// Parse username and email
var msg = feedbackResult.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (msg.Length <= 4) return; // Do not send feedback if format is not correct
var user = msg[0].Replace(userTemplate, "", StringComparison.InvariantCulture).Trim();
var email = msg[1].Replace(userTemplate, "", StringComparison.InvariantCulture).Trim();
var feedback = msg.Length > 4 ? string.Join("\n", msg.Skip(4)).Trim() : null;

if (string.IsNullOrEmpty(user)) user = "none";

// Validate email
var addr = System.Net.Mail.MailAddress.TryCreate(email, out var address);
email = addr ? address!.Address : "user@collapselauncher.com";

if (string.IsNullOrEmpty(feedback)) return;

var feedbackContent = $"{feedback}\n\nRating: {feedbackResult.Rating}/5";

SentryHelper.SendExceptionFeedback(ErrorSender.SentryErrorId, email, user, feedbackContent);
isFeedbackSent = true;
}
catch (Exception ex)
{
await SentryHelper.ExceptionHandlerAsync(ex, SentryHelper.ExceptionType.UnhandledOther);
}
finally
{
await Dialog_ShowUnhandledExceptionMenu(isFeedbackSent);
}
}

private static async void CopyTextToClipboard(object sender, RoutedEventArgs e)
{
try
Expand Down Expand Up @@ -1495,7 +1597,8 @@ public static Task<ContentDialogResult> SpawnDialog(string? title,
ContentDialogButton defaultButton =
ContentDialogButton.Primary,
ContentDialogTheme dialogTheme =
ContentDialogTheme.Informational)
ContentDialogTheme.Informational,
RoutedEventHandler? onLoaded = null)
{
_sharedDispatcherQueue ??=
parentUI?.DispatcherQueue ??
Expand Down Expand Up @@ -1524,8 +1627,19 @@ WindowUtility.CurrentWindow is MainWindow
: parentUI?.XamlRoot
};

// Queue and spawn the dialog instance
return await dialog.QueueAndSpawnDialog();
try
{
if (onLoaded is not null)
dialog.Loaded += onLoaded;

// Queue and spawn the dialog instance
return await dialog.QueueAndSpawnDialog();
}
finally
{
if (onLoaded is not null)
dialog.Loaded -= onLoaded;
}
}) ?? Task.FromResult(ContentDialogResult.None);
}

Expand All @@ -1548,18 +1662,41 @@ public static async Task<ContentDialogResult> QueueAndSpawnDialog(this ContentDi
dialog.RequestedTheme = InnerLauncherConfig.IsAppThemeLight ? ElementTheme.Light : ElementTheme.Dark;
}

dialog.XamlRoot ??= SharedXamlRoot;
try
{
dialog.XamlRoot ??= SharedXamlRoot;
dialog.Loaded += RecursivelySetDialogCursor;

// Assign the dialog to the global task
_currentSpawnedDialogTask = dialog switch
{
ContentDialogCollapse dialogCollapse => dialogCollapse.ShowAsync(),
ContentDialogOverlay overlapCollapse => overlapCollapse.ShowAsync(),
_ => dialog.ShowAsync()
};
// Spawn and await for the result
ContentDialogResult dialogResult = await _currentSpawnedDialogTask;
return dialogResult; // Return the result
// Assign the dialog to the global task
_currentSpawnedDialogTask = dialog switch
{
ContentDialogCollapse dialogCollapse => dialogCollapse.ShowAsync(),
ContentDialogOverlay overlapCollapse => overlapCollapse.ShowAsync(),
_ => dialog.ShowAsync()
};
// Spawn and await for the result
ContentDialogResult dialogResult = await _currentSpawnedDialogTask;
return dialogResult; // Return the result
}
finally
{
dialog.Loaded -= RecursivelySetDialogCursor;
}
}

private static void RecursivelySetDialogCursor(object sender, RoutedEventArgs args)
{
if (sender is not ContentDialog contentDialog)
{
return;
}

InputSystemCursor cursor = InputSystemCursor.Create(InputSystemCursorShape.Hand);
contentDialog.SetAllControlsCursorRecursive(cursor);

Grid? parent = (contentDialog.Content as UIElement)?.FindAscendant("LayoutRoot", StringComparison.OrdinalIgnoreCase) as Grid;
Grid? commandButtonGrid = parent?.FindDescendant("CommandSpace", StringComparison.OrdinalIgnoreCase) as Grid;
commandButtonGrid?.SetAllControlsCursorRecursive(cursor);
}
}
}
Loading

0 comments on commit 53f206d

Please sign in to comment.