From 3e9c4284ec8d770ee3224a740fb9f87e5b1bf910 Mon Sep 17 00:00:00 2001 From: jenkins build server Date: Tue, 3 Dec 2024 10:35:48 +0100 Subject: [PATCH 1/6] update translations --- src/common/misc/TranslationKey.ts | 3 +++ src/mail-app/translations/de.ts | 5 ++++- src/mail-app/translations/de_sie.ts | 5 ++++- src/mail-app/translations/en.ts | 5 ++++- src/mail-app/translations/es.ts | 6 +++++- src/mail-app/translations/fi.ts | 5 ++++- src/mail-app/translations/hu.ts | 5 ++++- src/mail-app/translations/pl.ts | 9 ++++++++- src/mail-app/translations/ru.ts | 17 ++++++++++++++++- src/mail-app/translations/sv.ts | 5 ++++- src/mail-app/translations/tr.ts | 9 ++++++++- src/mail-app/translations/zh_hant.ts | 9 ++++++--- 12 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index f139058adbc..78c2590b271 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -492,6 +492,7 @@ export type TranslationKeyType = | "domain_label" | "done_action" | "doNotAskAgain_label" + | "dontAskAgain_label" | "downloadCompleted_msg" | "downloadInvoicePdf_action" | "downloadInvoiceXml_action" @@ -1038,6 +1039,7 @@ export type TranslationKeyType = | "notificationSync_msg" | "notificationTargets_label" | "noTitle_label" + | "notNow_label" | "notSigned_msg" | "noUpdateAvailable_msg" | "noValidMembersToAdd_msg" @@ -1319,6 +1321,7 @@ export type TranslationKeyType = | "quit_action" | "ratingExplanation_msg" | "ratingHowAreWeDoing_title" + | "ratingLoveIt_label" | "ratingNeedsWork_label" | "readResponse_action" | "reallySubmitContent_msg" diff --git a/src/mail-app/translations/de.ts b/src/mail-app/translations/de.ts index a3e0085501d..e4bdd70914a 100644 --- a/src/mail-app/translations/de.ts +++ b/src/mail-app/translations/de.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-01-13T20:40:31Z", - "updated_at": "2024-11-28T11:39:19Z", + "updated_at": "2024-12-02T11:39:16Z", "source_locale": { "id": "fcd7471b347c8e517663e194dcddf237", "name": "en", @@ -513,6 +513,7 @@ export default { "domain_label": "Domain", "done_action": "Fertig", "doNotAskAgain_label": "Nicht noch einmal fragen", + "dontAskAgain_label": "Nicht mehr fragen", "downloadCompleted_msg": "Download abgeschlossen", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -1059,6 +1060,7 @@ export default { "notificationSync_msg": "Synchronisiere Benachrichtigungen", "notificationTargets_label": "Ziele für Benachrichtigungen", "noTitle_label": "", + "notNow_label": "Später", "notSigned_msg": "Nicht unterschrieben.", "noUpdateAvailable_msg": "Keine neue Version verfügbar.", "noValidMembersToAdd_msg": "Du administrierst keine Benutzer, welche noch nicht in dieser Gruppe sind.", @@ -1340,6 +1342,7 @@ export default { "quit_action": "Beenden", "ratingExplanation_msg": "Ob du Tuta liebst oder meinst, wir könnten etwas verbessern, lass es uns wissen!", "ratingHowAreWeDoing_title": "Wie gefällt dir Tuta?", + "ratingLoveIt_label": "Ich liebe es!", "ratingNeedsWork_label": "Braucht Arbeit", "readResponse_action": "Antwort lesen", "reallySubmitContent_msg": "Möchtest du die eingegebenen Daten wirklich an eine externe Seite senden?", diff --git a/src/mail-app/translations/de_sie.ts b/src/mail-app/translations/de_sie.ts index 40ea758e821..075e51e1586 100644 --- a/src/mail-app/translations/de_sie.ts +++ b/src/mail-app/translations/de_sie.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2018-01-12T10:51:54Z", - "updated_at": "2024-11-28T11:39:19Z", + "updated_at": "2024-12-02T11:39:17Z", "source_locale": { "id": "2001c6fdcc9cd338c1d600cb2636918b", "name": "de", @@ -513,6 +513,7 @@ export default { "domain_label": "Domain", "done_action": "Fertig", "doNotAskAgain_label": "Nicht noch einmal fragen", + "dontAskAgain_label": "Nicht mehr fragen", "downloadCompleted_msg": "Download abgeschlossen", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -1059,6 +1060,7 @@ export default { "notificationSync_msg": "Synchronisiere Benachrichtigungen", "notificationTargets_label": "Ziele für Benachrichtigungen", "noTitle_label": "", + "notNow_label": "Später", "notSigned_msg": "Nicht unterschrieben.", "noUpdateAvailable_msg": "Kein Update gefunden.", "noValidMembersToAdd_msg": "Sie administrieren keine Benutzer, welche noch nicht in dieser Gruppe sind.", @@ -1340,6 +1342,7 @@ export default { "quit_action": "Beenden", "ratingExplanation_msg": "Ob Sie Tuta lieben oder meinen, wir könnten etwas verbessern, lassen Sie es uns wissen!", "ratingHowAreWeDoing_title": "Wie gefällt Ihnen Tuta?", + "ratingLoveIt_label": "Ich liebe es!", "ratingNeedsWork_label": "Braucht Arbeit", "readResponse_action": "Antwort lesen", "reallySubmitContent_msg": "Möchten Sie die eingegebenen Daten wirklich an eine externe Seite senden?", diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index cfb29e87839..24e2d093b05 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-01-13T20:10:13Z", - "updated_at": "2024-11-28T11:39:19Z", + "updated_at": "2024-12-02T11:39:16Z", "source_locale": null, "fallback_locale": null, "keys": { @@ -509,6 +509,7 @@ export default { "domain_label": "Domain", "done_action": "Done", "doNotAskAgain_label": "Don't ask again for this file", + "dontAskAgain_label": "Don't ask again", "downloadCompleted_msg": "Download completed", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -1055,6 +1056,7 @@ export default { "notificationSync_msg": "Synchronizing notifications", "notificationTargets_label": "Notification targets", "noTitle_label": "", + "notNow_label": "Not now", "notSigned_msg": "Not signed.", "noUpdateAvailable_msg": "No Update found.", "noValidMembersToAdd_msg": "You are not administrating any users that are not already a member of this group.", @@ -1336,6 +1338,7 @@ export default { "quit_action": "Quit", "ratingExplanation_msg": "Whether you love Tuta or feel we could improve, let us know!", "ratingHowAreWeDoing_title": "How do you like Tuta?", + "ratingLoveIt_label": "Love it!", "ratingNeedsWork_label": "Needs work", "readResponse_action": "Read response", "reallySubmitContent_msg": "Do you really want to send the entered data to an external site?", diff --git a/src/mail-app/translations/es.ts b/src/mail-app/translations/es.ts index 320633b4f94..61418ca4705 100644 --- a/src/mail-app/translations/es.ts +++ b/src/mail-app/translations/es.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-01-27T13:13:02Z", - "updated_at": "2024-11-26T16:54:57Z", + "updated_at": "2024-11-29T12:20:34Z", "source_locale": { "id": "fcd7471b347c8e517663e194dcddf237", "name": "en", @@ -721,6 +721,7 @@ export default { "importContactsError_msg": "{amount} de {total} contactos no se pudieron importar.", "importContacts_label": "Importar contactos", "importContacts_msg": "Importar contactos desde la agenda de tu teléfono para tenerlos disponibles en todos tus dispositivos.", + "importedMailsWillBeDeleted_label": "Se eliminarán todos los correos importados", "importEndNotAfterStartInEvent_msg": "{amount} de {total} eventos no tienen su fecha de inicio antes de su fecha de finalización y no se importarán.", "importEventExistingUid_msg": "{amount} de {total} eventos ya existen y no se sobrescriben. Continuando con los eventos restantes…", "importEventsError_msg": "{amount} de {total} eventos no se pudieron importar.", @@ -1337,6 +1338,9 @@ export default { "quitDNSSetup_msg": "Por favor, configura todos los registros DNS como se indica. De lo contrario, no podrás usar tu dominio personalizado con Tuta.", "quitSetup_title": "¿Salir de la configuración?", "quit_action": "Salir", + "ratingExplanation_msg": "Tanto si te encanta Tuta como si crees que podríamos mejorar, ¡háznoslo saber!", + "ratingHowAreWeDoing_title": "¿Qué te parece Tuta?", + "ratingNeedsWork_label": "Necesita trabajo", "readResponse_action": "Leer respuesta", "reallySubmitContent_msg": "¿Quiere realmente enviar los datos introducidos a un sitio externo?", "receiveCalendarNotifications_label": "Recibir notificaciones de eventos del calendario", diff --git a/src/mail-app/translations/fi.ts b/src/mail-app/translations/fi.ts index 442d9b4ea7f..676932c7b62 100644 --- a/src/mail-app/translations/fi.ts +++ b/src/mail-app/translations/fi.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-03-10T10:52:15Z", - "updated_at": "2024-11-28T18:28:59Z", + "updated_at": "2024-12-02T14:16:05Z", "source_locale": null, "fallback_locale": null, "keys": { @@ -509,6 +509,7 @@ export default { "domain_label": "Verkkotunnus", "done_action": "Valmis", "doNotAskAgain_label": "Älä kysy uudestaan tämän tiedoston kohdalla", + "dontAskAgain_label": "Älä kysy enää", "downloadCompleted_msg": "Lataus valmis", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -1055,6 +1056,7 @@ export default { "notificationSync_msg": "Ilmoituksia synkronoidaan", "notificationTargets_label": "Ilmoituskohteet", "noTitle_label": "", + "notNow_label": "Ei nyt", "notSigned_msg": "Et ole kirjautunut sisään.", "noUpdateAvailable_msg": "Päivitystä ei löytynyt.", "noValidMembersToAdd_msg": "Et hallinnoi käyttäjiä, jotka eivät olisi jo tässä ryhmässä.", @@ -1336,6 +1338,7 @@ export default { "quit_action": "Poistu", "ratingExplanation_msg": "Kerro meille, jos pidät Tutasta tai jos sinusta meillä on parannettavaa!", "ratingHowAreWeDoing_title": "Mitä mieltä olet Tutasta?", + "ratingLoveIt_label": "Rakastan sitä!", "ratingNeedsWork_label": "On parannettavaa", "readResponse_action": "Lue vastaus", "reallySubmitContent_msg": "Haluatko lähettää annetut tiedot ulkopuoliselle sivustolle?", diff --git a/src/mail-app/translations/hu.ts b/src/mail-app/translations/hu.ts index 2765249c923..edfd2e6b59a 100644 --- a/src/mail-app/translations/hu.ts +++ b/src/mail-app/translations/hu.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-04-02T12:56:44Z", - "updated_at": "2024-11-28T14:08:38Z", + "updated_at": "2024-12-03T09:30:25Z", "source_locale": null, "fallback_locale": null, "keys": { @@ -509,6 +509,7 @@ export default { "domain_label": "Domain", "done_action": "Végrehajtva", "doNotAskAgain_label": "Ne kérdezze újra ennél a fájlnál.", + "dontAskAgain_label": "Ne kérdezze újra!", "downloadCompleted_msg": "A letöltés befejeződött.", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -1055,6 +1056,7 @@ export default { "notificationSync_msg": "Szinkronizációs értesítések", "notificationTargets_label": "Értesítési célok", "noTitle_label": "", + "notNow_label": "Most nem.", "notSigned_msg": "Nincs aláírva.", "noUpdateAvailable_msg": "Nincs újabb frissítés.", "noValidMembersToAdd_msg": "Nem kezelhet felhasználókat, mivel már nem tagja ennek a csoportnak!", @@ -1336,6 +1338,7 @@ export default { "quit_action": "Kilépés", "ratingExplanation_msg": "Akár szereted a Tutát, akár úgy érzed, hogy javíthatnánk rajta, tudasd velünk!", "ratingHowAreWeDoing_title": "Hogy tetszik a Tuta?", + "ratingLoveIt_label": "Imádja!", "ratingNeedsWork_label": "Munkára szorul", "readResponse_action": "Válasz olvasása", "reallySubmitContent_msg": "Valóban el akarja küldeni a megadott adatokat egy külső oldalnak?", diff --git a/src/mail-app/translations/pl.ts b/src/mail-app/translations/pl.ts index 8779dd16a62..1cd1f786282 100644 --- a/src/mail-app/translations/pl.ts +++ b/src/mail-app/translations/pl.ts @@ -15,7 +15,7 @@ export default { "other" ], "created_at": "2015-01-27T13:13:41Z", - "updated_at": "2024-11-26T20:54:01Z", + "updated_at": "2024-12-02T19:54:34Z", "source_locale": { "id": "fcd7471b347c8e517663e194dcddf237", "name": "en", @@ -515,6 +515,7 @@ export default { "domain_label": "Domena", "done_action": "Gotowe", "doNotAskAgain_label": "Nie pytaj ponownie dla tego pliku", + "dontAskAgain_label": "Nie pytaj więcej", "downloadCompleted_msg": "Pobieranie zakończone", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -723,6 +724,7 @@ export default { "importContactsError_msg": "{amount} z {total} kontaktów nie można zaimportować.", "importContacts_label": "Importuj kontakty", "importContacts_msg": "Importuj kontakty z książki telefonicznej, aby udostępniać je na wszystkich urządzeniach.", + "importedMailsWillBeDeleted_label": "Wszystkie ważne wiadomości e-mail zostaną usunięte", "importEndNotAfterStartInEvent_msg": "{amount} z {total} wydarzeń nie zaczynają się przed ich końcem i nie zostaną zaimportowane.", "importEventExistingUid_msg": "{amount} z {total} wydarzeń już istnieje i nie są nadpisywane. Pozostałe wydarzenia będą przetwarzane...", "importEventsError_msg": "{amount} z {total} wydarzeń nie można zaimportować.", @@ -1059,6 +1061,7 @@ export default { "notificationSync_msg": "Synchronizowanie zawiadomień", "notificationTargets_label": "Cele powiadomień", "noTitle_label": "", + "notNow_label": "Nie teraz", "notSigned_msg": "Niepodpisany.", "noUpdateAvailable_msg": "Nie znaleziono uaktualnienia.", "noValidMembersToAdd_msg": "Nie administrujesz żadnymi użytkownikami, którzy nie są już członkami tej grupy.", @@ -1337,6 +1340,10 @@ export default { "quitDNSSetup_msg": "Skonfiguruj wszystkie rekordy DNS zgodnie ze wskazówkami. W innym wypadku nie będzie możliwe używanie własnej domeny w Tuta. ", "quitSetup_title": "Zakończyć konfigurację?", "quit_action": "Wyjdź", + "ratingExplanation_msg": "Niezależnie od tego, czy kochasz Tuta, czy uważasz, że moglibyśmy coś poprawić, daj nam znać!", + "ratingHowAreWeDoing_title": "Jak ci się podoba Tuta?", + "ratingLoveIt_label": "Uwielbiam to!", + "ratingNeedsWork_label": "Potrzebuje poprawy", "readResponse_action": "Przeczytaj odpowiedź", "reallySubmitContent_msg": "Czy naprawdę chcesz wysłać wprowadzone dane do zewnętrznej witryny?", "receiveCalendarNotifications_label": "Otrzymuj powiadomienia o wydarzeniach z kalendarza", diff --git a/src/mail-app/translations/ru.ts b/src/mail-app/translations/ru.ts index 4160a49cb84..2064c23fbd4 100644 --- a/src/mail-app/translations/ru.ts +++ b/src/mail-app/translations/ru.ts @@ -15,7 +15,7 @@ export default { "other" ], "created_at": "2015-01-27T13:15:23Z", - "updated_at": "2024-11-27T00:42:45Z", + "updated_at": "2024-11-30T20:48:38Z", "source_locale": { "id": "fcd7471b347c8e517663e194dcddf237", "name": "en", @@ -306,6 +306,7 @@ export default { "confirmDeleteCustomFolder_msg": "Действительно ли хотите переместить в корзину папку «{1}» со всем её содержимым (например, письмами и вложенными папками)?\n\nПапки без писем удаляются навсегда.", "confirmDeleteFinallyCustomFolder_msg": "Вы действительно хотите безвозвратно удалить папку '{1}' и все письма в ней? В зависимости от числа писем эта операция может занять длительное время и будет выполнена в фоновом режиме.", "confirmDeleteFinallySystemFolder_msg": "Вы уверены, что хотите удалить всю почту из системной папки '{1}' навсегда? При большом числе писем, эта операция может занять длительное время и будет выполняться в фоновом режиме.", + "confirmDeleteLabel_msg": "Вы уверены, что хотите удалить ярлык «{1}»?", "confirmDeleteSecondFactor_msg": "Вы действительно хотите отключить авторизацию с одноразовыми паролями?", "confirmDeleteTemplateGroup_msg": "Вы уверены, что хотите удалить этот список шаблонов? Все включенные в него шаблоны будут потеряны и не смогут быть восстановлены.", "confirmFreeAccount_label": "Подтверждение бесплатной учётной записи", @@ -516,6 +517,7 @@ export default { "doNotAskAgain_label": "Больше не спрашивать для этого файла", "downloadCompleted_msg": "Загрузка завершена", "downloadInvoicePdf_action": "PDF (PDF/A)", + "downloadInvoiceXml_action": "XML (XRechnung)", "download_action": "Скачать", "draftNotSavedConnectionLost_msg": "Черновик не сохранен (потеряно соединение).", "draftNotSaved_msg": "Черновик не сохранён.", @@ -603,6 +605,8 @@ export default { "experienceSamplingSelectAnswer_msg": "Пожалуйста, выберите ответ на все вопросы.", "experienceSamplingThankYou_msg": "Благодарим вас за участие!", "expiredLink_msg": "К сожалению, эта ссылка больше не действительна.  Вам должно было прийти новое уведом­ле­ние по электронной почте с актуальной ссылкой.  Предыдущие ссылки отключены ради безопасности.", + "exportingEmails_label": "Экспорт электронных писем: {count}", + "exportMailbox_label": "Экспорт почтового ящика", "exportUsers_action": "Экспортировать пользователей", "exportVCard_action": "Экспорт vCard", "export_action": "Скачать как .eml", @@ -712,12 +716,14 @@ export default { "iCalNotSync_msg": "Не синхронизирован.", "iCalSync_error": "Ошибка при синхронизации, один или несколько календарей оказались недействительными.", "icsInSharingFiles_msg": "Обнаружен один или несколько файлов календаря. Вы хотите импортировать или прикрепить их?", + "importantLabel_label": "Важно", "importCalendar_label": "Импорт календаря", "importContactRemoveDuplicatesConfirm_msg": "При синхронизации на вашем устройстве было обнаружено {count} дубликатов контактов. Хотите ли вы удалить их с устройства? Обратите внимание, что это нельзя отменить.", "importContactRemoveImportedContactsConfirm_msg": "Вы хотите удалить импортированные контакты с вашего устройства? Обратите внимание, что это нельзя отменить.", "importContactsError_msg": "{amount} из {total} контактов не могут быть импортированы.", "importContacts_label": "Импортировать контакты", "importContacts_msg": "Импортируйте контакты из телефонной книги, чтобы сделать их доступными на всех устройствах.", + "importedMailsWillBeDeleted_label": "Все импортированные письма будут удалены", "importEndNotAfterStartInEvent_msg": "{amount} из {total} событий не имеют даты начала перед датой окончания и не будут импортированы.", "importEventExistingUid_msg": "{amount} из {total} событий уже существуют и не перезаписаны. Будет продолжено с оставшимися событиями...", "importEventsError_msg": "{amount} из {total} событий не могут быть импортированы. ", @@ -881,6 +887,7 @@ export default { "largeSignature_msg": "Размер подписи превышает {1} Кб. Она будет добавлена к каждому письму по умолчанию. Вы всё равно хотите использовать её?", "lastAccessWithTime_label": "Последний доступ: {время}", "lastAccess_label": "Время", + "lastExportTime_Label": "Последний экспорт: {date}", "lastName_placeholder": "Фамилия", "lastSync_label": "Последняя синхронизация: {date}", "laterInvoicingInfo_msg": "Информация: Дополнительно заказанные функции будут выставлены в счёте в начале следующего месяца подписки.", @@ -933,6 +940,7 @@ export default { "mailAuthMissing_label": "Мы не смогли доказать подлинность отправителя или содержания этого сообщения.", "mailBodyTooLarge_msg": "Ваше письмо не удалось отправить на сервер, поскольку основной текст превышает максимальный размер в 1 МБ.", "mailBody_label": "Тело почты", + "mailboxToExport_label": "Почтовый ящик для экспорта", "mailbox_label": "Почтовый ящик", "mailExportModeHelp_msg": "Формат файла электронной почты для использования при экспорте или перетаскивании", "mailExportMode_label": "Формат экспорта почты", @@ -943,6 +951,7 @@ export default { "mailName_label": "Имя отправителя", "mailPartsNotLoaded_msg": "Некоторые части письма не удалось загрузить из-за потери соединения.", "mailServer_label": "Почтовый сервер", + "mailsExported_label": "Экспортированные письма: {numbers}", "mailViewerRecipients_label": "кому:", "mailView_action": "Переключение на просмотр электронной почты", "makeAdminPendingUserGroupKeyRotationError_msg": "В настоящее время пользователь не может стать администратором. Поопросите пользователя выйти из системы на всех устройствах, а затем снова войти в систему. После этого повторите попытку.", @@ -1242,6 +1251,7 @@ export default { "pricing.custom_title": "Пользовательский брендинг", "pricing.custom_tooltip": "Tuta Whitelabel с вашим собственным брендом, определенными логотипами и цветами веб-, мобильных и настольных клиентов Tuta.", "pricing.cyberMonday_label": "Сэкономьте 62%", + "pricing.cyber_monday_msg": "Получите план высшего уровня на первый год за меньшую сумму!", "pricing.cyber_monday_select_action": "Получите выгодное предложение!", "pricing.encryptedCalendar_label": "Полностью зашифрованный календарь", "pricing.encryptedCalendar_tooltip": "Все данные в ваших календарях Tuta зашифрованы, даже уведомления отправляются на ваше устройство в зашифрованном виде.", @@ -1262,6 +1272,7 @@ export default { "pricing.gdpr_tooltip": "Все данные хранятся в соответствии со строгими европейскими правилами защиты данных согласно GDPR.", "pricing.getStarted_label": "Начать", "pricing.includesTaxes_msg": "Включая налоги.", + "pricing.legendAsterisk_msg": "Скидка на Legend действует только в течение первого года. После этого цена составит 96 евро в год.", "pricing.login_title": "Вход в почту на своём сайте", "pricing.login_tooltip": "Разместите логин Tuta на своем сайте для сотрудников и внешних пользователей.", "pricing.mailAddressAliasesShort_label": "{amount} дополнительных адресов электронной почты", @@ -1317,6 +1328,7 @@ export default { "privateCalendar_label": "Приватный", "private_label": "Личный", "progressDeleting_msg": "Удаление ...", + "promotion.oneYear_msg": "Специальное предложение: Приватная и защищенная электронная почта на один год в подарок", "pronouns_label": "Местоимения", "providePaymentDetails_msg": "Пожалуйста, укажите данные об оплате", "purchaseDate_label": "Дата приобретения", @@ -1327,6 +1339,9 @@ export default { "quitDNSSetup_msg": "Пожалуйста, настройте все DNS записи как указано. В противном случае вы не сможете использовать ваш кастомный домен с Tuta.", "quitSetup_title": "Выйти из настройки?", "quit_action": "Выйти", + "ratingExplanation_msg": "Нравится ли вам Tuta или вы считаете, что мы можем улучшить ее, дайте нам знать!", + "ratingHowAreWeDoing_title": "Нравится ли вам Тута?", + "ratingNeedsWork_label": "Needs work", "readResponse_action": "Прочитать ответ", "reallySubmitContent_msg": "Действительно ли хотите отправить введённые данные на внешний сайт?", "receiveCalendarNotifications_label": "Получайте уведомления о событиях в календаре", diff --git a/src/mail-app/translations/sv.ts b/src/mail-app/translations/sv.ts index d5310d328e1..b33da3d1b38 100644 --- a/src/mail-app/translations/sv.ts +++ b/src/mail-app/translations/sv.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-03-23T11:36:16Z", - "updated_at": "2024-11-28T12:52:05Z", + "updated_at": "2024-12-02T14:44:50Z", "source_locale": null, "fallback_locale": null, "keys": { @@ -509,6 +509,7 @@ export default { "domain_label": "Domännamn", "done_action": "Klart", "doNotAskAgain_label": "Fråga inte igen efter den här filen", + "dontAskAgain_label": "Fråga inte igen", "downloadCompleted_msg": "Nedladdningen slutförd", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -1055,6 +1056,7 @@ export default { "notificationSync_msg": "Synkroniseringsnotiser", "notificationTargets_label": "Destination för avisering", "noTitle_label": "", + "notNow_label": "Inte nu", "notSigned_msg": "Inte signerad.", "noUpdateAvailable_msg": "Ingen uppdatering hittades.", "noValidMembersToAdd_msg": "Du administrerar inga användare som inte redan är medlemmar i den här gruppen.", @@ -1336,6 +1338,7 @@ export default { "quit_action": "Avsluta", "ratingExplanation_msg": "Oavsett om du älskar Tuta eller tycker att vi kan bli bättre låt oss få veta det!", "ratingHowAreWeDoing_title": "Vad tycker du om Tuta?", + "ratingLoveIt_label": "Älskar den!", "ratingNeedsWork_label": "Behöver förbättras", "readResponse_action": "Läs svar", "reallySubmitContent_msg": "Vill du verkligen skicka inmatade data till en extern plats?", diff --git a/src/mail-app/translations/tr.ts b/src/mail-app/translations/tr.ts index 59afefa696f..32c95607227 100644 --- a/src/mail-app/translations/tr.ts +++ b/src/mail-app/translations/tr.ts @@ -13,7 +13,7 @@ export default { "other" ], "created_at": "2015-01-27T13:15:41Z", - "updated_at": "2024-11-27T04:14:39Z", + "updated_at": "2024-12-02T20:08:19Z", "source_locale": { "id": "fcd7471b347c8e517663e194dcddf237", "name": "en", @@ -513,6 +513,7 @@ export default { "domain_label": "Alan adı", "done_action": "Tamamlandı", "doNotAskAgain_label": "Bu dosya için bir daha sorma", + "dontAskAgain_label": "Tekrar sorma", "downloadCompleted_msg": "İndirme tamamlandı", "downloadInvoicePdf_action": "PDF (PDF/A)", "downloadInvoiceXml_action": "XML (XRechnung)", @@ -721,6 +722,7 @@ export default { "importContactsError_msg": "{total} kişiden {amount} tanesi içe aktarılamadı.", "importContacts_label": "Kişileri içe aktar", "importContacts_msg": "Bütün cihazlarınızda kullanılabilir hale getirmek için kişileri telefon defterinizden içe aktarın.", + "importedMailsWillBeDeleted_label": "Tüm içe aktarılan e-postalar silinecektir", "importEndNotAfterStartInEvent_msg": "{amount} {total} etkinliklerinin başlangıç tarihi bitiş tarihinden önce değildir ve içe aktarılmayacaktır.", "importEventExistingUid_msg": "{total} etkinlikten {amount} tanesi zaten var ve üzerine yazılmadı. Kalan etkinliklerle devam edilecek...", "importEventsError_msg": "{total} etkinlikten {amount} tanesi içeri aktarılamadı.", @@ -1058,6 +1060,7 @@ export default { "notificationSync_msg": "Bildirimler senkronize ediliyor", "notificationTargets_label": "Bildirim hedefleri", "noTitle_label": "", + "notNow_label": "Şimdi değil", "notSigned_msg": "İmzalanmadı.", "noUpdateAvailable_msg": "Güncelleme bulunamadı.", "noValidMembersToAdd_msg": "Bu grubun üyesi olmayan hiçbir kullanıcıyı yönetmiyorsunuz.", @@ -1337,6 +1340,10 @@ export default { "quitDNSSetup_msg": "Lütfen tüm DNS kayıtlarını belirtildiği şekilde ayarlayın. Aksi takdirde, Tuta'da özel alan adınızı kullanamazsınız.", "quitSetup_title": "Kurulumdan çık?", "quit_action": "Çıkış", + "ratingExplanation_msg": "Tuta'yı beğenip beğenmediğinizi veya onu iyileştirmemiz gerektiğini bize söyleyin!", + "ratingHowAreWeDoing_title": "Tuta'yı nasıl buluyorsunuz?", + "ratingLoveIt_label": "Beğeniyorum!", + "ratingNeedsWork_label": "Üzerinde çalışmak lazım", "readResponse_action": "Cevabı oku", "reallySubmitContent_msg": "Girilen verileri gerçekten harici bir siteye göndermek istiyor musunuz?", "receiveCalendarNotifications_label": "Takvim etkinlik bildirimlerini al", diff --git a/src/mail-app/translations/zh_hant.ts b/src/mail-app/translations/zh_hant.ts index 2b8f1d7d496..cb439d84249 100644 --- a/src/mail-app/translations/zh_hant.ts +++ b/src/mail-app/translations/zh_hant.ts @@ -12,7 +12,7 @@ export default { "other" ], "created_at": "2019-01-02T11:09:03Z", - "updated_at": "2024-11-28T17:49:43Z", + "updated_at": "2024-12-02T17:19:39Z", "source_locale": null, "fallback_locale": null, "keys": { @@ -226,7 +226,7 @@ export default { "cancellationReasonOtherFeature_label": "欠缺其他功能", "cancellationReasonOther_label": "其他原因", "cancellationReasonPrice_label": "服務太貴", - "cancellationReasonSearch_label": "電郵搜尋耗時太長", + "cancellationReasonSearch_label": "電郵搜尋需時太長", "cancellationReasonSpam_label": "我收到太多垃圾電郵", "cancellationReasonUI_label": "我不喜歡Tuta的外觀", "cancellationReasonUsability_label": "Tuta太難用", @@ -507,7 +507,8 @@ export default { "domainStillHasContactForms_msg": "{domain}無法停用,因為白標域名上仍然有使用中的聯絡表格。停用{domain}前,請先刪除聯絡表格。", "domain_label": "域名", "done_action": "完成", - "doNotAskAgain_label": "不再對此檔案詢問", + "doNotAskAgain_label": "就此檔案不要再詢問", + "dontAskAgain_label": "不要再詢問", "downloadCompleted_msg": "下載完成", "downloadInvoicePdf_action": "PDF(PDF/A)", "downloadInvoiceXml_action": "XML(XRechnung)", @@ -1054,6 +1055,7 @@ export default { "notificationSync_msg": "同步通知", "notificationTargets_label": "通知目標", "noTitle_label": "<無標題>", + "notNow_label": "現在不", "notSigned_msg": "未簽署。", "noUpdateAvailable_msg": "沒有更新。", "noValidMembersToAdd_msg": "您沒有管理任何不已是此群組成員的用戶。", @@ -1334,6 +1336,7 @@ export default { "quit_action": "離開", "ratingExplanation_msg": "無論您是喜歡Tuta還是覺得我們可以改善,請讓我們知道!", "ratingHowAreWeDoing_title": "您覺得Tuta怎麼樣?", + "ratingLoveIt_label": "喜歡!", "ratingNeedsWork_label": "需要改進", "readResponse_action": "閱讀回覆", "reallySubmitContent_msg": "您確定要發送已輸入的資料到外部網站嗎?", From f38581510afaa96cebdb4b31fcbd5142e18e46f5 Mon Sep 17 00:00:00 2001 From: jenkins build server Date: Tue, 3 Dec 2024 10:36:36 +0100 Subject: [PATCH 2/6] v253.241203.0 --- app-android/app/build.gradle | 4 +- app-android/calendar/build.gradle.kts | 4 +- .../TutanotaNotificationExtension/Info.plist | 4 +- app-ios/calendar/Info.plist | 4 +- app-ios/tutanota/Info.plist | 4 +- package-lock.json | 52 +++++++++---------- package.json | 18 +++---- packages/licc/package.json | 6 +-- packages/otest/package.json | 2 +- packages/tuta-wasm-loader/package.json | 2 +- packages/tutanota-crypto/package.json | 8 +-- packages/tutanota-error/package.json | 2 +- packages/tutanota-test-utils/package.json | 4 +- packages/tutanota-usagetests/package.json | 4 +- packages/tutanota-utils/package.json | 4 +- tuta-sdk/rust/Cargo.lock | 2 +- tuta-sdk/rust/sdk/Cargo.toml | 2 +- 17 files changed, 63 insertions(+), 63 deletions(-) diff --git a/app-android/app/build.gradle b/app-android/app/build.gradle index 34a8bb8dd45..0a23a062912 100644 --- a/app-android/app/build.gradle +++ b/app-android/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "de.tutao.tutanota" minSdkVersion 26 targetSdkVersion 34 - versionCode 396419 - versionName "253.241129.0" + versionCode 396420 + versionName "253.241203.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // https://issuetracker.google.com/issues/181593646 diff --git a/app-android/calendar/build.gradle.kts b/app-android/calendar/build.gradle.kts index 58e4990da9e..c3d2b9078bc 100644 --- a/app-android/calendar/build.gradle.kts +++ b/app-android/calendar/build.gradle.kts @@ -18,8 +18,8 @@ android { applicationId = "de.tutao.calendar" minSdk = 26 targetSdk = 34 - versionCode = 58 - versionName = "253.241129.0" + versionCode = 59 + versionName = "253.241203.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app-ios/TutanotaNotificationExtension/Info.plist b/app-ios/TutanotaNotificationExtension/Info.plist index 3334324967d..b5daf8fad0b 100644 --- a/app-ios/TutanotaNotificationExtension/Info.plist +++ b/app-ios/TutanotaNotificationExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleShortVersionString - 253.241129.0 + 253.241203.0 CFBundleVersion - 253.241129.0 + 253.241203.0 NSExtension NSExtensionPointIdentifier diff --git a/app-ios/calendar/Info.plist b/app-ios/calendar/Info.plist index 5518a420573..de00de564a5 100644 --- a/app-ios/calendar/Info.plist +++ b/app-ios/calendar/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 253.241129.0 + 253.241203.0 CFBundleURLTypes @@ -33,7 +33,7 @@ CFBundleVersion - 253.241129.0 + 253.241203.0 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/app-ios/tutanota/Info.plist b/app-ios/tutanota/Info.plist index 288600a9298..01872044e28 100644 --- a/app-ios/tutanota/Info.plist +++ b/app-ios/tutanota/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 253.241129.0 + 253.241203.0 CFBundleURLTypes @@ -34,7 +34,7 @@ CFBundleVersion - 253.241129.0 + 253.241203.0 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/package-lock.json b/package-lock.json index 47edbad5e3e..99290357cb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tutanota", - "version": "253.241129.0", + "version": "253.241203.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tutanota", - "version": "253.241129.0", + "version": "253.241203.0", "hasInstallScript": true, "license": "GPL-3.0", "workspaces": [ @@ -14,11 +14,11 @@ ], "dependencies": { "@tutao/oxmsg": "0.0.9-beta.0", - "@tutao/tuta-wasm-loader": "253.241129.0", - "@tutao/tutanota-crypto": "253.241129.0", - "@tutao/tutanota-error": "253.241129.0", - "@tutao/tutanota-usagetests": "253.241129.0", - "@tutao/tutanota-utils": "253.241129.0", + "@tutao/tuta-wasm-loader": "253.241203.0", + "@tutao/tutanota-crypto": "253.241203.0", + "@tutao/tutanota-error": "253.241203.0", + "@tutao/tutanota-usagetests": "253.241203.0", + "@tutao/tutanota-utils": "253.241203.0", "@types/better-sqlite3": "7.4.2", "@types/dompurify": "3.0.5", "@types/linkifyjs": "2.1.7", @@ -53,9 +53,9 @@ "@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "11.1.6", - "@tutao/licc": "253.241129.0", - "@tutao/otest": "253.241129.0", - "@tutao/tutanota-test-utils": "253.241129.0", + "@tutao/licc": "253.241203.0", + "@tutao/otest": "253.241203.0", + "@tutao/tutanota-test-utils": "253.241203.0", "@types/express": "^4.17.17", "@types/pako": "^2.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -10955,7 +10955,7 @@ }, "packages/licc": { "name": "@tutao/licc", - "version": "253.241129.0", + "version": "253.241203.0", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { @@ -10967,8 +10967,8 @@ "licc": "dist/cli.js" }, "devDependencies": { - "@tutao/otest": "253.241129.0", - "@tutao/tutanota-test-utils": "253.241129.0", + "@tutao/otest": "253.241203.0", + "@tutao/tutanota-test-utils": "253.241203.0", "typescript": "5.3.3" } }, @@ -11072,7 +11072,7 @@ }, "packages/otest": { "name": "@tutao/otest", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "devDependencies": { "typescript": "5.3.3" @@ -11080,7 +11080,7 @@ }, "packages/tuta-wasm-loader": { "name": "@tutao/tuta-wasm-loader", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "devDependencies": { "typescript": "5.3.3" @@ -11088,20 +11088,20 @@ }, "packages/tutanota-crypto": { "name": "@tutao/tutanota-crypto", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "dependencies": { - "@tutao/tutanota-error": "253.241129.0" + "@tutao/tutanota-error": "253.241203.0" }, "devDependencies": { - "@tutao/otest": "253.241129.0", - "@tutao/tutanota-utils": "253.241129.0", + "@tutao/otest": "253.241203.0", + "@tutao/tutanota-utils": "253.241203.0", "typescript": "5.3.3" } }, "packages/tutanota-error": { "name": "@tutao/tutanota-error", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "devDependencies": { "typescript": "5.3.3" @@ -11109,10 +11109,10 @@ }, "packages/tutanota-test-utils": { "name": "@tutao/tutanota-test-utils", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "dependencies": { - "@tutao/otest": "253.241129.0", + "@tutao/otest": "253.241203.0", "testdouble": "3.18.0" }, "devDependencies": { @@ -11121,20 +11121,20 @@ }, "packages/tutanota-usagetests": { "name": "@tutao/tutanota-usagetests", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GLP-3.0", "devDependencies": { - "@tutao/otest": "253.241129.0", + "@tutao/otest": "253.241203.0", "@types/node-forge": "1.0.0", "typescript": "5.3.3" } }, "packages/tutanota-utils": { "name": "@tutao/tutanota-utils", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "devDependencies": { - "@tutao/otest": "253.241129.0", + "@tutao/otest": "253.241203.0", "typescript": "5.3.3" } } diff --git a/package.json b/package.json index ac49d5ea364..3bef114ee6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tutanota", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "repository": { "type": "git", @@ -33,11 +33,11 @@ }, "dependencies": { "@tutao/oxmsg": "0.0.9-beta.0", - "@tutao/tuta-wasm-loader": "253.241129.0", - "@tutao/tutanota-crypto": "253.241129.0", - "@tutao/tutanota-error": "253.241129.0", - "@tutao/tutanota-usagetests": "253.241129.0", - "@tutao/tutanota-utils": "253.241129.0", + "@tutao/tuta-wasm-loader": "253.241203.0", + "@tutao/tutanota-crypto": "253.241203.0", + "@tutao/tutanota-error": "253.241203.0", + "@tutao/tutanota-usagetests": "253.241203.0", + "@tutao/tutanota-utils": "253.241203.0", "@types/better-sqlite3": "7.4.2", "@types/dompurify": "3.0.5", "@types/linkifyjs": "2.1.7", @@ -75,9 +75,9 @@ "@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "11.1.6", - "@tutao/licc": "253.241129.0", - "@tutao/otest": "253.241129.0", - "@tutao/tutanota-test-utils": "253.241129.0", + "@tutao/licc": "253.241203.0", + "@tutao/otest": "253.241203.0", + "@tutao/tutanota-test-utils": "253.241203.0", "@types/express": "^4.17.17", "@types/pako": "^2.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", diff --git a/packages/licc/package.json b/packages/licc/package.json index 2b82ac0f4a9..aacf3327661 100644 --- a/packages/licc/package.json +++ b/packages/licc/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/licc", - "version": "253.241129.0", + "version": "253.241203.0", "bin": { "licc": "dist/cli.js" }, @@ -21,7 +21,7 @@ }, "devDependencies": { "typescript": "5.3.3", - "@tutao/tutanota-test-utils": "253.241129.0", - "@tutao/otest": "253.241129.0" + "@tutao/tutanota-test-utils": "253.241203.0", + "@tutao/otest": "253.241203.0" } } diff --git a/packages/otest/package.json b/packages/otest/package.json index f62821c591e..628b4efe17e 100644 --- a/packages/otest/package.json +++ b/packages/otest/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/otest", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "description": "little test runner", "main": "./dist/index.js", diff --git a/packages/tuta-wasm-loader/package.json b/packages/tuta-wasm-loader/package.json index bc50203f32c..dbd9ce4c963 100644 --- a/packages/tuta-wasm-loader/package.json +++ b/packages/tuta-wasm-loader/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/tuta-wasm-loader", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/tutanota-crypto/package.json b/packages/tutanota-crypto/package.json index 9721dcc6a1f..9a50eac05d4 100644 --- a/packages/tutanota-crypto/package.json +++ b/packages/tutanota-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/tutanota-crypto", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "main": "./dist/index.js", "exports": { @@ -25,11 +25,11 @@ "tsconfig.json" ], "dependencies": { - "@tutao/tutanota-error": "253.241129.0" + "@tutao/tutanota-error": "253.241203.0" }, "devDependencies": { "typescript": "5.3.3", - "@tutao/tutanota-utils": "253.241129.0", - "@tutao/otest": "253.241129.0" + "@tutao/tutanota-utils": "253.241203.0", + "@tutao/otest": "253.241203.0" } } diff --git a/packages/tutanota-error/package.json b/packages/tutanota-error/package.json index 17d6085b1d6..834446e3899 100644 --- a/packages/tutanota-error/package.json +++ b/packages/tutanota-error/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/tutanota-error", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/tutanota-test-utils/package.json b/packages/tutanota-test-utils/package.json index ce651b31e67..aad89da610f 100644 --- a/packages/tutanota-test-utils/package.json +++ b/packages/tutanota-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/tutanota-test-utils", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "main": "./dist/index.js", "repository": { @@ -21,7 +21,7 @@ "tsconfig.json" ], "dependencies": { - "@tutao/otest": "253.241129.0", + "@tutao/otest": "253.241203.0", "testdouble": "3.18.0" }, "devDependencies": { diff --git a/packages/tutanota-usagetests/package.json b/packages/tutanota-usagetests/package.json index 672fa0285e0..c188aa56193 100644 --- a/packages/tutanota-usagetests/package.json +++ b/packages/tutanota-usagetests/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/tutanota-usagetests", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GLP-3.0", "description": "", "main": "./dist/index.js", @@ -26,6 +26,6 @@ "devDependencies": { "@types/node-forge": "1.0.0", "typescript": "5.3.3", - "@tutao/otest": "253.241129.0" + "@tutao/otest": "253.241203.0" } } diff --git a/packages/tutanota-utils/package.json b/packages/tutanota-utils/package.json index b9060b733a0..a118b97dbc1 100644 --- a/packages/tutanota-utils/package.json +++ b/packages/tutanota-utils/package.json @@ -1,6 +1,6 @@ { "name": "@tutao/tutanota-utils", - "version": "253.241129.0", + "version": "253.241203.0", "license": "GPL-3.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -23,6 +23,6 @@ ], "devDependencies": { "typescript": "5.3.3", - "@tutao/otest": "253.241129.0" + "@tutao/otest": "253.241203.0" } } diff --git a/tuta-sdk/rust/Cargo.lock b/tuta-sdk/rust/Cargo.lock index b67aff3781c..c2aef458825 100644 --- a/tuta-sdk/rust/Cargo.lock +++ b/tuta-sdk/rust/Cargo.lock @@ -2638,7 +2638,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tuta-sdk" -version = "253.241129.0" +version = "253.241203.0" dependencies = [ "aes", "android_log", diff --git a/tuta-sdk/rust/sdk/Cargo.toml b/tuta-sdk/rust/sdk/Cargo.toml index a59ec6811b1..814765825d0 100644 --- a/tuta-sdk/rust/sdk/Cargo.toml +++ b/tuta-sdk/rust/sdk/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "tuta-sdk" -version = "253.241129.0" +version = "253.241203.0" [dependencies] async-trait = "0.1.77" From 9a686a5aa6e8af2097e3937537f7e44fa14df029 Mon Sep 17 00:00:00 2001 From: jug Date: Thu, 21 Nov 2024 11:23:00 +0100 Subject: [PATCH 3/6] Add iOS in-app rating mechanism - Own dialog, then, if user is happy, open native iOS in app rating dialog - Triggers: -- After using the app for minimum 7 days, we can show the dialog after the user created three events / after three mails -- When the user did 10 activities within a 28-day period. (sending and email creating event. ) -- After succeeding the paywall - Use device config to store data needed to trigger the dialog - Added tests - Added method to get the native app's installation date (Android & iOS) - Added comment about `SKStoreReviewController.requestReview()` deprecation Co-authored-by: arm Co-authored-by: jat --- .../tutanota/AndroidMobileSystemFacade.kt | 6 + .../calendar/AndroidMobileSystemFacade.kt | 5 + .../java/de/tutao/tutashared/SystemUtils.kt | 22 ++ .../generated_ipc/MobileSystemFacade.kt | 10 + .../MobileSystemFacadeReceiveDispatcher.kt | 10 + .../GeneratedIpc/MobileSystemFacade.swift | 10 + .../MobileSystemFacadeReceiveDispatcher.swift | 8 + .../Sources/IosMobileSystemFacade.swift | 11 + .../Sources/IosMobileSystemFacade.swift | 14 ++ buildSrc/RollupConfig.js | 3 +- ipc-schema/facades/MobileSystemFacade.json | 10 + resources/images/rating/calendar.png | Bin 0 -> 31871 bytes resources/images/rating/mail.png | Bin 0 -> 31533 bytes .../CalendarEventEditDialog.ts | 4 + src/common/gui/base/Dialog.ts | 3 +- src/common/misc/DeviceConfig.ts | 82 +++++++ src/common/misc/TranslationKey.ts | 5 + .../common/generatedipc/MobileSystemFacade.ts | 10 + .../MobileSystemFacadeSendDispatcher.ts | 6 + src/common/native/main/wizard/SetupWizard.ts | 5 - src/common/ratings/InAppRatingDialog.ts | 166 ++++++++++++++ src/common/ratings/InAppRatingUtils.ts | 91 ++++++++ .../UpgradeConfirmSubscriptionPage.ts | 12 + src/mail-app/mail/editor/MailEditor.ts | 4 + src/mail-app/settings/SettingsView.ts | 1 + src/mail-app/translations/en.ts | 8 +- test/tests/Suite.ts | 1 + test/tests/misc/DeviceConfigTest.ts | 3 + test/tests/misc/InAppRatingUtilsTest.ts | 215 ++++++++++++++++++ 29 files changed, 716 insertions(+), 9 deletions(-) create mode 100644 app-android/tutashared/src/main/java/de/tutao/tutashared/SystemUtils.kt create mode 100644 resources/images/rating/calendar.png create mode 100644 resources/images/rating/mail.png create mode 100644 src/common/ratings/InAppRatingDialog.ts create mode 100644 src/common/ratings/InAppRatingUtils.ts create mode 100644 test/tests/misc/InAppRatingUtilsTest.ts diff --git a/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt b/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt index 478c63280fe..1b41f80ae33 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt @@ -21,6 +21,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.IOException +import de.tutao.tutashared.SystemUtils + class AndroidMobileSystemFacade( private val fileFacade: AndroidFileFacade, @@ -168,4 +170,8 @@ class AndroidMobileSystemFacade( override suspend fun openMailApp(query: String) { Log.e(TAG, "Trying to open Tuta Mail from Tuta Mail") } + + override suspend fun getInstallationDate(): String { + return SystemUtils.getInstallationDate(activity.packageManager, activity.packageName) + } } \ No newline at end of file diff --git a/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt b/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt index 0517947450d..404846ad01a 100644 --- a/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt +++ b/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt @@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat.startActivity import androidx.core.content.FileProvider import androidx.fragment.app.FragmentActivity import de.tutao.tutashared.CredentialAuthenticationException +import de.tutao.tutashared.SystemUtils import de.tutao.tutashared.atLeastTiramisu import de.tutao.tutashared.credentials.AuthenticationPrompt import de.tutao.tutashared.data.AppDatabase @@ -195,4 +196,8 @@ class AndroidMobileSystemFacade( tryToLaunchStore() } } + + override suspend fun getInstallationDate(): String { + return SystemUtils.getInstallationDate(activity.packageManager, activity.packageName) + } } \ No newline at end of file diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/SystemUtils.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/SystemUtils.kt new file mode 100644 index 00000000000..7fe3ac29bc7 --- /dev/null +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/SystemUtils.kt @@ -0,0 +1,22 @@ +package de.tutao.tutashared + +import android.content.pm.PackageManager +import java.io.File + +sealed class SystemUtils { + companion object { + /** + * Returns the installation time of a package in UNIX Epoch time. + * Adapted from https://stackoverflow.com/a/2832419 + */ + @JvmStatic + fun getInstallationDate(pm: PackageManager, packageName: String): String { + val appInfo = pm.getApplicationInfo(packageName, 0) + val appFile = appInfo.sourceDir + val installedTime = File(appFile).lastModified() //Epoch Time + return installedTime.toString() + } + } +} + + diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacade.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacade.kt index 3e402dbdaf8..e96a9b79d46 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacade.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacade.kt @@ -53,4 +53,14 @@ interface MobileSystemFacade { suspend fun openMailApp( query: String, ): Unit + /** + * Returns the date and time the app was installed as a string with milliseconds in UNIX epoch. + */ + suspend fun getInstallationDate( + ): String + /** + * Requests the system in-app rating dialog to be displayed + */ + suspend fun requestInAppRating( + ): Unit } diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacadeReceiveDispatcher.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacadeReceiveDispatcher.kt index 480f42faf0d..d80bc7b7394 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacadeReceiveDispatcher.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/MobileSystemFacadeReceiveDispatcher.kt @@ -80,6 +80,16 @@ class MobileSystemFacadeReceiveDispatcher( ) return json.encodeToString(result) } + "getInstallationDate" -> { + val result: String = this.facade.getInstallationDate( + ) + return json.encodeToString(result) + } + "requestInAppRating" -> { + val result: Unit = this.facade.requestInAppRating( + ) + return json.encodeToString(result) + } else -> throw Error("unknown method for MobileSystemFacade: $method") } } diff --git a/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacade.swift b/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacade.swift index a23820c7f90..36d0e3626e3 100644 --- a/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacade.swift +++ b/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacade.swift @@ -50,4 +50,14 @@ public protocol MobileSystemFacade { func openMailApp( _ query: String ) async throws -> Void + /** + * Returns the date and time the app was installed as a string with milliseconds in UNIX epoch. + */ + func getInstallationDate( + ) async throws -> String + /** + * Requests the system in-app rating dialog to be displayed + */ + func requestInAppRating( + ) async throws -> Void } diff --git a/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacadeReceiveDispatcher.swift b/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacadeReceiveDispatcher.swift index edaab3eee86..562994c6d91 100644 --- a/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacadeReceiveDispatcher.swift +++ b/app-ios/TutanotaSharedFramework/GeneratedIpc/MobileSystemFacadeReceiveDispatcher.swift @@ -66,6 +66,14 @@ public class MobileSystemFacadeReceiveDispatcher { query ) return "null" + case "getInstallationDate": + let result = try await self.facade.getInstallationDate( + ) + return toJson(result) + case "requestInAppRating": + try await self.facade.requestInAppRating( + ) + return "null" default: fatalError("licc messed up! \(method)") } diff --git a/app-ios/calendar/Sources/IosMobileSystemFacade.swift b/app-ios/calendar/Sources/IosMobileSystemFacade.swift index 0cef8b75bda..90a90bd4807 100644 --- a/app-ios/calendar/Sources/IosMobileSystemFacade.swift +++ b/app-ios/calendar/Sources/IosMobileSystemFacade.swift @@ -1,4 +1,5 @@ import Contacts +import StoreKit import Foundation import TutanotaSharedFramework @@ -93,4 +94,14 @@ class IosMobileSystemFacade: MobileSystemFacade { DispatchQueue.main.async { UIApplication.shared.open(URL(string: "https://itunes.apple.com/us/app/id922429609")!) } } } + func getInstallationDate() async throws -> String { + let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + let creationDate = try FileManager.default.attributesOfItem(atPath: documentsURL.path)[FileAttributeKey.creationDate] as! Date + let creationTimeInMilliseconds = Int(creationDate.timeIntervalSince1970 * 1000) + return String(creationTimeInMilliseconds) + } + func requestInAppRating() async throws { + let windowScene = await UIApplication.shared.connectedScenes.first as! UIWindowScene + await SKStoreReviewController.requestReview(in: windowScene) + } } diff --git a/app-ios/tutanota/Sources/IosMobileSystemFacade.swift b/app-ios/tutanota/Sources/IosMobileSystemFacade.swift index 10066fd963f..72ff836bf1a 100644 --- a/app-ios/tutanota/Sources/IosMobileSystemFacade.swift +++ b/app-ios/tutanota/Sources/IosMobileSystemFacade.swift @@ -1,5 +1,6 @@ import Contacts import Foundation +import StoreKit import TutanotaSharedFramework private let APP_LOCK_METHOD = "AppLockMethod" @@ -78,4 +79,17 @@ class IosMobileSystemFacade: MobileSystemFacade { } func openMailApp(_ query: String) async throws { TUTSLog("Tried to open Mail App from Mail App") } + func getInstallationDate() async throws -> String { + let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + let creationDate = try FileManager.default.attributesOfItem(atPath: documentsURL.path)[FileAttributeKey.creationDate] as! Date + let creationTimeInMilliseconds = Int(creationDate.timeIntervalSince1970 * 1000) + return String(creationTimeInMilliseconds) + } + func requestInAppRating() async throws { + // TODO: Replace `SKStoreReviewController.requestReview()` with StoreKit's/SwiftUI's `requestReview()` + // as `SKStoreReviewController.requestReview()` will be removed in iOS 19 (release roughly September 2025) + // This will require migrating from UIKit to Swift UI + let windowScene = await UIApplication.shared.connectedScenes.first as! UIWindowScene + await SKStoreReviewController.requestReview(in: windowScene) + } } diff --git a/buildSrc/RollupConfig.js b/buildSrc/RollupConfig.js index 81a92d36617..de7cdbab841 100644 --- a/buildSrc/RollupConfig.js +++ b/buildSrc/RollupConfig.js @@ -35,7 +35,7 @@ export const allowedImports = { date: ["polyfill-helpers", "common-min", "common"], "date-gui": ["polyfill-helpers", "common-min", "common", "boot", "gui-base", "main", "sharing", "date", "contacts"], "mail-view": ["polyfill-helpers", "common-min", "common", "boot", "gui-base", "main"], - "mail-editor": ["polyfill-helpers", "common-min", "common", "boot", "gui-base", "main", "mail-view", "sanitizer", "sharing"], + "mail-editor": ["polyfill-helpers", "common-min", "common", "boot", "gui-base", "main", "mail-view", "sanitizer", "sharing", "date-gui"], search: ["polyfill-helpers", "common-min", "common", "boot", "gui-base", "main", "mail-view", "calendar-view", "contacts", "date", "date-gui", "sharing"], // ContactMergeView needs HtmlEditor even though ContactEditor doesn't? contacts: ["polyfill-helpers", "common-min", "common", "boot", "gui-base", "main", "mail-view", "date", "date-gui", "mail-editor"], @@ -174,6 +174,7 @@ export function getChunkName(moduleId, { getModuleInfo }) { isIn("src/calendar-app/calendar/export") || isIn("src/common/misc/DateParser") || isIn("src/common/misc/CyberMondayUtils") || + isIn("src/common/ratings") || isIn("src/calendar-app/calendar/model") || isIn("src/calendar-app/calendar/gui") || isIn("src/common/calendar/import") diff --git a/ipc-schema/facades/MobileSystemFacade.json b/ipc-schema/facades/MobileSystemFacade.json index 301c5ff3a6d..d9a37d17d83 100644 --- a/ipc-schema/facades/MobileSystemFacade.json +++ b/ipc-schema/facades/MobileSystemFacade.json @@ -80,6 +80,16 @@ } ], "ret": "void" + }, + "getInstallationDate": { + "doc": "Returns the date and time the app was installed as a string with milliseconds in UNIX epoch.", + "arg": [], + "ret": "string" + }, + "requestInAppRating": { + "doc": "Requests the system in-app rating dialog to be displayed", + "arg": [], + "ret": "void" } } } diff --git a/resources/images/rating/calendar.png b/resources/images/rating/calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1780910675d50e1d9215795344b3afcb096780 GIT binary patch literal 31871 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 clientOnlyCalendars: Map + + /** + * A list of dates on which a user has sent an e-mail or created a calendar event. Each date is represented as the date's timestamp. + */ + events: Array + + /** + * The last date on which the user was prompted to rate the app as a timestamp. + */ + lastRatingPromptedDate?: number + + /** + * The date of the earliest possible next date from which another rating can be requested from the user. + * This is only for the case the user does not want to rate right now or completely opts out of the in-app ratings. + */ + retryRatingPromptAfter?: number } /** @@ -125,6 +141,9 @@ export class DeviceConfig implements UsageTestStorage, NewsItemStorage { isCredentialsMigratedToNative: loadedConfig.isCredentialsMigratedToNative ?? false, lastExternalCalendarSync: loadedConfig.lastExternalCalendarSync ?? {}, clientOnlyCalendars: loadedConfig.clientOnlyCalendars ? new Map(typedEntries(loadedConfig.clientOnlyCalendars)) : new Map(), + events: loadedConfig.events ?? [], + lastRatingPromptedDate: loadedConfig.lastRatingPromptedDate ?? null, + retryRatingPromptAfter: loadedConfig.retryRatingPromptAfter ?? null, } this.lastSyncStream(new Map(Object.entries(this.config.lastExternalCalendarSync))) @@ -414,6 +433,69 @@ export class DeviceConfig implements UsageTestStorage, NewsItemStorage { this.config.clientOnlyCalendars.set(calendarId, clientOnlyCalendarConfig) this.writeToStorage() } + + public writeEvents(events: Date[]): void { + this.config.events = events.map((date) => date.getTime()) + this.writeToStorage() + } + + /** + * Gets a list of dates on which a certain event has occurred. Could be email sent, replied, contact created etc. + * + * Only present on iOS. + */ + public getEvents(): Date[] { + return (this.config.events ?? []).flatMap((timestamp) => { + try { + return new Date(timestamp) + } catch (e) { + return [] + } + }) + } + + public setLastRatingPromptedDate(date: Date): void { + this.config.lastRatingPromptedDate = date.getTime() + this.writeToStorage() + } + + /** + * Gets the last date on which the user was prompted to rate the app. + */ + public getLastRatingPromptedDate(): Date | null { + if (this.config.lastRatingPromptedDate == null) { + return null + } + + try { + return new Date(this.config.lastRatingPromptedDate) + } catch (e) { + return null + } + } + + /** + * Sets the date of the earliest possible next date from which another rating can be requested from the user. + */ + public setRetryRatingPromptAfter(date: Date): void { + this.config.retryRatingPromptAfter = date.getTime() + this.writeToStorage() + } + + /** + * Gets the date of the earliest possible next date from which another rating can be requested from the user. + */ + public getRetryRatingPromptAfter(): Date | null { + if (this.config.retryRatingPromptAfter == null) { + return null + } + + try { + return new Date(this.config.retryRatingPromptAfter) + } catch (e) { + return null + } + } } export function migrateConfig(loadedConfig: any) { diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index 78c2590b271..2d87393a889 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1816,3 +1816,8 @@ export type TranslationKeyType = | "yourMessage_label" | "you_label" | "emptyString_msg" + | "ratingHowAreWeDoing_title" + | "ratingExplanation_msg" + | "ratingLoveIt_label" + | "ratingNeedsWork_label" + | "notNow_label" diff --git a/src/common/native/common/generatedipc/MobileSystemFacade.ts b/src/common/native/common/generatedipc/MobileSystemFacade.ts index 1a0bde709b6..01e64294e52 100644 --- a/src/common/native/common/generatedipc/MobileSystemFacade.ts +++ b/src/common/native/common/generatedipc/MobileSystemFacade.ts @@ -40,4 +40,14 @@ export interface MobileSystemFacade { getSupportedAppLockMethods(): Promise> openMailApp(query: string): Promise + + /** + * Returns the date and time the app was installed as a string with milliseconds in UNIX epoch. + */ + getInstallationDate(): Promise + + /** + * Requests the system in-app rating dialog to be displayed + */ + requestInAppRating(): Promise } diff --git a/src/common/native/common/generatedipc/MobileSystemFacadeSendDispatcher.ts b/src/common/native/common/generatedipc/MobileSystemFacadeSendDispatcher.ts index 3738375db8d..1a968a00694 100644 --- a/src/common/native/common/generatedipc/MobileSystemFacadeSendDispatcher.ts +++ b/src/common/native/common/generatedipc/MobileSystemFacadeSendDispatcher.ts @@ -37,4 +37,10 @@ export class MobileSystemFacadeSendDispatcher implements MobileSystemFacade { async openMailApp(...args: Parameters) { return this.transport.invokeNative("ipc", ["MobileSystemFacade", "openMailApp", ...args]) } + async getInstallationDate(...args: Parameters) { + return this.transport.invokeNative("ipc", ["MobileSystemFacade", "getInstallationDate", ...args]) + } + async requestInAppRating(...args: Parameters) { + return this.transport.invokeNative("ipc", ["MobileSystemFacade", "requestInAppRating", ...args]) + } } diff --git a/src/common/native/main/wizard/SetupWizard.ts b/src/common/native/main/wizard/SetupWizard.ts index 528f1e1f1ab..e5011759132 100644 --- a/src/common/native/main/wizard/SetupWizard.ts +++ b/src/common/native/main/wizard/SetupWizard.ts @@ -2,13 +2,8 @@ import { createWizardDialog, WizardPageWrapper, wizardPageWrapper } from "../../ import { defer } from "@tutao/tutanota-utils" import { SetupCongratulationsPage, SetupCongratulationsPageAttrs } from "./setupwizardpages/SetupCongraulationsPage.js" import { DeviceConfig } from "../../../misc/DeviceConfig.js" -import m from "mithril" import { SetupNotificationsPage, SetupNotificationsPageAttrs } from "./setupwizardpages/SetupNotificationsPage.js" -import { BannerButton } from "../../../gui/base/buttons/BannerButton.js" -import { theme } from "../../../gui/theme.js" -import { ClickHandler } from "../../../gui/base/GuiUtils.js" import { DialogType } from "../../../gui/base/Dialog.js" -import { TranslationKey } from "../../../misc/LanguageViewModel.js" import { SetupThemePage, SetupThemePageAttrs } from "./setupwizardpages/SetupThemePage.js" import { SetupContactsPage, SetupContactsPageAttrs } from "./setupwizardpages/SetupContactsPage.js" import { SetupLockPage, SetupLockPageAttrs } from "./setupwizardpages/SetupLockPage.js" diff --git a/src/common/ratings/InAppRatingDialog.ts b/src/common/ratings/InAppRatingDialog.ts new file mode 100644 index 00000000000..f3d370aeb8d --- /dev/null +++ b/src/common/ratings/InAppRatingDialog.ts @@ -0,0 +1,166 @@ +import { Dialog, DialogType } from "../gui/base/Dialog.js" +import m from "mithril" +import { deviceConfig } from "../misc/DeviceConfig.js" +import { createEvent, getRatingAllowed, isEventHappyMoment, RatingCheckResult } from "./InAppRatingUtils.js" +import { isIOSApp } from "../api/common/Env.js" +import { locator } from "../api/main/CommonLocator.js" +import { Button, ButtonType } from "../gui/base/Button.js" +import { DefaultAnimationTime } from "../gui/animation/Animations.js" +import { neverNull, resolveMaybeLazy } from "@tutao/tutanota-utils" +import { Keys } from "../api/common/TutanotaConstants.js" +import { DialogHeaderBarAttrs } from "../gui/base/DialogHeaderBar.js" +import { BaseButton, BaseButtonAttrs } from "../gui/base/buttons/BaseButton.js" +import { theme } from "../gui/theme.js" +import { px, size } from "../gui/size.js" +import { client } from "../misc/ClientDetector.js" +import { lang } from "../misc/LanguageViewModel.js" +import { DateTime } from "luxon" + +const enum AppRatingValue { + Happy = "happy", + Unhappy = "unhappy", + NotNow = "notNow", +} + +/** + * Displays the application rating dialog to the user and handles their selection. + * + * The dialog provides options for users to rate their experience as positive or negative, + * or to dismiss the dialog with actions such as "Not now"." Based on + * the user's choice, appropriate follow-up actions are taken: + * + * - **Happy**: Prompts the user to leave a review on the App Store. Will not ask for a review again within one year. + * - **Unhappy**: No immediate action; the user is not redirected. Will not ask for a review again within one year. + * - **Not now**: Prevents the dialog from being shown for one month. + * + * @returns {Promise} Resolves after the user makes a selection and associated actions are completed. + */ +export async function showAppRatingDialog(): Promise { + const selectedValue: AppRatingValue = await new Promise((resolve) => { + const choose = (choice: AppRatingValue) => { + dialog.close() + setTimeout(() => resolve(choice), DefaultAnimationTime) + } + + const headerBarProps: DialogHeaderBarAttrs = { + right: [ + { + label: "notNow_label", + click: () => choose(AppRatingValue.NotNow), + type: ButtonType.Secondary, + }, + ], + } + + const columnClass = ".flex-half.overflow-hidden" + + const dialog = new Dialog(DialogType.EditSmall, { + view: () => + m("", [ + m(".dialog-header.plr-l.flex-space-between.dialog-header-line-height", [ + m(columnClass + ".ml-negative-s"), + m( + columnClass + ".mr-negative-s.flex.justify-end", + resolveMaybeLazy(neverNull(headerBarProps.right)).map((a) => m(Button, a)), + ), + ]), + m( + ".plr-l.pb-ml.text-break", + m(".flex.flex-column", [ + m( + "#dialog-message.dialog-max-height.text-break.text-prewrap.selectable.scroll", + m( + "", + m( + ".flex-center.mt-m", + m("img.pb.pt.block.height-100p", { + src: `${window.tutao.appState.prefixWithoutFile}/images/rating/${client.isCalendarApp() ? "calendar" : "mail"}.png`, + alt: "", + rel: "noreferrer", + loading: "lazy", + decoding: "async", + style: { + width: "80%", + }, + }), + ), + m("h1.text-center", lang.get("ratingHowAreWeDoing_title")), + m("p.text-center", lang.get("ratingExplanation_msg")), + ), + ), + m( + ".flex.flex-column.mt", + { style: { gap: "1em" } }, + m(BaseButton, { + label: lang.get("ratingLoveIt_label"), + text: lang.get("ratingLoveIt_label"), + onclick: () => choose(AppRatingValue.Happy), + class: `full-width border-radius-small center b flash accent-bg button-content`, + style: { + height: px(size.button_height + size.vpad_xs * 1.5), + }, + } satisfies BaseButtonAttrs), + + m(BaseButton, { + label: lang.get("ratingNeedsWork_label"), + text: lang.get("ratingNeedsWork_label"), + onclick: () => choose(AppRatingValue.Unhappy), + class: `full-width border-radius-small center b flash`, + style: { + border: `2px solid ${theme.content_accent}`, + height: px(size.button_height + size.vpad_xs * 1.5), + color: theme.content_accent, + }, + } satisfies BaseButtonAttrs), + ), + ]), + ), + ]), + }).addShortcut({ + help: "close_alt", + key: Keys.ESC, + exec: () => choose(AppRatingValue.NotNow), + }) + + dialog.show() + }) + + handleRatingDialogSelection(selectedValue) +} + +function handleRatingDialogSelection(selectedValue: AppRatingValue) { + switch (selectedValue) { + case AppRatingValue.Unhappy: + deviceConfig.setLastRatingPromptedDate(new Date()) + break + case AppRatingValue.NotNow: + // Ask again in minimum one month from now. + deviceConfig.setRetryRatingPromptAfter(DateTime.now().plus({ months: 1 }).toJSDate()) + break + case AppRatingValue.Happy: { + deviceConfig.setLastRatingPromptedDate(new Date()) + + void locator.systemFacade.requestInAppRating() + break + } + } +} + +/** + * If the client is on iOS, we save the current date as an event to determine if we want to trigger a "rate Tuta" dialog. + */ +export async function handleRatingByEvent() { + const isTheIOSApp = isIOSApp() + + if (isTheIOSApp) { + createEvent(deviceConfig) + } + + const now = new Date() + + if ((await getRatingAllowed(now, deviceConfig, isTheIOSApp)) === RatingCheckResult.RATING_ALLOWED) { + if (isEventHappyMoment(now, deviceConfig)) { + void showAppRatingDialog() + } + } +} diff --git a/src/common/ratings/InAppRatingUtils.ts b/src/common/ratings/InAppRatingUtils.ts new file mode 100644 index 00000000000..7ba2853be05 --- /dev/null +++ b/src/common/ratings/InAppRatingUtils.ts @@ -0,0 +1,91 @@ +import { DeviceConfig } from "../misc/DeviceConfig.js" +import { DateTime } from "../../../libs/luxon.js" +import { locator } from "../api/main/CommonLocator.js" + +export function createEvent(deviceConfig: DeviceConfig): void { + const retentionPeriod: number = 30 + let events = deviceConfig.getEvents().filter((event) => isWithinLastNDays(new Date(), event, retentionPeriod)) + events.push(new Date()) + deviceConfig.writeEvents(events) +} + +export function isWithinLastNDays(now: Date, date: Date, days: number) { + return DateTime.fromJSDate(now).diff(DateTime.fromJSDate(date), "days").days < days +} + +export enum RatingCheckResult { + RATING_ALLOWED, + UNSUPPORTED_PLATFORM, + LAST_RATING_TOO_YOUNG, + APP_INSTALLATION_TOO_YOUNG, + ACCOUNT_TOO_YOUNG, + RATING_DISMISSED, +} + +/** + * Determines if we are allowed to ask the user for their rating. + * It is possible that the user delayed his choice or if we already asked them within the past year. + * + * 1. The app must be running on an iOS device. + * 2. The app installation date and customer creation date must both be at least 7 days in the past. + * 3. The dialog must not have been shown within the last year (When the dialog is dismissed with the cancel button it is not considered being shown). + * 4. The retry prompt timer (if set) must have expired. + */ +export async function getRatingAllowed(now: Date, deviceConfig: DeviceConfig, isIOSApp: boolean): Promise { + if (!isIOSApp) { + return RatingCheckResult.UNSUPPORTED_PLATFORM + } + + const lastRatingPromptedDate: Date | null = deviceConfig.getLastRatingPromptedDate() + + if (lastRatingPromptedDate != null && DateTime.fromJSDate(now).diff(DateTime.fromJSDate(lastRatingPromptedDate), "years").years < 1) { + return RatingCheckResult.LAST_RATING_TOO_YOUNG + } + + const appInstallationDate = await locator.systemFacade.getInstallationDate().then((rawDate) => new Date(Number(rawDate))) + if (isWithinLastNDays(now, appInstallationDate, 7)) { + return RatingCheckResult.APP_INSTALLATION_TOO_YOUNG + } + + const customerCreationDate = (await locator.logins.getUserController().loadCustomerInfo()).creationTime + if (isWithinLastNDays(now, customerCreationDate, 7)) { + return RatingCheckResult.ACCOUNT_TOO_YOUNG + } + + const retryRatingPromptAfter = deviceConfig.getRetryRatingPromptAfter() + if (retryRatingPromptAfter != null && now.getTime() < retryRatingPromptAfter.getTime()) { + return RatingCheckResult.RATING_DISMISSED + } + + return RatingCheckResult.RATING_ALLOWED +} + +/** + * Determines if the user is experiencing a "happy moment". + * + * At least one of the following activity-based conditions must be satisfied: + * - The user has created at least 3 events/emails, and no previous prompt was shown. + * - The user has performed at least 10 activities (events/emails) in the last 28 days. + * + * @returns {boolean} A promise that resolves to `true` if the user is in a "happy moment". + */ +export function isEventHappyMoment(now: Date, deviceConfig: DeviceConfig): boolean { + //region Trigger 1: Check for minimum 3 events/emails created + const lastRatingPromptedDate: Date | null = deviceConfig.getLastRatingPromptedDate() + + const events: Date[] = deviceConfig.getEvents() + if (events.length >= 3 && lastRatingPromptedDate == null) { + return true + } + //endregion + + //region Trigger 2: Check for at least 10 activities in the last 28 days + const twentyEightDaysAgo = DateTime.fromJSDate(now).minus({ days: 28 }).toMillis() + const recentActivityCount = events.filter((event) => new Date(event).getTime() >= twentyEightDaysAgo).length + + if (recentActivityCount >= 10) { + return true + } + //endregion + return false +} diff --git a/src/common/subscription/UpgradeConfirmSubscriptionPage.ts b/src/common/subscription/UpgradeConfirmSubscriptionPage.ts index 5b5d2915265..b05fd860a30 100644 --- a/src/common/subscription/UpgradeConfirmSubscriptionPage.ts +++ b/src/common/subscription/UpgradeConfirmSubscriptionPage.ts @@ -21,6 +21,10 @@ import { MobilePaymentResultType } from "../native/common/generatedipc/MobilePay import { updatePaymentData } from "./InvoiceAndPaymentDataPage" import { SessionType } from "../api/common/SessionType" import { MobilePaymentError } from "../api/common/error/MobilePaymentError.js" +import { getRatingAllowed, RatingCheckResult } from "../ratings/InAppRatingUtils.js" +import { showAppRatingDialog } from "../ratings/InAppRatingDialog.js" +import { deviceConfig } from "../misc/DeviceConfig.js" +import { isIOSApp } from "../api/common/Env.js" export class UpgradeConfirmSubscriptionPage implements WizardPageN { private dom!: HTMLElement @@ -77,6 +81,14 @@ export class UpgradeConfirmSubscriptionPage implements WizardPageN { + const ratingCheckResult = await getRatingAllowed(new Date(), deviceConfig, isIOSApp()) + if (ratingCheckResult === RatingCheckResult.RATING_ALLOWED) { + setTimeout(async () => { + void showAppRatingDialog() + }, 2000) + } + }) .catch( ofClass(PreconditionFailedError, (e) => { Dialog.message( diff --git a/src/mail-app/mail/editor/MailEditor.ts b/src/mail-app/mail/editor/MailEditor.ts index 3a786de3fb4..b16e4689704 100644 --- a/src/mail-app/mail/editor/MailEditor.ts +++ b/src/mail-app/mail/editor/MailEditor.ts @@ -96,6 +96,8 @@ import { } from "../../../common/mailFunctionality/SharedMailUtils.js" import { mailLocator } from "../../mailLocator.js" +import { handleRatingByEvent } from "../../../common/ratings/InAppRatingDialog.js" + export type MailEditorAttrs = { model: SendMailModel doBlockExternalContent: Stream @@ -832,6 +834,8 @@ async function createMailEditorDialog(model: SendMailModel, blockExternalContent if (success) { dispose() dialog.close() + + await handleRatingByEvent() } } catch (e) { if (e instanceof UserError) { diff --git a/src/mail-app/settings/SettingsView.ts b/src/mail-app/settings/SettingsView.ts index 63484e66969..15f7aee52a2 100644 --- a/src/mail-app/settings/SettingsView.ts +++ b/src/mail-app/settings/SettingsView.ts @@ -67,6 +67,7 @@ import { NotificationSettingsViewer } from "./NotificationSettingsViewer.js" import { SettingsViewAttrs, UpdatableSettingsDetailsViewer, UpdatableSettingsViewer } from "../../common/settings/Interfaces.js" import { AffiliateSettingsViewer } from "../../common/settings/AffiliateSettingsViewer.js" import { AffiliateKpisViewer } from "../../common/settings/AffiliateKpisViewer.js" +import { showAppRatingDialog } from "../../common/ratings/InAppRatingDialog.js" assertMainOrNode() diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index 24e2d093b05..05ab49e09ef 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1831,6 +1831,12 @@ export default { "yourCalendars_label": "Your calendars", "yourFolders_action": "YOUR FOLDERS", "yourMessage_label": "Your message", - "you_label": "You" + "you_label": "You", + "ratingHowAreWeDoing_title": "How are we doing?", + "ratingExplanation_msg": "Whether you love us or feel we could be doing better, let us know!", + "ratingLoveIt_label": "Love it!", + "dontAskAgain_label": "Don't ask again", + "ratingNeedsWork_label": "Needs work", + "notNow_label": "Not now" } } diff --git a/test/tests/Suite.ts b/test/tests/Suite.ts index 90f4838283c..d3a7a1cf84c 100644 --- a/test/tests/Suite.ts +++ b/test/tests/Suite.ts @@ -132,6 +132,7 @@ import "./mail/view/ConversationViewModelTest.js" import "./mail/view/MailViewerViewModelTest.js" import "./api/worker/facades/KeyCacheTest.js" import "./mail/view/MailViewModelTest.js" +import "./misc/InAppRatingUtilsTest.js" import * as td from "testdouble" import { random } from "@tutao/tutanota-crypto" diff --git a/test/tests/misc/DeviceConfigTest.ts b/test/tests/misc/DeviceConfigTest.ts index 8d1b00d0177..8b2612c84fc 100644 --- a/test/tests/misc/DeviceConfigTest.ts +++ b/test/tests/misc/DeviceConfigTest.ts @@ -106,6 +106,9 @@ o.spec("DeviceConfig", function () { isSetupComplete: true, lastExternalCalendarSync: {}, clientOnlyCalendars: new Map(), + events: [], + lastRatingPromptedDate: null, + retryRatingPromptAfter: null, } when(localStorageMock.getItem(DeviceConfig.LocalStorageKey)).thenReturn(JSON.stringify(storedInLocalStorage)) diff --git a/test/tests/misc/InAppRatingUtilsTest.ts b/test/tests/misc/InAppRatingUtilsTest.ts new file mode 100644 index 00000000000..7f7d884291f --- /dev/null +++ b/test/tests/misc/InAppRatingUtilsTest.ts @@ -0,0 +1,215 @@ +import o from "@tutao/otest" +import { getRatingAllowed, isEventHappyMoment, RatingCheckResult } from "../../../src/common/ratings/InAppRatingUtils.js" +import { DeviceConfig } from "../../../src/common/misc/DeviceConfig.js" +import { object, verify, when } from "testdouble" +import { CommonLocator, initCommonLocator } from "../../../src/common/api/main/CommonLocator.js" +import { UserController } from "../../../src/common/api/main/UserController.js" + +o.spec("InAppRatingUtilsTest", () => { + let deviceConfigMock: DeviceConfig = object() + let locatorMock: CommonLocator = object() + + o.beforeEach(() => { + deviceConfigMock = object() + locatorMock = object() + initCommonLocator(locatorMock) + }) + + const now = new Date("2024-10-27T12:34:00Z") + + const userControllerMock: UserController = object({ + // @ts-ignore + async loadCustomerInfo() {}, + }) + + o.spec("getRatingAllowed", () => { + o("Should not trigger if the app is not on iOS", async () => { + // Arrange + const appInstallationDate = new Date("2024-10-11T11:12:04Z") + + when(deviceConfigMock.getRetryRatingPromptAfter()).thenReturn(null) + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(locatorMock.systemFacade.getInstallationDate()).thenResolve(String(appInstallationDate.getTime())) + + // Act + const res = await getRatingAllowed(now, deviceConfigMock, false) + + // Assert + o(res).equals(RatingCheckResult.UNSUPPORTED_PLATFORM) + }) + + o("Should not trigger if the rating dialog was shown less than a year ago", async () => { + // Arrange + const appInstallationDate = new Date("2024-10-11T11:12:04Z") + + when(deviceConfigMock.getRetryRatingPromptAfter()).thenReturn(null) + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(new Date("2024-06-06T06:06:06Z")) + when(locatorMock.systemFacade.getInstallationDate()).thenResolve(String(appInstallationDate.getTime())) + + // Act + const res = await getRatingAllowed(now, deviceConfigMock, true) + + // Assert + o(res).equals(RatingCheckResult.LAST_RATING_TOO_YOUNG) + }) + + o("Should not trigger if the app was installed less than 7 days ago", async () => { + // Arrange + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(locatorMock.systemFacade.getInstallationDate()).thenResolve(String(new Date("2024-10-23T11:12:04Z").getTime())) + + // Act + const res = await getRatingAllowed(now, deviceConfigMock, true) + + // Assert + o(res).equals(RatingCheckResult.APP_INSTALLATION_TOO_YOUNG) + }) + + o("Should not trigger if the customer account was created less than 7 days ago", async () => { + // Arrange + const appInstallationDate = new Date("2024-10-11T11:12:04Z") // The app is installed long enough ago. + const customerCreationDate = new Date("2024-10-23T11:12:04Z") + + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(locatorMock.systemFacade.getInstallationDate()).thenResolve(String(appInstallationDate.getTime())) + when(locatorMock.logins.getUserController()).thenReturn(userControllerMock) + when(locatorMock.logins.getUserController().loadCustomerInfo()).thenResolve({ creationTime: customerCreationDate }) + + // Act + const res = await getRatingAllowed(now, deviceConfigMock, true) + + // Assert + o(res).equals(RatingCheckResult.ACCOUNT_TOO_YOUNG) + }) + + o("Should not trigger if the retry prompt timer has not elapsed", async () => { + // Arrange + const appInstallationDate = new Date("2024-10-11T11:12:04Z") + const customerCreationDate = new Date("2024-10-11T11:12:04Z") + + when(deviceConfigMock.getRetryRatingPromptAfter()).thenReturn(new Date("2024-11-27T14:34:00Z")) + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(locatorMock.systemFacade.getInstallationDate()).thenResolve(String(appInstallationDate.getTime())) + when(locatorMock.logins.getUserController()).thenReturn(userControllerMock) + when(locatorMock.logins.getUserController().loadCustomerInfo()).thenResolve({ creationTime: customerCreationDate }) + + // Act + const res = await getRatingAllowed(now, deviceConfigMock, true) + + // Assert + verify(locatorMock.logins.getUserController().loadCustomerInfo(), { times: 1 }) + o(res).equals(RatingCheckResult.RATING_DISMISSED) + }) + }) + + o.spec("isEventHappyMoment", () => { + o("Should trigger when three activities reached and retry timer has elapsed", () => { + // Arrange + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(deviceConfigMock.getEvents()).thenReturn([ + new Date("2024-10-11T11:12:04Z"), + new Date("2024-10-10T11:12:04Z"), + new Date("2024-10-09T11:12:04Z"), + ]) + + // Act + const res = isEventHappyMoment(now, deviceConfigMock) + + // Assert + o(res).equals(true) + }) + + o("Should trigger if there are at least 3 events/emails created and no previous prompt", () => { + // Arrange + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(deviceConfigMock.getEvents()).thenReturn([ + new Date("2024-10-11T11:12:04Z"), + new Date("2024-10-10T11:12:04Z"), + new Date("2024-10-09T11:12:04Z"), + ]) + + // Act + const res = isEventHappyMoment(now, deviceConfigMock) + + // Assert + verify(deviceConfigMock.getEvents(), { times: 1 }) + o(res).equals(true) + }) + + o("Should not trigger if there are at least 3 events/emails created and no previous prompt", () => { + // Arrange + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(new Date("2024-02-11T11:12:04Z")) + when(deviceConfigMock.getEvents()).thenReturn([ + new Date("2024-10-11T11:12:04Z"), + new Date("2024-10-10T11:12:04Z"), + new Date("2024-10-09T11:12:04Z"), + ]) + + // Act + const res = isEventHappyMoment(now, deviceConfigMock) + + // Assert + verify(deviceConfigMock.getEvents(), { times: 1 }) + o(res).equals(false) + }) + + o("Should trigger if there are at least 10 recent activities in the last 28 days", () => { + // Arrange + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(new Date("2022-10-11T11:12:04Z")) + when(deviceConfigMock.getEvents()).thenReturn([ + new Date("2024-10-11T11:12:04Z"), + new Date("2024-10-10T11:12:04Z"), + new Date("2024-10-09T11:12:04Z"), + new Date("2024-10-08T11:12:04Z"), + new Date("2024-10-07T11:12:04Z"), + new Date("2024-10-06T11:12:04Z"), + new Date("2024-10-05T11:12:04Z"), + new Date("2024-10-04T11:12:04Z"), + new Date("2024-10-03T11:12:04Z"), + new Date("2024-10-02T11:12:04Z"), + ]) + + // Act + const res = isEventHappyMoment(now, deviceConfigMock) + + // Assert + o(res).equals(true) + }) + + o("Should not trigger if there are less than 10 recent activities in the last 28 days", () => { + // Arrange + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(new Date("2022-10-11T11:12:04Z")) + when(deviceConfigMock.getEvents()).thenReturn([ + new Date("2024-10-11T11:12:04Z"), + new Date("2024-10-10T11:12:04Z"), + new Date("2024-10-09T11:12:04Z"), + new Date("2024-10-08T11:12:04Z"), + new Date("2024-10-07T11:12:04Z"), + new Date("2024-10-06T11:12:04Z"), + new Date("2024-10-05T11:12:04Z"), + new Date("2024-10-04T11:12:04Z"), + new Date("2024-10-03T11:12:04Z"), + new Date("2024-09-28T11:12:04Z"), + ]) + + // Act + const res = isEventHappyMoment(now, deviceConfigMock) + + // Assert + o(res).equals(false) + }) + + o("Should not trigger if less than 3 events/emails created", () => { + // Arrange + const events = [new Date("2024-10-11T11:12:04Z"), new Date("2024-10-10T11:12:04Z")] + when(deviceConfigMock.getLastRatingPromptedDate()).thenReturn(null) + when(deviceConfigMock.getEvents()).thenReturn(events) + + // Act + const res = isEventHappyMoment(now, deviceConfigMock) + + // Assert + o(res).equals(false) + }) + }) +}) From d64befc4de072ec9e29ba836a272fb9e185a4705 Mon Sep 17 00:00:00 2001 From: mac-github Date: Tue, 3 Dec 2024 10:54:59 +0100 Subject: [PATCH 4/6] Fix Swift formatting Co-authored-by: arm Co-authored-by: Jamie Turner <48037381+rezbyte@users.noreply.github.com> Co-authored-by: jug --- app-ios/calendar/Sources/IosMobileSystemFacade.swift | 2 +- app-ios/tutanota/Sources/IosMobileSystemFacade.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-ios/calendar/Sources/IosMobileSystemFacade.swift b/app-ios/calendar/Sources/IosMobileSystemFacade.swift index 90a90bd4807..9fd8cbbfe54 100644 --- a/app-ios/calendar/Sources/IosMobileSystemFacade.swift +++ b/app-ios/calendar/Sources/IosMobileSystemFacade.swift @@ -1,6 +1,6 @@ import Contacts -import StoreKit import Foundation +import StoreKit import TutanotaSharedFramework private let APP_LOCK_METHOD = "AppLockMethod" diff --git a/app-ios/tutanota/Sources/IosMobileSystemFacade.swift b/app-ios/tutanota/Sources/IosMobileSystemFacade.swift index 72ff836bf1a..ea73ae39073 100644 --- a/app-ios/tutanota/Sources/IosMobileSystemFacade.swift +++ b/app-ios/tutanota/Sources/IosMobileSystemFacade.swift @@ -87,8 +87,8 @@ class IosMobileSystemFacade: MobileSystemFacade { } func requestInAppRating() async throws { // TODO: Replace `SKStoreReviewController.requestReview()` with StoreKit's/SwiftUI's `requestReview()` - // as `SKStoreReviewController.requestReview()` will be removed in iOS 19 (release roughly September 2025) - // This will require migrating from UIKit to Swift UI + // as `SKStoreReviewController.requestReview()` will be removed in iOS 19 (release roughly September 2025) + // This will require migrating from UIKit to Swift UI let windowScene = await UIApplication.shared.connectedScenes.first as! UIWindowScene await SKStoreReviewController.requestReview(in: windowScene) } From 5ae627fc5074843bed5e532db7a9f9e7942cc41c Mon Sep 17 00:00:00 2001 From: jug Date: Tue, 3 Dec 2024 11:01:15 +0100 Subject: [PATCH 5/6] Remove duplicate lang keys --- src/mail-app/translations/en.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index 05ab49e09ef..24e2d093b05 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1831,12 +1831,6 @@ export default { "yourCalendars_label": "Your calendars", "yourFolders_action": "YOUR FOLDERS", "yourMessage_label": "Your message", - "you_label": "You", - "ratingHowAreWeDoing_title": "How are we doing?", - "ratingExplanation_msg": "Whether you love us or feel we could be doing better, let us know!", - "ratingLoveIt_label": "Love it!", - "dontAskAgain_label": "Don't ask again", - "ratingNeedsWork_label": "Needs work", - "notNow_label": "Not now" + "you_label": "You" } } From c4d1b2729e435eb0967bd789b38e3c88b7b9c287 Mon Sep 17 00:00:00 2001 From: jat Date: Tue, 3 Dec 2024 11:26:20 +0100 Subject: [PATCH 6/6] [android] Fix missing function in `AndroidMobileSystemFacade` Co-authored-by: jug Co-authored-by: arm <7211155+armhub@users.noreply.github.com> --- .../main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt | 4 ++++ .../main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt b/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt index 1b41f80ae33..b8c19bf1496 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/AndroidMobileSystemFacade.kt @@ -174,4 +174,8 @@ class AndroidMobileSystemFacade( override suspend fun getInstallationDate(): String { return SystemUtils.getInstallationDate(activity.packageManager, activity.packageName) } + + override suspend fun requestInAppRating() { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt b/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt index 404846ad01a..705a6c4cecc 100644 --- a/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt +++ b/app-android/calendar/src/main/java/de/tutao/calendar/AndroidMobileSystemFacade.kt @@ -200,4 +200,8 @@ class AndroidMobileSystemFacade( override suspend fun getInstallationDate(): String { return SystemUtils.getInstallationDate(activity.packageManager, activity.packageName) } + + override suspend fun requestInAppRating() { + TODO("Not yet implemented") + } } \ No newline at end of file