diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b6c5cd7ab..fe1fa7f57 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -3323,6 +3323,8 @@ "reportTitleHint": "Eine Zeile dazu, was schiefging", "reportDescriptionLabel": "Was passiert ist", "reportDescriptionHint": "Was du getan hast, was du erwartet hast, was du gesehen hast", + "reportContactLabel": "Kontakt (optional)", + "reportContactHint": "E-Mail oder Discord, nur wenn du eine Antwort möchtest", "reportAttachGroup": "Anhänge", "reportScreenshotRow": "Screenshot hinzufügen", "reportScreenshotRemove": "Screenshot entfernen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5a99c0202..4ea800171 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -15689,6 +15689,14 @@ "@reportDescriptionHint": { "description": "Issue report: description field hint." }, + "reportContactLabel": "Contact (optional)", + "@reportContactLabel": { + "description": "Issue report: optional contact field label." + }, + "reportContactHint": "Email or Discord, only if you want a reply", + "@reportContactHint": { + "description": "Issue report: optional contact field hint. Only shared so the developer can reply." + }, "reportAttachGroup": "Attachments", "@reportAttachGroup": { "description": "Issue report: heading over screenshot and database options." diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 72cd0bc6b..c86544a36 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -7500,6 +7500,8 @@ "reportTitleHint": "Одной строкой: что пошло не так", "reportDescriptionLabel": "Что произошло", "reportDescriptionHint": "Что вы делали, чего ожидали и что увидели", + "reportContactLabel": "Контакт (необязательно)", + "reportContactHint": "Email или Discord, если хотите получить ответ", "reportAttachGroup": "Вложения", "reportScreenshotRow": "Добавить снимок экрана", "reportScreenshotRemove": "Убрать снимок экрана", diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 798460b54..924d3dbf9 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -15689,6 +15689,8 @@ "@reportDescriptionHint": { "description": "Issue report: description field hint." }, + "reportContactLabel": "ช่องทางติดต่อ (ไม่บังคับ)", + "reportContactHint": "อีเมลหรือ Discord หากต้องการให้ตอบกลับ", "reportAttachGroup": "ไฟล์แนบ", "@reportAttachGroup": { "description": "Issue report: heading over screenshot and database options." diff --git a/lib/support/issue_report.dart b/lib/support/issue_report.dart index 351fba304..ad213331f 100644 --- a/lib/support/issue_report.dart +++ b/lib/support/issue_report.dart @@ -85,6 +85,10 @@ String describeReportFile(AppLocalizations l, String path) => switch (path) { class ReportDraft { final ReportCategory category; final String title, description; + + /// Email or handle for a reply. Only in the encrypted user.txt, never in + /// the plaintext server meta. + final String contact; final String? screenshotPath; /// The last-2-weeks slice (uploadable). final bool includeDatabase; @@ -95,6 +99,7 @@ class ReportDraft { required this.category, required this.title, required this.description, + this.contact = '', this.screenshotPath, this.includeDatabase = false, this.fullDatabase = false, @@ -146,7 +151,9 @@ Future writeReportZip( 'logs/sync.log': utf8.encode(syncLog), 'logs/telemetry.json': utf8.encode(pretty.convert(telemetry)), 'user.txt': utf8.encode('Category: ${draft.category.name}\n' - 'Title: ${draft.title}\n\n${draft.description}\n'), + 'Title: ${draft.title}\n' + '${draft.contact.isEmpty ? '' : 'Contact: ${draft.contact}\n'}' + '\n${draft.description}\n'), 'screenshot.${draft.screenshotExt}': ?screenshot, }; final zip = ZipFileEncoder()..create(path); diff --git a/lib/ui2/profile/report_issue.dart b/lib/ui2/profile/report_issue.dart index d924331ca..fcab87436 100644 --- a/lib/ui2/profile/report_issue.dart +++ b/lib/ui2/profile/report_issue.dart @@ -33,6 +33,7 @@ class ReportIssueScreen extends StatefulWidget { class _ReportIssueScreenState extends State { final _title = TextEditingController(); final _body = TextEditingController(); + final _contact = TextEditingController(); ReportCategory _category = ReportCategory.other; String? _shot; bool _db = false; // OFF by default: 2 weeks of the user's health record @@ -54,6 +55,7 @@ class _ReportIssueScreenState extends State { void dispose() { _title.dispose(); _body.dispose(); + _contact.dispose(); super.dispose(); } @@ -66,6 +68,7 @@ class _ReportIssueScreenState extends State { category: _category, title: _title.text.trim(), description: _body.text.trim(), + contact: _contact.text.trim(), screenshotPath: _shot, includeDatabase: _db, fullDatabase: _full, @@ -285,6 +288,8 @@ class _ReportIssueScreenState extends State { _field(c, _body, l.reportDescriptionLabel, l.reportDescriptionHint, lines: 4, max: 4000), + _field(c, _contact, l.reportContactLabel, l.reportContactHint, + max: 200), settingsGroup(c, l.reportAttachGroup, [ SetRow(LucideIcons.image, C.blue, _shot == null diff --git a/test/issue_report_test.dart b/test/issue_report_test.dart index 7a850b3f5..41af38fd0 100644 --- a/test/issue_report_test.dart +++ b/test/issue_report_test.dart @@ -47,6 +47,7 @@ Future _zip( category: ReportCategory.sync, title: 'Sync stuck', description: 'It never finishes', + contact: 'me@example.com', screenshotPath: shot ? '/tmp/x/Shot.PNG' : null, includeDatabase: db, fullDatabase: full, @@ -124,6 +125,16 @@ void main() { {'rawDays': 7}).containsKey('rawDays'), isFalse); }); + test('contact rides only in the encrypted user.txt', () async { + final a = ZipDecoder().decodeBytes(await _zip()); + expect(utf8.decode(a.findFile('user.txt')!.content as List), + contains('Contact: me@example.com\n')); + const d = ReportDraft( + category: ReportCategory.sync, title: 't', description: 'd', + contact: 'me@example.com'); + expect(jsonEncode(reportServerMeta(d, {})), isNot(contains('example.com'))); + }); + test('manifest lists exactly the files in the archive, same as the screen', () async { for (final (shot, db, full) in [