Skip to content

Commit 53f206d

Browse files
neon-nyanbagusnl
andauthored
New User Feedback Dialog UI (#683)
* 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 <[email protected]> * 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 <[email protected]>
1 parent a739c34 commit 53f206d

File tree

22 files changed

+1410
-55
lines changed

22 files changed

+1410
-55
lines changed

CollapseLauncher/App.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/ImageEx/ImageEx.xaml" />
2929
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/CommunityToolkit.Labs/DataTable/DataColumn.xaml" />
3030
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/CommunityToolkit.Labs/MarkdownTextBlock/MarkdownTextBlock.xaml" />
31+
<ResourceDictionary Source="ms-appx:///XAMLs/Theme/CustomControls/UserFeedbackDialog/UserFeedbackDialog.xaml" />
3132
<ResourceDictionary>
3233
<ResourceDictionary.ThemeDictionaries>
3334
<ResourceDictionary x:Key="Default">
23.5 KB
Loading
20.9 KB
Loading
21.2 KB
Loading
24.4 KB
Loading

CollapseLauncher/Classes/EventsManagement/EventsHandler.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,23 +128,31 @@ public enum ErrorType { Unhandled, GameError, Connection, Warning, DiskCrc }
128128
internal static class ErrorSender
129129
{
130130
private static readonly ErrorSenderInvoker Invoker = new();
131+
public static Exception Exception;
131132
public static string ExceptionContent;
132133
public static ErrorType ExceptionType;
133134
public static string ExceptionTitle;
134135
public static string ExceptionSubtitle;
136+
public static Guid SentryErrorId;
135137

136138
public static void SendException(Exception e, ErrorType eT = ErrorType.Unhandled, bool isSendToSentry = true)
137139
{
140+
// Reset previous Sentry ID
141+
SentryErrorId = Guid.Empty;
142+
Exception = e;
143+
var sentryGuid = Guid.Empty;
138144
if (isSendToSentry)
139-
SentryHelper.ExceptionHandler(e, eT == ErrorType.Unhandled ?
145+
sentryGuid = SentryHelper.ExceptionHandler(e, eT == ErrorType.Unhandled ?
140146
SentryHelper.ExceptionType.UnhandledOther : SentryHelper.ExceptionType.Handled);
147+
SentryErrorId = sentryGuid;
141148
Invoker.SendException(e, eT);
142149
}
143150
public static void SendWarning(Exception e, ErrorType eT = ErrorType.Warning) =>
144-
Invoker.SendException(e, eT);
151+
Invoker.SendException(Exception = e, eT);
145152
public static void SendExceptionWithoutPage(Exception e, ErrorType eT = ErrorType.Unhandled)
146153
{
147154
SentryHelper.ExceptionHandler(e, eT == ErrorType.Unhandled ? SentryHelper.ExceptionType.UnhandledOther : SentryHelper.ExceptionType.Handled);
155+
Exception = e;
148156
ExceptionContent = e!.ToString();
149157
ExceptionType = eT;
150158
SetPageTitle(eT);

CollapseLauncher/CollapseLauncher.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
This decoder supports lots of newest format, including AV1, HEVC and MPEG-DASH Contained video.
7878
- USENEWZIPDECOMPRESS : Use sharpcompress for decompressing .zip game package files
7979
- USEVELOPACK : Use Velopack as the update manager
80+
- ENABLEUSERFEEDBACK : Enable user feedback feature
8081
-->
8182
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
8283
<!-- !! IMPORTANT !!-->

CollapseLauncher/XAMLs/MainApp/MainWindow.xaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@
162162
<customcontrol:ContentDialogCollapse x:Name="ContentDialog"
163163
Grid.Row="0"
164164
x:FieldModifier="internal" />
165+
<Grid x:Name="OverlayRootGrid"
166+
Grid.Row="0"
167+
Grid.RowSpan="3"
168+
HorizontalAlignment="Stretch"
169+
VerticalAlignment="Stretch" />
165170
<Grid x:Name="TitleBarFrameGrid"
166171
Grid.Row="0">
167172
<Grid x:Name="AppTitleBar"

CollapseLauncher/XAMLs/MainApp/Pages/Dialogs/SimpleDialogs.cs

Lines changed: 160 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using CollapseLauncher.Helper.Animation;
55
using CollapseLauncher.Helper.Metadata;
66
using CollapseLauncher.InstallManager.Base;
7+
using CollapseLauncher.XAMLs.Theme.CustomControls.UserFeedbackDialog;
78
using CommunityToolkit.WinUI;
89
using Hi3Helper;
910
using Hi3Helper.SentryHelper;
@@ -1182,7 +1183,7 @@ public static Task<ContentDialogResult> Dialog_GenericWarning()
11821183
ContentDialogTheme.Warning);
11831184
}
11841185

1185-
public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu()
1186+
public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(bool isUserFeedbackSent = false)
11861187
{
11871188
Button? copyButton = null;
11881189

@@ -1196,22 +1197,23 @@ public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(
11961197
.WithHorizontalAlignment(HorizontalAlignment.Stretch)
11971198
.WithVerticalAlignment(VerticalAlignment.Stretch)
11981199
.WithRows(GridLength.Auto, new GridLength(1, GridUnitType.Star),
1199-
GridLength.Auto);
1200+
GridLength.Auto)
1201+
.WithColumns(GridLength.Auto, new GridLength(1, GridUnitType.Star));
12001202

1201-
_ = rootGrid.AddElementToGridRow(new TextBlock
1203+
_ = rootGrid.AddElementToGridRowColumn(new TextBlock
12021204
{
12031205
Text = subtitle,
12041206
TextWrapping = TextWrapping.Wrap,
12051207
FontWeight = FontWeights.Medium
1206-
}, 0);
1207-
_ = rootGrid.AddElementToGridRow(new TextBox
1208+
}, 0, 0, 0, 2);
1209+
_ = rootGrid.AddElementToGridRowColumn(new TextBox
12081210
{
12091211
IsReadOnly = true,
12101212
TextWrapping = TextWrapping.Wrap,
12111213
MaxHeight = 300,
12121214
AcceptsReturn = true,
12131215
Text = exceptionContent
1214-
}, 1).WithMargin(0d, 8d)
1216+
}, 1, 0, 0, 2).WithMargin(0d, 8d)
12151217
.WithHorizontalAlignment(HorizontalAlignment.Stretch)
12161218
.WithVerticalAlignment(VerticalAlignment.Stretch);
12171219

@@ -1220,18 +1222,47 @@ public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(
12201222
"",
12211223
"FontAwesomeSolid",
12221224
"AccentButtonStyle"
1223-
), 2)
1224-
.WithHorizontalAlignment(HorizontalAlignment.Center);
1225+
).WithHorizontalAlignment(
1226+
HorizontalAlignment.Left
1227+
), 2);
12251228
copyButton.Click += CopyTextToClipboard;
12261229

1230+
var btnText = isUserFeedbackSent ? Lang._Misc.ExceptionFeedbackBtn_FeedbackSent :
1231+
ErrorSender.SentryErrorId == Guid.Empty
1232+
? Lang._Misc.ExceptionFeedbackBtn_Unavailable
1233+
: Lang._Misc.ExceptionFeedbackBtn;
1234+
1235+
Button submitFeedbackButton = rootGrid.AddElementToGridRowColumn(CollapseUIExt.CreateButtonWithIcon<Button>(
1236+
btnText,
1237+
"\ue594",
1238+
"FontAwesomeSolid",
1239+
"TransparentDefaultButtonStyle",
1240+
14,
1241+
10
1242+
).WithMargin(8,0,0,0).WithHorizontalAlignment(HorizontalAlignment.Right),
1243+
2, 1);
1244+
1245+
if (ErrorSender.SentryErrorId == Guid.Empty || isUserFeedbackSent)
1246+
{
1247+
submitFeedbackButton.IsEnabled = false;
1248+
}
1249+
1250+
submitFeedbackButton.Click += SubmitFeedbackButton_Click;
1251+
// TODO: Change button content after feedback is submitted
1252+
12271253
ContentDialogResult result = await SpawnDialog(title, rootGrid, null,
12281254
Lang._UnhandledExceptionPage.GoBackPageBtn1,
12291255
null,
12301256
null,
12311257
ContentDialogButton.Close,
1232-
ContentDialogTheme.Error);
1258+
ContentDialogTheme.Error,
1259+
OnLoadedDialog
1260+
);
12331261

12341262
return result;
1263+
1264+
void OnLoadedDialog(object? sender, RoutedEventArgs e)
1265+
=> submitFeedbackButton.SetTag(sender);
12351266
}
12361267
catch (Exception ex)
12371268
{
@@ -1247,6 +1278,77 @@ public static async Task<ContentDialogResult> Dialog_ShowUnhandledExceptionMenu(
12471278
}
12481279
}
12491280

1281+
// ReSharper disable once AsyncVoidMethod
1282+
private static async void SubmitFeedbackButton_Click(object sender, RoutedEventArgs e)
1283+
{
1284+
bool isFeedbackSent = false;
1285+
if (sender is not Button { Tag: ContentDialog contentDialog })
1286+
{
1287+
return;
1288+
}
1289+
1290+
try
1291+
{
1292+
contentDialog.Hide();
1293+
1294+
var userTemplate = Lang._Misc.ExceptionFeedbackTemplate_User;
1295+
var emailTemplate = Lang._Misc.ExceptionFeedbackTemplate_Email;
1296+
1297+
string exceptionContent = $"""
1298+
{userTemplate}
1299+
{emailTemplate}
1300+
{Lang._Misc.ExceptionFeedbackTemplate_Message}
1301+
------------------------------------
1302+
""";
1303+
string exceptionTitle = $"{Lang._Misc.ExceptionFeedbackTitle} {ErrorSender.ExceptionTitle}";
1304+
1305+
UserFeedbackDialog feedbackDialog = new UserFeedbackDialog(contentDialog.XamlRoot)
1306+
{
1307+
Title = exceptionTitle,
1308+
IsTitleReadOnly = true,
1309+
Message = exceptionContent
1310+
};
1311+
UserFeedbackResult? feedbackResult = await feedbackDialog.ShowAsync();
1312+
// TODO: (Optional) Implement generic user feedback pathway (preferably when SentryErrorId is null
1313+
// Using https://paste.mozilla.org/
1314+
// API Documentation: https://docs.dpaste.org/api/
1315+
// Though im not sure since user will still need to paste the link to us 🤷
1316+
1317+
if (feedbackResult is null)
1318+
{
1319+
return;
1320+
}
1321+
1322+
// Parse username and email
1323+
var msg = feedbackResult.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
1324+
if (msg.Length <= 4) return; // Do not send feedback if format is not correct
1325+
var user = msg[0].Replace(userTemplate, "", StringComparison.InvariantCulture).Trim();
1326+
var email = msg[1].Replace(userTemplate, "", StringComparison.InvariantCulture).Trim();
1327+
var feedback = msg.Length > 4 ? string.Join("\n", msg.Skip(4)).Trim() : null;
1328+
1329+
if (string.IsNullOrEmpty(user)) user = "none";
1330+
1331+
// Validate email
1332+
var addr = System.Net.Mail.MailAddress.TryCreate(email, out var address);
1333+
email = addr ? address!.Address : "[email protected]";
1334+
1335+
if (string.IsNullOrEmpty(feedback)) return;
1336+
1337+
var feedbackContent = $"{feedback}\n\nRating: {feedbackResult.Rating}/5";
1338+
1339+
SentryHelper.SendExceptionFeedback(ErrorSender.SentryErrorId, email, user, feedbackContent);
1340+
isFeedbackSent = true;
1341+
}
1342+
catch (Exception ex)
1343+
{
1344+
await SentryHelper.ExceptionHandlerAsync(ex, SentryHelper.ExceptionType.UnhandledOther);
1345+
}
1346+
finally
1347+
{
1348+
await Dialog_ShowUnhandledExceptionMenu(isFeedbackSent);
1349+
}
1350+
}
1351+
12501352
private static async void CopyTextToClipboard(object sender, RoutedEventArgs e)
12511353
{
12521354
try
@@ -1495,7 +1597,8 @@ public static Task<ContentDialogResult> SpawnDialog(string? title,
14951597
ContentDialogButton defaultButton =
14961598
ContentDialogButton.Primary,
14971599
ContentDialogTheme dialogTheme =
1498-
ContentDialogTheme.Informational)
1600+
ContentDialogTheme.Informational,
1601+
RoutedEventHandler? onLoaded = null)
14991602
{
15001603
_sharedDispatcherQueue ??=
15011604
parentUI?.DispatcherQueue ??
@@ -1524,8 +1627,19 @@ WindowUtility.CurrentWindow is MainWindow
15241627
: parentUI?.XamlRoot
15251628
};
15261629

1527-
// Queue and spawn the dialog instance
1528-
return await dialog.QueueAndSpawnDialog();
1630+
try
1631+
{
1632+
if (onLoaded is not null)
1633+
dialog.Loaded += onLoaded;
1634+
1635+
// Queue and spawn the dialog instance
1636+
return await dialog.QueueAndSpawnDialog();
1637+
}
1638+
finally
1639+
{
1640+
if (onLoaded is not null)
1641+
dialog.Loaded -= onLoaded;
1642+
}
15291643
}) ?? Task.FromResult(ContentDialogResult.None);
15301644
}
15311645

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

1551-
dialog.XamlRoot ??= SharedXamlRoot;
1665+
try
1666+
{
1667+
dialog.XamlRoot ??= SharedXamlRoot;
1668+
dialog.Loaded += RecursivelySetDialogCursor;
15521669

1553-
// Assign the dialog to the global task
1554-
_currentSpawnedDialogTask = dialog switch
1555-
{
1556-
ContentDialogCollapse dialogCollapse => dialogCollapse.ShowAsync(),
1557-
ContentDialogOverlay overlapCollapse => overlapCollapse.ShowAsync(),
1558-
_ => dialog.ShowAsync()
1559-
};
1560-
// Spawn and await for the result
1561-
ContentDialogResult dialogResult = await _currentSpawnedDialogTask;
1562-
return dialogResult; // Return the result
1670+
// Assign the dialog to the global task
1671+
_currentSpawnedDialogTask = dialog switch
1672+
{
1673+
ContentDialogCollapse dialogCollapse => dialogCollapse.ShowAsync(),
1674+
ContentDialogOverlay overlapCollapse => overlapCollapse.ShowAsync(),
1675+
_ => dialog.ShowAsync()
1676+
};
1677+
// Spawn and await for the result
1678+
ContentDialogResult dialogResult = await _currentSpawnedDialogTask;
1679+
return dialogResult; // Return the result
1680+
}
1681+
finally
1682+
{
1683+
dialog.Loaded -= RecursivelySetDialogCursor;
1684+
}
1685+
}
1686+
1687+
private static void RecursivelySetDialogCursor(object sender, RoutedEventArgs args)
1688+
{
1689+
if (sender is not ContentDialog contentDialog)
1690+
{
1691+
return;
1692+
}
1693+
1694+
InputSystemCursor cursor = InputSystemCursor.Create(InputSystemCursorShape.Hand);
1695+
contentDialog.SetAllControlsCursorRecursive(cursor);
1696+
1697+
Grid? parent = (contentDialog.Content as UIElement)?.FindAscendant("LayoutRoot", StringComparison.OrdinalIgnoreCase) as Grid;
1698+
Grid? commandButtonGrid = parent?.FindDescendant("CommandSpace", StringComparison.OrdinalIgnoreCase) as Grid;
1699+
commandButtonGrid?.SetAllControlsCursorRecursive(cursor);
15631700
}
15641701
}
15651702
}

0 commit comments

Comments
 (0)