diff --git a/CHANGES.md b/CHANGES.md index d410a5033d..40228f7db8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,43 @@ +Changes in Element 1.1.4 (2021-04-09) +=================================================== + +Improvements 🙌: + - Split network request `/keys/query` into smaller requests (250 users max) (#2925) + - Crypto improvement | Bulk send NO_OLM withheld code + - Display the room shield in all room setting screens + - Improve message with Emoji only detection (#3017) + - Picture preview when replying. Also add the image preview in the message detail bottomsheet (#2916) + - Api interceptor to allow app developers peek responses (#2986) + - Update reactions to Unicode 13.1 (#2998) + - Be more robust when parsing some enums + - Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior) + - Add better support for empty room name fallback (#3106) + - Room list improvements (paging) + - Fix quick click action (#3127) + - Get Event after a Push for a faster notification display in some conditions + - Always try to retry Http requests in case of 429 (#1300) + - registration availability endpoint added to matrix-sdk + +Bugfix 🐛: + - Fix bad theme change for the MainActivity + - Handle encrypted reactions (#2509) + - Disable URL preview for some domains (#2995) + - Fix avatar rendering for DMs, after initial sync (#2693) + - Fix mandatory parameter in API (#3065) + - If signout request fails, do not start LoginActivity, but restart the app (#3099) + - Retain keyword order in emoji import script, and update the generated file (#3147) + +SDK API changes ⚠️: + - Several Services have been migrated to coroutines (#2449) + - Removes filtering options on Timeline. + +Build 🧱: + - Properly exclude gms dependencies in fdroid build flavour which were pulled in through the jitsi SDK (#3125) + +Other changes: + - Add version details on the login screen, in debug or developer mode + - Migrate Retrofit interface to coroutine calls + Changes in Element 1.1.3 (2021-03-18) =================================================== diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 7725bf23db..5a8cce92e8 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -69,7 +69,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.recyclerview:recyclerview:1.2.0-beta02" + implementation "androidx.recyclerview:recyclerview:1.2.0-rc01" implementation 'com.google.android.material:material:1.3.0' } \ No newline at end of file diff --git a/build.gradle b/build.gradle index ec7ec8c1de..b8da6c3864 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.31' + ext.kotlin_version = '1.4.32' ext.kotlin_coroutines_version = "1.4.2" repositories { google() @@ -12,11 +12,11 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.google.gms:google-services:4.3.5' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1' - classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2' + classpath 'com.google.android.gms:oss-licenses-plugin:0.10.3' classpath "com.likethesalad.android:string-reference:1.2.1" // NOTE: Do not place your application dependencies here; they belong diff --git a/docs/notifications.md b/docs/notifications.md index 63bf593d0d..a00fef8fae 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -2,7 +2,7 @@ This document aims to describe how Element android displays notifications to the # Table of Contents 1. [Prerequisites Knowledge](#prerequisites-knowledge) - * [How does a matrix client gets a message from a Home Server?](#how-does-a-matrix-client-gets-a-message-from-a-home-server) + * [How does a matrix client get a message from a Home Server?](#how-does-a-matrix-client-get-a-message-from-a-home-server) * [How does a mobile app receives push notification?](#how-does-a-mobile-app-receives-push-notification) * [Push VS Notification](#push-vs-notification) * [Push in the matrix federated world](#push-in-the-matrix-federated-world) @@ -22,7 +22,7 @@ First let's start with some prerequisite knowledge # Prerequisites Knowledge -## How does a matrix client gets a message from a Home Server? +## How does a matrix client get a message from a Home Server? In order to get messages from a home server, a matrix client need to perform a ``sync`` operation. diff --git a/fastlane/metadata/android/ar/changelogs/40101010.txt b/fastlane/metadata/android/ar/changelogs/40101010.txt new file mode 100644 index 0000000000..329fffeb3c --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/40101010.txt @@ -0,0 +1,2 @@ +التغييرات الرئيسة في هذه النسخة: تحسينات على الأداء وإصلاح للعلل! +اطّلع على سجل التغييرات الكامل هنا: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt index 9b382729c8..11992d355d 100644 --- a/fastlane/metadata/android/ar/title.txt +++ b/fastlane/metadata/android/ar/title.txt @@ -1 +1 @@ -Element (سابقاً Riot.im) +‏Element (‏Riot.im سابقًا) diff --git a/fastlane/metadata/android/ca/changelogs/40101010.txt b/fastlane/metadata/android/ca/changelogs/40101010.txt new file mode 100644 index 0000000000..26ce0562d0 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Canvis principals d'aquesta versió: millora de rendiment i correcció d'errors! +Registre de canvis complet: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/cs/changelogs/40101010.txt b/fastlane/metadata/android/cs/changelogs/40101010.txt new file mode 100644 index 0000000000..73c691da06 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: vylepšení výkonnosti a opravy chyb! +Úplný záznam změn: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/de/changelogs/40100110.txt b/fastlane/metadata/android/de/changelogs/40100110.txt index e70007b5d7..24bc6e518c 100644 --- a/fastlane/metadata/android/de/changelogs/40100110.txt +++ b/fastlane/metadata/android/de/changelogs/40100110.txt @@ -1,2 +1,2 @@ -Diese neue Version enthält hauptsächlich Verbesserungen der Benutzer*innenoberfläche und der Handhabung. Du kannst jetzt ganz schnell Freund*innen einladen und DMs erstellen, indem du schlicht einen QR-Code scannst. +Diese neue Version enthält hauptsächlich Verbesserungen der Benutzeroberfläche und der Handhabung. Du kannst jetzt ganz schnell Freund*innen einladen und DMs erstellen, indem du schlicht einen QR-Code scannst. Vollständige Versionshinweise: https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/de/changelogs/40101010.txt b/fastlane/metadata/android/de/changelogs/40101010.txt new file mode 100644 index 0000000000..59758edcc9 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Hauptänderungen in dieser Version: Leistungsverbesserungen und Fehlerbehebungen! +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index 133f5e10d4..568ae61875 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -3,7 +3,7 @@ Element ist eine neuartige Messaging- und Kollaborationsapp: 1. Volle Kontrolle über deine Privatssphäre 2. Kommuniziere mit jedem aus dem Matrix-Netzwerk und mit der Integration von z.B. Slack sogar über Matrix hinaus 3. Schutz vor Werbung, Datamining und geschlossenen Platformen -4. Absicherung durch Ende-zu-Ende-Verschlüsselung, und Cross Signing um andere zu verifizieren +4. Absicherung durch Ende-zu-Ende-Verschlüsselung, und Cross-Signing um andere zu verifizieren Element unterscheidet sich durch Dezentralität und Open Source deutlich von anderen Messaging- und Kollaborationsapps. @@ -11,11 +11,11 @@ Element ermöglicht es einen eigenen Server zu betreiben - oder einen beliebigen Element ist zu all diesem in der Lage, weil es Matrix nutzt - einen Standard für offene, dezentrale Kommunikation. -Element gibt dir die Kontrolle, indem es dir die Wahl darüber lässt, wer deine Konversationen hostet. In der Element App kannst du zwischen verschiedenen Möglichkeiten auswählen: +Element gibt dir die Kontrolle, indem es dir die Wahl darüber lässt, wer deine Konversationen hostet. In der Element-App kannst du zwischen verschiedenen Möglichkeiten auswählen: 1. Kostenlos auf dem öffentlichen matrix.org Server registrieren, der von den Matrix-Entwicklern gehostet wird, oder wähle aus Tausenden von öffentlichen Servern, die von Freiwilligen gehostet werden -2. Einen Account auf einem eigenen Server auf eigener Hardware betreiben -3. Einen Account auf einem benutzerdefinierten Server erstellen, zum Beispiel durch ein Abonnment bei der Element Matrix Services Hosting-Platform +2. Einen Konto auf einem eigenen Server auf eigener Hardware betreiben +3. Einen Konto auf einem benutzerdefinierten Server erstellen, zum Beispiel durch ein Abonnement bei Element Matrix Services (kurz EMS) Wieso Element nutzen? @@ -23,8 +23,8 @@ Element gibt dir die Kontrolle, indem es dir die Wahl darüber lässt, wer deine OFFENE KOMMUNIKATION UND KOLLABORATION: Du kannst mit jedem im Matrix-Netzwerk schreiben, ob sie nun Element oder eine andere Matrix-App nutzen, oder gar ein anderes Kommunikationssystem wie z.B. Slack, IRC oder XMPP. -SUPER SICHER: Echte Ende-zu-Ende-Verschlüsselung (nur Personen in der Konversation können die Nachrichten entschlüsseln), und Cross Signing um die Geräte der anderen Personen zu verifizieren. +SUPER SICHER: Echte Ende-zu-Ende-Verschlüsselung (nur Personen in der Konversation können die Nachrichten entschlüsseln), und Cross-Signing um die Geräte der anderen Personen zu verifizieren. VOLLSTÄNDIGE KOMMUNIKATION: Nachrichten, Telefonate und Videoanrufe, Teilen von Dateien oder dem eigenen Bildschirm und viele andere Integrationen, Bots und Widgets. Erstelle Räume, Communities, bleib in Kontakt und sei produktiv. -ÜBERALL WO DU BIST: Bleib in Kontakt wo auch immer du bist - mit einem vollständig synchronisierten Nachrichtenverlauf über alle Geräte und im Web auf https://app.element.io. +ÜBERALL WO DU BIST: Bleib in Kontakt wo auch immer du bist - mit einem vollständig synchronisierten Nachrichtenverlauf über alle Geräte und im Netz auf https://app.element.io. diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de/short_description.txt index 0ffacfd8d9..d2c30d4167 100644 --- a/fastlane/metadata/android/de/short_description.txt +++ b/fastlane/metadata/android/de/short_description.txt @@ -1 +1 @@ -Sicherer dezentraler Chat & Telefonie. Schütze deine Daten vor Dritten. +Sicherer dezentraler Chat und Telefonie. Schütze deine Daten vor Dritten. diff --git a/fastlane/metadata/android/en-US/changelogs/40101040.txt b/fastlane/metadata/android/en-US/changelogs/40101040.txt new file mode 100644 index 0000000000..e8977f3211 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Main changes in this version: performance improvement and bug fixes! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.4 \ No newline at end of file diff --git a/fastlane/metadata/android/et/changelogs/40101010.txt b/fastlane/metadata/android/et/changelogs/40101010.txt new file mode 100644 index 0000000000..4db2c52cb0 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: jõudluse parandused ja pisikohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/fa/changelogs/40100120.txt b/fastlane/metadata/android/fa/changelogs/40100120.txt new file mode 100644 index 0000000000..511cdb49fa --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100120.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: پیش‌نمایش نشانی، صفحه‌کلید اموجی جدید، تنظیم‌های اتاق جدید و برف برای کریسمس! +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/fa/changelogs/40100130.txt b/fastlane/metadata/android/fa/changelogs/40100130.txt new file mode 100644 index 0000000000..d78c76e041 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100130.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: پیش‌نمایش نشانی، صفحه‌کلید اموجی جدید، تنظیم‌های اتاق جدید و برف برای کریسمس! +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/fa/changelogs/40100140.txt b/fastlane/metadata/android/fa/changelogs/40100140.txt new file mode 100644 index 0000000000..5defa284aa --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100140.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: ویرایش اجازه‌های اتاق، زمینهٔ تاریک/روشن خودکار و رفع دسته‌ای از مشکل‌ها. +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/fa/changelogs/40100150.txt b/fastlane/metadata/android/fa/changelogs/40100150.txt new file mode 100644 index 0000000000..d856b3a252 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100150.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: پشتیبانی از ورود اجتماعی. +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/fa/changelogs/40100160.txt b/fastlane/metadata/android/fa/changelogs/40100160.txt new file mode 100644 index 0000000000..4d8aea0cb6 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100160.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: پشتیبانی از ورود اجتماعی. +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.15 و https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/fa/changelogs/40100170.txt b/fastlane/metadata/android/fa/changelogs/40100170.txt new file mode 100644 index 0000000000..6de164e57f --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100170.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: رفع مشکل‌ها! +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/fa/changelogs/40101000.txt b/fastlane/metadata/android/fa/changelogs/40101000.txt new file mode 100644 index 0000000000..6a3c154ae4 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101000.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: بهبود ویپ (تماس‌های صوتی و تصویری در پیام‌های مستقیم) و رفع مشکل‌ها! +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/fa/changelogs/40101010.txt b/fastlane/metadata/android/fa/changelogs/40101010.txt new file mode 100644 index 0000000000..8e29373452 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101010.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: بهبود عملکرد و رفع مشکل‌ها! +گزارش تغییر کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/fi/changelogs/40101000.txt b/fastlane/metadata/android/fi/changelogs/40101000.txt new file mode 100644 index 0000000000..1b85b6d00d --- /dev/null +++ b/fastlane/metadata/android/fi/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Suurimmat muutokset tässä versiossa: VoIP-parannuksia ja korjauksia (ääni- ja videopuhelut yksityiskeskusteluissa) +Täysi muutosloki: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/fi/changelogs/40101010.txt b/fastlane/metadata/android/fi/changelogs/40101010.txt new file mode 100644 index 0000000000..c79023c148 --- /dev/null +++ b/fastlane/metadata/android/fi/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Suurimmat muutokset tässä versiossa: suorituskykyparannuksia ja bugikorjauksia! +Täysi muutosloki: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/fr/changelogs/40100130.txt b/fastlane/metadata/android/fr/changelogs/40100130.txt index 412b2b9db2..a7e233616a 100644 --- a/fastlane/metadata/android/fr/changelogs/40100130.txt +++ b/fastlane/metadata/android/fr/changelogs/40100130.txt @@ -1,2 +1,2 @@ Principaux changements apportés par cette version : aperçu des URL, nouveau clavier Emoji, nouvelles options de configuration pour le salon et neige pour Noël. -Liste complète des changements : https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Liste complète des changements : https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/fr/changelogs/40100140.txt b/fastlane/metadata/android/fr/changelogs/40100140.txt new file mode 100644 index 0000000000..e823d7a89a --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : modification des permissions dans les salons, thème lumineux/sombre automatique, et plein de corrections de bugs. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/fr/changelogs/40100150.txt b/fastlane/metadata/android/fr/changelogs/40100150.txt new file mode 100644 index 0000000000..cfc92299d4 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : prise en charge de l’authentification avec les réseaux sociaux. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/fr/changelogs/40100160.txt b/fastlane/metadata/android/fr/changelogs/40100160.txt new file mode 100644 index 0000000000..b5bca83268 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : prise en charge de l’authentification avec les réseaux sociaux ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.0.15 et https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/fr/changelogs/40100170.txt b/fastlane/metadata/android/fr/changelogs/40100170.txt new file mode 100644 index 0000000000..5474f15417 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/40100170.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1..017 diff --git a/fastlane/metadata/android/fr/changelogs/40101000.txt b/fastlane/metadata/android/fr/changelogs/40101000.txt new file mode 100644 index 0000000000..e9330611ee --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : améliorations de la VoIP (appels audio et vidéo dans les conversations primées) et corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/fr/changelogs/40101010.txt b/fastlane/metadata/android/fr/changelogs/40101010.txt new file mode 100644 index 0000000000..8e9de64423 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : amélioration des performances et corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/ga/title.txt b/fastlane/metadata/android/ga/title.txt new file mode 100644 index 0000000000..85dd3fa07f --- /dev/null +++ b/fastlane/metadata/android/ga/title.txt @@ -0,0 +1 @@ +Element (Riot.im roimhe sin) diff --git a/fastlane/metadata/android/it/changelogs/40101000.txt b/fastlane/metadata/android/it/changelogs/40101000.txt new file mode 100644 index 0000000000..bd13c2c185 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: migliorato il VoIP (chiamate audio e video in MD) e correzione di errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/it/changelogs/40101010.txt b/fastlane/metadata/android/it/changelogs/40101010.txt new file mode 100644 index 0000000000..51e6659827 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: prestazioni migliorate e correzione di errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/kab/short_description.txt b/fastlane/metadata/android/kab/short_description.txt index bee72ea427..453d31fc2a 100644 --- a/fastlane/metadata/android/kab/short_description.txt +++ b/fastlane/metadata/android/kab/short_description.txt @@ -1 +1 @@ -Adiwenni aɣellsan ur nelli aslammas & VoIP. Ḥrez isefra-k•m seg tama tis tlata. +Adiwenni aɣellsan ur nelli d aslammas & VoIP. Ḥrez isefra-k•m seg wis tlata. diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101000.txt b/fastlane/metadata/android/pt-BR/changelogs/40101000.txt new file mode 100644 index 0000000000..8138e376c6 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Melhoria de VoIP (chamadas de áudio e vídeo em conversas) e correção de erros! +Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101010.txt b/fastlane/metadata/android/pt-BR/changelogs/40101010.txt new file mode 100644 index 0000000000..56f9c2955d --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: melhoria de desempenho e correção de erros! +Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/ru/changelogs/40101000.txt b/fastlane/metadata/android/ru/changelogs/40101000.txt new file mode 100644 index 0000000000..8ec344a85a --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: VoIP (аудио и видео звонки в ЛС) Улучшение и исправления ошибок! +Полный список изменений: https://github.com/vector-im/element-android/release/tag/v1.1.0 diff --git a/fastlane/metadata/android/ru/changelogs/40101010.txt b/fastlane/metadata/android/ru/changelogs/40101010.txt new file mode 100644 index 0000000000..7295e0df60 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: улучшение производительности и исправления ошибок! +Полный список изменений: https://github.com/vector-im/element-android/release/tag/v1.1.1 diff --git a/fastlane/metadata/android/sv/changelogs/40101010.txt b/fastlane/metadata/android/sv/changelogs/40101010.txt new file mode 100644 index 0000000000..66a3751aac --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Förbättringar och buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/tr/changelogs/40100140.txt b/fastlane/metadata/android/tr/changelogs/40100140.txt new file mode 100644 index 0000000000..9a5cf6d5f0 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Bu sürümdeki başlıca değişiklikler: Oda izinlerini düzenleme, otomatik koyu/açık tema ve bir avuç hata düzeltmeleri. +Değişim günlüğünün tamamı: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/tr/changelogs/40100170.txt b/fastlane/metadata/android/tr/changelogs/40100170.txt new file mode 100644 index 0000000000..a93cbb4908 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/40100170.txt @@ -0,0 +1,2 @@ +Bu sürümdeki başlıca değişiklikler: Hata düzeltmeleri! +değişim günlüğünün tamamı: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/tr/changelogs/40101000.txt b/fastlane/metadata/android/tr/changelogs/40101000.txt new file mode 100644 index 0000000000..ce457ee0f4 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Bu sürümdeki ana değişiklikler: VoIP (DM'de sesli ve görüntülü aramalar) geliştirmeleri ve hata düzeltmeleri! +Değişim günlüğünün tamamı: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/tr/changelogs/40101010.txt b/fastlane/metadata/android/tr/changelogs/40101010.txt new file mode 100644 index 0000000000..912d5bcd7c --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Bu sürümdeki ana değişiklikler: performans iyileştirme ve hata düzeltmeleri! +Değişim günlüğünün tamamı: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/uk/changelogs/40101010.txt b/fastlane/metadata/android/uk/changelogs/40101010.txt new file mode 100644 index 0000000000..085ac5a118 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: поліпшення продуктивності та виправлення помилок! +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100100.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100100.txt index 0dc493cf40..0c226c1c8f 100644 --- a/fastlane/metadata/android/zh-Hans/changelogs/40100100.txt +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100100.txt @@ -1,2 +1,2 @@ -此新版本主要包含错误修复和改进。现在,发送消息要快得多。 +此新版本主要包含错误修复和改进。现在,发送消息比以前快多了。 完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100120.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100120.txt new file mode 100644 index 0000000000..67d69a3834 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100120.txt @@ -0,0 +1,2 @@ +此版本的主要变化:链接预览,全新 Emoji 键盘,全新聊天室设置功能,以及圣诞节雪花! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100130.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100130.txt new file mode 100644 index 0000000000..5a2ba4256f --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100130.txt @@ -0,0 +1,2 @@ +此版本的主要变化:链接预览,全新 Emoji 键盘,全新聊天室设置功能,以及圣诞节雪花! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100140.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100140.txt new file mode 100644 index 0000000000..dc25b5094b --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100140.txt @@ -0,0 +1,2 @@ +此版本的主要变化:支持编辑聊天室权限,自动切换浅色/深色主题,修复大量错误。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100150.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100150.txt new file mode 100644 index 0000000000..d5f37ff3a6 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100150.txt @@ -0,0 +1,2 @@ +此版本的主要变化:支持通过社交网络登录。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100160.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100160.txt new file mode 100644 index 0000000000..c0658e1881 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100160.txt @@ -0,0 +1,2 @@ +此版本的主要变化:支持通过社交网络登录。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.15 和 https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40100170.txt b/fastlane/metadata/android/zh-Hans/changelogs/40100170.txt new file mode 100644 index 0000000000..55cbadb37f --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40100170.txt @@ -0,0 +1,2 @@ +此版本的主要变化:修复错误! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40101000.txt b/fastlane/metadata/android/zh-Hans/changelogs/40101000.txt new file mode 100644 index 0000000000..95bd9c55c0 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40101000.txt @@ -0,0 +1,2 @@ +此版本的主要变化:改进 VoIP(私聊中的音频与视频通话)以及修复错误! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/40101010.txt b/fastlane/metadata/android/zh-Hans/changelogs/40101010.txt new file mode 100644 index 0000000000..9a4e611cf9 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/40101010.txt @@ -0,0 +1,2 @@ +此版本的主要变化:改进性能以及修复错误! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/zh-Hans/full_description.txt b/fastlane/metadata/android/zh-Hans/full_description.txt index 12664f7c9b..4791c9652b 100644 --- a/fastlane/metadata/android/zh-Hans/full_description.txt +++ b/fastlane/metadata/android/zh-Hans/full_description.txt @@ -1,30 +1,30 @@ -Element 是一种新型消息和协作应用: +Element 是一种新型的通讯与协作应用: 1. 使您可以掌控您的隐私 -2. 使您与 Matrix 网络中的任何人交流,甚至可以通过与其他应用如 Slack 集成 -3. 保护您远离广告,数据挖掘和围墙花园 -4. 通过端到端加密保护您,通过交叉签名验证其他人 +2. 使您与 Matrix 网络中的任何人交流,甚至可以通过集成功能与如 Slack 之类的其他应用通讯 +3. 保护您免受广告,大数据挖掘和封闭服务的侵害 +4. 通过端到端加密保证安全,通过交叉签名验证其他人 -Element 与其他消息和协作应用完全不同,因为它是去中心化且开源的。 +Element 与其他通讯与协作应用完全不同,因为它是去中心化且开源的。 -Element 使您可以自托管 - 或选择托管商 - 因此您拥有您的数据和会话的隐私权,所有权和控制权。它使您可以访问开放网络;因此您可以不仅仅与其他 Element 用户交流。并且它非常安全。 +Element 允许您自托管——或者选择托管商——因此,您能拥有数据和会话的隐私权,所有权和控制权。它允许您访问开放网络;因此,您可以与 Element 用户以外的人交流。并且它非常安全。 -Element 可以做到这些因为它在 Matrix 上运行 - 开放,去中心化通信标准。 +Element 之所以可以做到这些,是因为它在 Matrix 上运行——开放,去中心化通讯的标准。 -Element 通过让您选择谁来托管您的会话使您掌控一切。在 Element 应用中,您可以选择不同的托管方式: +通过让您选择由谁来托管您的会话,Element 让您掌控一切。在 Element 应用中,您可以选择不同的托管方式: -1. 在由 Matrix 开发者托管的 matrix.org 公共服务器上获取免费帐户,或从志愿者托管的几千个公共服务器中选择 -2. 在您自己的硬件上运行服务器自托管您的会话 -3. 通过简单地订阅 Element Matrix Services 托管平台在自定义服务器上注册账户 +1. 在由 Matrix 开发者托管的 matrix.org 公共服务器上获取免费帐户,或从志愿者托管的上千个公共服务器中选择 +2. 在您自己的硬件上运行服务器,自托管您的会话 +3. 通过订阅 Element Matrix Services 托管平台,简单地在自定义服务器上注册账户 为什么选择 Element? -拥有您的数据:您来决定存放您的数据和消息的位置。拥有并控制它的是您,而不是挖掘您的数据或与第三方分享的巨型企业。 +掌控您的数据:您来决定存放您的数据和消息的位置。拥有并控制它的是您,而不是挖掘您的数据或与第三方分享的巨型企业。 -开放消息与协作:您可以与 Matrix 网络中的任何人聊天,不论他们使用 Element 还是其他 Matrix 应用,甚至即使他们在使用不同的消息系统例如 Slack,IRC 或 XMPP。 +开放通讯与协作:您可以与 Matrix 网络中的任何人聊天,不论他们使用 Element 还是其他 Matrix 应用,甚至/即使他们在使用不同的通讯系统,例如 Slack,IRC 或 XMPP。 -超级安全:真正的端到端加密(仅有会话中的人可以解密消息),及用于验证会话参与方的设备的交叉签名。 +超级安全:支持真正的端到端加密(仅有会话中的人可以解密消息),还有能够验证会话参与方的设备的交叉签名。 -丰富的通信方式:消息,语音和视频通话,文件分享,屏幕分享和大量集成,机器人和小部件。建立房间,社区,保持联系并做好工作。 +完善的通讯方式:消息,语音和视频通话,文件共享,屏幕共享和大量集成功能,机器人和小挂件。建立房间与社区,保持联系并完成工作。 -随时随地:通过在您的全部设备和 https://app.element.io 网页上完全同步的消息历史,无论您在哪里都可以保持联系。 +随时随地:消息历史可在您的全部设备和 https://app.element.io 网页端之间完全同步,无论您在哪里,都可以保持联系。 diff --git a/fastlane/metadata/android/zh-Hans/short_description.txt b/fastlane/metadata/android/zh-Hans/short_description.txt index 87d127335b..53d7d33403 100644 --- a/fastlane/metadata/android/zh-Hans/short_description.txt +++ b/fastlane/metadata/android/zh-Hans/short_description.txt @@ -1 +1 @@ -安全去中心化的聊天和 VoIP。保护您的数据不受第三方的影响。 +安全、去中心化的聊天与 VoIP 通话。保护您的数据不被第三方窃取。 diff --git a/fastlane/metadata/android/zh-Hant/changelogs/40101010.txt b/fastlane/metadata/android/zh-Hant/changelogs/40101010.txt new file mode 100644 index 0000000000..8b0e45e6b3 --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/changelogs/40101010.txt @@ -0,0 +1,2 @@ +此版本的主要變更:效能改進與錯誤修復! +完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index b938f60e39..21db4e1893 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -21,6 +21,7 @@ import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single import kotlinx.coroutines.rx2.rxCompletable +import kotlinx.coroutines.rx2.rxSingle import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.ThreePid @@ -90,13 +91,13 @@ class RxRoom(private val room: Room) { return room.getMyReadReceiptLive().asObservable() } - fun loadRoomMembersIfNeeded(): Single = singleBuilder { - room.loadRoomMembersIfNeeded(it) + fun loadRoomMembersIfNeeded(): Single = rxSingle { + room.loadRoomMembersIfNeeded() } fun joinRoom(reason: String? = null, - viaServers: List = emptyList()): Single = singleBuilder { - room.join(reason, viaServers, it) + viaServers: List = emptyList()): Single = rxSingle { + room.join(reason, viaServers) } fun liveEventReadReceipts(eventId: String): Observable> { @@ -114,12 +115,12 @@ class RxRoom(private val room: Room) { return room.getLiveRoomNotificationState().asObservable() } - fun invite(userId: String, reason: String? = null): Completable = completableBuilder { - room.invite(userId, reason, it) + fun invite(userId: String, reason: String? = null): Completable = rxCompletable { + room.invite(userId, reason) } - fun invite3pid(threePid: ThreePid): Completable = completableBuilder { - room.invite3pid(threePid, it) + fun invite3pid(threePid: ThreePid): Completable = rxCompletable { + room.invite3pid(threePid) } fun updateTopic(topic: String): Completable = rxCompletable { diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index a7b269fcc6..0d5b5ed821 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -20,6 +20,7 @@ import androidx.paging.PagedList import io.reactivex.Observable import io.reactivex.Single import io.reactivex.functions.Function3 +import kotlinx.coroutines.rx2.rxSingle import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -129,8 +130,8 @@ class RxSession(private val session: Session) { fun searchUsersDirectory(search: String, limit: Int, - excludedUserIds: Set): Single> = singleBuilder { - session.searchUsersDirectory(search, limit, excludedUserIds, it) + excludedUserIds: Set): Single> = rxSingle { + session.searchUsersDirectory(search, limit, excludedUserIds) } fun joinRoom(roomIdOrAlias: String, @@ -144,8 +145,8 @@ class RxSession(private val session: Session) { session.getRoomIdByAlias(roomAlias, searchOnServer, it) } - fun getProfileInfo(userId: String): Single = singleBuilder { - session.getProfile(userId, it) + fun getProfileInfo(userId: String): Single = rxSingle { + session.getProfile(userId) } fun liveUserCryptoDevices(userId: String): Observable> { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index c9918cceaf..34460c6ab5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -6,10 +6,10 @@ apply plugin: 'realm-android' buildscript { repositories { - jcenter() + mavenCentral() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.3.1" + classpath "io.realm:realm-gradle-plugin:10.4.0" } } @@ -108,7 +108,7 @@ static def gitRevisionDate() { dependencies { def arrow_version = "0.8.2" - def moshi_version = '1.11.0' + def moshi_version = '1.12.0' def lifecycle_version = '2.2.0' def arch_version = '2.1.0' def markwon_version = '3.1.0' @@ -163,16 +163,16 @@ dependencies { // Logging implementation 'com.jakewharton.timber:timber:4.7.1' - implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' + implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.19' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.21' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.5.1' //testImplementation 'org.robolectric:shadows-support-v4:3.0' // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 - testImplementation 'io.mockk:mockk:1.10.6' + testImplementation 'io.mockk:mockk:1.11.0' testImplementation 'org.amshove.kluent:kluent-android:1.65' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" // Plant Timber tree for test @@ -185,8 +185,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'org.amshove.kluent:kluent-android:1.61' - // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 - androidTestImplementation 'io.mockk:mockk-android:1.10.6' + androidTestImplementation 'io.mockk:mockk-android:1.11.0' androidTestImplementation "androidx.arch.core:core-testing:$arch_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" // Plant Timber tree for test diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt index b784884363..583406346e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import org.matrix.android.sdk.test.shared.createTimberTestRule import org.junit.Rule -import java.io.File interface InstrumentedTest { @@ -30,8 +29,4 @@ interface InstrumentedTest { fun context(): Context { return ApplicationProvider.getApplicationContext() } - - fun cacheDir(): File { - return context().cacheDir - } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index 03943cea14..c439da8407 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -27,9 +27,12 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.api.network.ApiInterceptorListener +import org.matrix.android.sdk.api.network.ApiPath import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.network.ApiInterceptor import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager @@ -51,6 +54,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService + @Inject internal lateinit var apiInterceptor: ApiInterceptor private val uiHandler = Handler(Looper.getMainLooper()) @@ -79,6 +83,14 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo return legacySessionImporter } + fun registerApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) { + apiInterceptor.addListener(path, listener) + } + + fun unregisterApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) { + apiInterceptor.removeListener(path, listener) + } + companion object { private lateinit var instance: Matrix diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt new file mode 100644 index 0000000000..9371154aaf --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.network + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import timber.log.Timber + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class ApiInterceptorTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun apiInterceptorTest() { + val responses = mutableListOf() + + val listener = object : ApiInterceptorListener { + override fun onApiResponse(path: ApiPath, response: String) { + Timber.w("onApiResponse($path): $response") + responses.add(response) + } + } + + commonTestHelper.matrix.registerApiInterceptorListener(ApiPath.REGISTER, listener) + + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + commonTestHelper.signOutAndClose(session) + + commonTestHelper.matrix.unregisterApiInterceptorListener(ApiPath.REGISTER, listener) + + responses.size shouldBeEqualTo 2 + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index eb7e4a9fbe..5815b23c06 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -112,8 +112,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { bobRoomSummariesLive.observeForever(newRoomObserver) } - mTestHelper.doSync { - aliceRoom.invite(bobSession.myUserId, callback = it) + mTestHelper.runBlockingTest { + aliceRoom.invite(bobSession.myUserId) } mTestHelper.await(lock1) @@ -172,8 +172,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { fun createSamAccountAndInviteToTheRoom(room: Room): Session { val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) - mTestHelper.doSync { - room.invite(samSession.myUserId, null, it) + mTestHelper.runBlockingTest { + room.invite(samSession.myUserId, null) } mTestHelper.doSync { @@ -337,8 +337,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { requestID, roomId, bob.myUserId, - bob.sessionParams.credentials.deviceId!!, - null) + bob.sessionParams.credentials.deviceId!!) // we should reach SHOW SAS on both var alicePovTx: OutgoingSasVerificationTransaction? = null @@ -411,7 +410,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { val sessions = mutableListOf(aliceSession) for (index in 1 until numberOfMembers) { val session = mTestHelper.createAccount("User_$index", defaultSessionParams) - mTestHelper.doSync(timeout = 600_000) { room.invite(session.myUserId, null, it) } + mTestHelper.runBlockingTest(timeout = 600_000) { room.invite(session.myUserId, null) } println("TEST -> " + session.myUserId + " invited") mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } println("TEST -> " + session.myUserId + " joined") diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt index 7a1d4604f0..af2d57f9ce 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt @@ -18,23 +18,26 @@ package org.matrix.android.sdk.common import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider -class TestRoomDisplayNameFallbackProvider() : RoomDisplayNameFallbackProvider { +class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { override fun getNameForRoomInvite() = "Room invite" - override fun getNameForEmptyRoom() = + override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List) = "Empty room" - override fun getNameFor2members(name1: String?, name2: String?) = + override fun getNameFor1member(name: String) = + name + + override fun getNameFor2members(name1: String, name2: String) = "$name1 and $name2" - override fun getNameFor3members(name1: String?, name2: String?, name3: String?) = + override fun getNameFor3members(name1: String, name2: String, name3: String) = "$name1, $name2 and $name3" - override fun getNameFor4members(name1: String?, name2: String?, name3: String?, name4: String?) = + override fun getNameFor4members(name1: String, name2: String, name3: String, name4: String) = "$name1, $name2, $name3 and $name4" - override fun getNameFor4membersAndMore(name1: String?, name2: String?, name3: String?, remainingCount: Int) = + override fun getNameFor4membersAndMore(name1: String, name2: String, name3: String, remainingCount: Int) = "$name1, $name2, $name3 and $remainingCount others" } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 8c3917adc1..e6b364f3fb 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -367,8 +367,8 @@ class KeyShareTests : InstrumentedTest { } // Let alice invite bob - mTestHelper.doSync { - roomAlicePov.invite(bobSession.myUserId, null, it) + mTestHelper.runBlockingTest { + roomAlicePov.invite(bobSession.myUserId, null) } mTestHelper.doSync { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index 0489ee179f..eb4773f3c8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.ssss import androidx.lifecycle.Observer import androidx.test.ext.junit.runners.AndroidJUnit4 import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent import org.matrix.android.sdk.api.session.securestorage.KeySigner @@ -31,7 +30,6 @@ import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService @@ -40,7 +38,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.amshove.kluent.shouldBe import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -70,8 +67,8 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - mTestHelper.doSync { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) + mTestHelper.runBlockingTest { + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) } // Assert Account data is updated @@ -99,7 +96,9 @@ class QuadSTests : InstrumentedTest { assertNull("Key was not generated from passphrase", parsed.passphrase) // Set as default key - quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback {}) + GlobalScope.launch { + quadS.setDefaultKey(TEST_KEY_ID) + } var defaultKeyAccountData: UserAccountDataEvent? = null val defaultDataLock = CountDownLatch(1) @@ -133,12 +132,11 @@ class QuadSTests : InstrumentedTest { // Store a secret val clearSecret = "42".toByteArray().toBase64NoPadding() - mTestHelper.doSync { + mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.storeSecret( "secret.of.life", clearSecret, - listOf(SharedSecretStorageService.KeyRef(null, keySpec)), // default key - it + listOf(SharedSecretStorageService.KeyRef(null, keySpec)) // default key ) } @@ -155,12 +153,11 @@ class QuadSTests : InstrumentedTest { // Try to decrypt?? - val decryptedSecret = mTestHelper.doSync { + val decryptedSecret = mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.getSecret( "secret.of.life", null, // default key - keySpec!!, - it + keySpec!! ) } @@ -176,13 +173,13 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - mTestHelper.doSync { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) + mTestHelper.runBlockingTest { + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) } // Test that we don't need to wait for an account data sync to access directly the keyid from DB - mTestHelper.doSync { - quadS.setDefaultKey(TEST_KEY_ID, it) + mTestHelper.runBlockingTest { + quadS.setDefaultKey(TEST_KEY_ID) } mTestHelper.signOutAndClose(aliceSession) @@ -198,15 +195,14 @@ class QuadSTests : InstrumentedTest { val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - mTestHelper.doSync { + mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.storeSecret( "my.secret", mySecretText.toByteArray().toBase64NoPadding(), listOf( SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) - ), - it + ) ) } @@ -219,19 +215,17 @@ class QuadSTests : InstrumentedTest { assertNotNull(encryptedContent?.get(keyId2)) // Assert that can decrypt with both keys - mTestHelper.doSync { + mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.getSecret("my.secret", keyId1, - RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, - it + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! ) } - mTestHelper.doSync { + mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.getSecret("my.secret", keyId2, - RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, - it + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! ) } @@ -247,50 +241,34 @@ class QuadSTests : InstrumentedTest { val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - mTestHelper.doSync { + mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.storeSecret( "my.secret", mySecretText.toByteArray().toBase64NoPadding(), - listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))), - it + listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) ) } - val decryptCountDownLatch = CountDownLatch(1) - var error = false - aliceSession.sharedSecretStorageService.getSecret("my.secret", - keyId1, - RawBytesKeySpec.fromPassphrase( - "A bad passphrase", - key1Info.content?.passphrase?.salt ?: "", - key1Info.content?.passphrase?.iterations ?: 0, - null), - object : MatrixCallback { - override fun onSuccess(data: String) { - decryptCountDownLatch.countDown() - } - - override fun onFailure(failure: Throwable) { - error = true - decryptCountDownLatch.countDown() - } - } - ) - - mTestHelper.await(decryptCountDownLatch) - - error shouldBe true + mTestHelper.runBlockingTest { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + "A bad passphrase", + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null) + ) + } // Now try with correct key - mTestHelper.doSync { + mTestHelper.runBlockingTest { aliceSession.sharedSecretStorageService.getSecret("my.secret", keyId1, RawBytesKeySpec.fromPassphrase( passphrase, key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.iterations ?: 0, - null), - it + null) ) } @@ -321,15 +299,15 @@ class QuadSTests : InstrumentedTest { private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService - val creationInfo = mTestHelper.doSync { - quadS.generateKey(keyId, null, keyId, emptyKeySigner, it) + val creationInfo = mTestHelper.runBlockingTest { + quadS.generateKey(keyId, null, keyId, emptyKeySigner) } assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - mTestHelper.doSync { - quadS.setDefaultKey(keyId, it) + mTestHelper.runBlockingTest { + quadS.setDefaultKey(keyId) } assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } @@ -340,21 +318,20 @@ class QuadSTests : InstrumentedTest { private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService - val creationInfo = mTestHelper.doSync { + val creationInfo = mTestHelper.runBlockingTest { quadS.generateKeyWithPassphrase( keyId, keyId, passphrase, emptyKeySigner, - null, - it) + null) } assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - val setDefaultLatch = CountDownLatch(1) - quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch)) - mTestHelper.await(setDefaultLatch) + mTestHelper.runBlockingTest { + quadS.setDefaultKey(keyId) + } assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index a81f503e77..4ea8cdc074 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -593,16 +593,14 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, bobSession.myUserId, - bobSession.sessionParams.deviceId!!, - null) + bobSession.sessionParams.deviceId!!) bobVerificationService.beginKeyVerificationInDMs( VerificationMethod.SAS, requestID!!, cryptoTestData.roomId, aliceSession.myUserId, - aliceSession.sessionParams.deviceId!!, - null) + aliceSession.sessionParams.deviceId!!) // we should reach SHOW SAS on both var alicePovTx: SasVerificationTransaction? diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index cadb83ca00..1baf490dfc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -17,115 +17,38 @@ package org.matrix.android.sdk.session.search import org.junit.Assert.assertTrue -import org.junit.Assert.fail import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestData import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.TestConstants import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) class SearchMessagesTest : InstrumentedTest { - private val MESSAGE = "Lorem ipsum dolor sit amet" + companion object { + private const val MESSAGE = "Lorem ipsum dolor sit amet" + } private val commonTestHelper = CommonTestHelper(context()) private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) @Test fun sendTextMessageAndSearchPartOfItUsingSession() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - aliceSession.cryptoService().setWarnOnUnknownDevices(false) - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(10)) - aliceTimeline.start() - - commonTestHelper.sendTextMessage( - roomFromAlicePOV, - MESSAGE, - 2) - - run { - val lock = CountDownLatch(1) - - val eventListener = commonTestHelper.createEventListener(lock) { snapshot -> - snapshot.count { it.root.content.toModel()?.body?.startsWith(MESSAGE).orFalse() } == 2 - } - - aliceTimeline.addListener(eventListener) - commonTestHelper.await(lock) - - val data = commonTestHelper.runBlockingTest { - aliceSession - .searchService() - .search( - searchTerm = "lore", - limit = 10, - includeProfile = true, - afterLimit = 0, - beforeLimit = 10, - orderByRecent = true, - nextBatch = null, - roomId = aliceRoomId - ) - } - assertTrue(data.results?.size == 2) - assertTrue( - data.results - ?.all { - (it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse() - }.orFalse() - ) - - aliceTimeline.removeAllListeners() - cryptoTestData.cleanUp(commonTestHelper) - } - - aliceSession.startSync(true) - } - - @Test - fun sendTextMessageAndSearchPartOfItUsingRoom() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - aliceSession.cryptoService().setWarnOnUnknownDevices(false) - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(10)) - aliceTimeline.start() - - commonTestHelper.sendTextMessage( - roomFromAlicePOV, - MESSAGE, - 2) - - run { - var lock = CountDownLatch(1) - - val eventListener = commonTestHelper.createEventListener(lock) { snapshot -> - snapshot.count { it.root.content.toModel()?.body?.startsWith(MESSAGE).orFalse() } == 2 - } - - aliceTimeline.addListener(eventListener) - commonTestHelper.await(lock) - - lock = CountDownLatch(1) - roomFromAlicePOV + doTest { cryptoTestData -> + cryptoTestData.firstSession + .searchService() .search( searchTerm = "lore", limit = 10, @@ -134,32 +57,64 @@ class SearchMessagesTest : InstrumentedTest { beforeLimit = 10, orderByRecent = true, nextBatch = null, - callback = object : MatrixCallback { - override fun onSuccess(data: SearchResult) { - super.onSuccess(data) - assertTrue(data.results?.size == 2) - assertTrue( - data.results - ?.all { - (it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse() - }.orFalse() - ) - lock.countDown() - } - - override fun onFailure(failure: Throwable) { - super.onFailure(failure) - fail(failure.localizedMessage) - lock.countDown() - } - } + roomId = cryptoTestData.roomId ) - lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS) + } + } - aliceTimeline.removeAllListeners() - cryptoTestData.cleanUp(commonTestHelper) + @Test + fun sendTextMessageAndSearchPartOfItUsingRoom() { + doTest { cryptoTestData -> + cryptoTestData.firstSession + .getRoom(cryptoTestData.roomId)!! + .search( + searchTerm = "lore", + limit = 10, + includeProfile = true, + afterLimit = 0, + beforeLimit = 10, + orderByRecent = true, + nextBatch = null + ) + } + } + + private fun doTest(block: suspend (CryptoTestData) -> SearchResult) { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(10)) + aliceTimeline.start() + + val lock = CountDownLatch(1) + + val eventListener = commonTestHelper.createEventListener(lock) { snapshot -> + snapshot.count { it.root.content.toModel()?.body?.startsWith(MESSAGE).orFalse() } == 2 } - aliceSession.startSync(true) + aliceTimeline.addListener(eventListener) + + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + MESSAGE, + 2) + + commonTestHelper.await(lock) + + val data = commonTestHelper.runBlockingTest { + block.invoke(cryptoTestData) + } + + assertTrue(data.results?.size == 2) + assertTrue( + data.results + ?.all { + (it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse() + }.orFalse() + ) + + aliceTimeline.removeAllListeners() + cryptoTestData.cleanUp(commonTestHelper) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index a5d457222f..9980259266 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -25,9 +25,12 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.api.network.ApiInterceptorListener +import org.matrix.android.sdk.api.network.ApiPath import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.di.DaggerMatrixComponent +import org.matrix.android.sdk.internal.network.ApiInterceptor import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager @@ -49,6 +52,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService + @Inject internal lateinit var apiInterceptor: ApiInterceptor init { Monarchy.init(context) @@ -73,6 +77,14 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo return legacySessionImporter } + fun registerApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) { + apiInterceptor.addListener(path, listener) + } + + fun unregisterApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) { + apiInterceptor.removeListener(path, listener) + } + companion object { private lateinit var instance: Matrix diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt index 4ac14d5f63..a34dbcc196 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt @@ -18,9 +18,10 @@ package org.matrix.android.sdk.api interface RoomDisplayNameFallbackProvider { fun getNameForRoomInvite(): String - fun getNameForEmptyRoom(): String - fun getNameFor2members(name1: String?, name2: String?): String - fun getNameFor3members(name1: String?, name2: String?, name3: String?): String - fun getNameFor4members(name1: String?, name2: String?, name3: String?, name4: String?): String - fun getNameFor4membersAndMore(name1: String?, name2: String?, name3: String?, remainingCount: Int): String + fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String + fun getNameFor1member(name: String): String + fun getNameFor2members(name1: String, name2: String): String + fun getNameFor3members(name1: String, name2: String, name3: String): String + fun getNameFor4members(name1: String, name2: String, name3: String, name4: String): String + fun getNameFor4membersAndMore(name1: String, name2: String, name3: String, remainingCount: Int): String } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationAvailability.kt similarity index 62% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationAvailability.kt index 63d37f409f..f9a7ace7ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationAvailability.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.util +package org.matrix.android.sdk.api.auth.registration -import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.failure.Failure -/** - * Simple MatrixCallback implementation which delegate its calls to another callback - */ -open class MatrixCallbackDelegate(private val callback: MatrixCallback) : MatrixCallback by callback +sealed class RegistrationAvailability { + object Available : RegistrationAvailability() + data class NotAvailable(val failure: Failure.ServerError) : RegistrationAvailability() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt index d00c9a0c82..38a5a77291 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -36,6 +36,8 @@ interface RegistrationWizard { suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult + suspend fun registrationAvailable(userName: String): RegistrationAvailability + val currentThreePid: String? // True when login and password has been sent with success to the homeserver diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index e0ee9f36ba..0ba61e5890 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -37,6 +37,18 @@ fun Throwable.shouldBeRetried(): Boolean { || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) } +/** + * Get the retry delay in case of rate limit exceeded error, adding 100 ms, of defaultValue otherwise + */ +fun Throwable.getRetryDelay(defaultValue: Long): Long { + return (this as? Failure.ServerError) + ?.error + ?.takeIf { it.code == MatrixError.M_LIMIT_EXCEEDED } + ?.retryAfterMillis + ?.plus(100L) + ?: defaultValue +} + fun Throwable.isInvalidPassword(): Boolean { return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN @@ -53,13 +65,16 @@ fun Throwable.isInvalidUIAAuth(): Boolean { * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible */ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { - return if (this is Failure.OtherServerError && httpCode == 401) { + return if (this is Failure.OtherServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */) { tryOrNull { MoshiProvider.providesMoshi() .adapter(RegistrationFlowResponse::class.java) .fromJson(errorBody) } - } else if (this is Failure.ServerError && httpCode == 401 && error.code == MatrixError.M_FORBIDDEN) { + } else if (this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && error.code == MatrixError.M_FORBIDDEN) { // This happens when the submission for this stage was bad (like bad password) if (error.session != null && error.flows != null) { RegistrationFlowResponse( @@ -75,3 +90,11 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { null } } + +fun Throwable.isRegistrationAvailabilityError(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_BAD_REQUEST /* 400 */ + && (error.code == MatrixError.M_USER_IN_USE + || error.code == MatrixError.M_INVALID_USERNAME + || error.code == MatrixError.M_EXCLUSIVE) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiInterceptorListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiInterceptorListener.kt new file mode 100644 index 0000000000..ad21da8fdf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiInterceptorListener.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.network + +interface ApiInterceptorListener { + fun onApiResponse(path: ApiPath, response: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt new file mode 100644 index 0000000000..db112a30b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.network + +import org.matrix.android.sdk.internal.network.NetworkConstants + +enum class ApiPath(val path: String, val method: String) { + // AuthApi + VERSIONS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "versions", "GET"), + REGISTER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register", "POST"), + ADD_3PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken", "POST"), + LOGIN_FLOWS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login", "GET"), + LOGIN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login", "POST"), + RESET_PASSWORD(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken", "POST"), + RESET_PASSWORD_MAIL_CONFIRMED(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password", "POST"), + + // DirectoryApi + ROOM_ID_BY_ALIAS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}", "GET"), + ROOM_DIRECTORY_VISIBILITY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}", "GET"), + SET_ROOM_DIRECTORY_VISIBILITY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}", "PUT"), + ADD_ROOM_ALIAS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}", "PUT"), + DELETE_ROOM_ALIAS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}", "DELETE"), + + // CryptoApi + GET_DEVICES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices", "GET"), + GET_DEVICE_INFO(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}", "GET"), + UPLOAD_KEYS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload", "POST"), + DOWNLOAD_KEYS_FOR_USERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query", "POST"), + UPLOAD_SIGNING_KEYS(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload", "POST"), + UPLOAD_SIGNATURES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload", "POST"), + CLAIM_ONE_TIME_KEYS_FOR_USERS_DEVICES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim", "POST"), + SEND_TO_DEVICE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}", "PUT"), + DELETE_DEVICE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", "DELETE"), + UPDATE_DEVICE_INFO(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", "PUT"), + GET_KEY_CHANGES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes", "GET"), + + // RoomKeysApi + CREATE_KEYS_BACKUP_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version", "POST"), + GET_KEYS_BACKUP_LAST_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version", "GET"), + GET_KEYS_BACKUP_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}", "GET"), + UPDATE_KEYS_BACKUP_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}", "PUT"), + STORE_ROOM_SESSION_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}", "PUT"), + STORE_ROOM_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}", "PUT"), + STORE_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys", "PUT"), + GET_ROOM_SESSION_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}", "GET"), + GET_ROOM_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}", "GET"), + GET_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys", "GET"), + DELETE_ROOM_SESSION_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}", "DELETE"), + DELETE_ROOM_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}", "DELETE"), + DELETE_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys", "DELETE"), + DELETE_BACKUP(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}", "DELETE"), + + // AccountApi + CHANGE_PASSWORD(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password", "POST"), + DEACTIVATE_ACCOUNT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate", "POST"), + + // SearchApi + SEARCH(NetworkConstants.URI_API_PREFIX_PATH_R0 + "search", "POST"), + + // FederationApi + GET_FEDERATION_VERSION(NetworkConstants.URI_FEDERATION_PATH + "version", "GET"), + + // VoipApi + GET_TURN_SERVER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer", "GET"), + + // PushGatewayApi + NOTIFY_PUSH_GATEWAY(NetworkConstants.URI_PUSH_GATEWAY_PREFIX_PATH + "notify", "POST"), + + // GroupApi + GET_GROUP_SUMMARY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary", "GET"), + GET_GROUP_ROOMS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/rooms", "GET"), + GET_GROUP_USERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/users", "GET"), + + // CapabilitiesApi + GET_CAPABILITIES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities", "GET"), + GET_VERSIONS(NetworkConstants.URI_API_PREFIX_PATH_ + "versions", "GET"), + PING(NetworkConstants.URI_API_PREFIX_PATH_ + "versions", "GET"), + + // IdentityApi + GET_ACCOUNT(NetworkConstants.URI_IDENTITY_PATH_V2 + "account", "GET"), + LOGOUT(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout", "POST"), + IDENTITY_HAS_DETAILS(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details", "GET"), + LOOKUP(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup", "POST"), + REQUEST_TOKEN_TO_BIND_EMAIL(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken", "POST"), + REQUEST_TOKEN_TO_BIND_MSISDN(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken", "POST"), + SUBMIT_TOKEN(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken", "POST"), + + // FilterApi + UPLOAD_FILTER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter", "POST"), + GET_FILTER_BY_ID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}", "GET"), + + // IndentityAuthApi + IDENTITY_REGISTER(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register", "POST"), + + // MediaApi + GET_MEDIA_CONFIG(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config", "GET"), + GET_PREVIEW_URL_DATA(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url", "GET"), + + // OpenIdApi + OPEN_ID_TOKEN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token", "POST"), + + // ProfileApi + GET_PROFILE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}", "GET"), + GET_THREE_PIDS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid", "GET"), + SET_DISPLAY_NAME(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname", "PUT"), + SET_AVATAR_URL(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url", "PUT"), + BIND_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind", "POST"), + UNBIND_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind", "POST"), + ADD_EMAIL(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken", "POST"), + ADD_MSISDN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken", "POST"), + FINALIZE_ADD_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add", "POST"), + DELETE_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete", "POST"), + + // PusherRulesApi + GET_ALL_PUSHER_RULES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/", "GET"), + UPDATE_ENABLE_PUSH_RULE_STATUS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled", "PUT"), + UPDATE_PUSH_RULE_ACTIONS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions", "PUT"), + DELETE_PUSH_RULE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}", "DELETE"), + ADD_PUSH_RULE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}", "PUT"), + + // PusherApi + GET_PUSHERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers", "GET"), + SET_PUSHER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set", "POST"), + + // SignOutApi + LOGIN_AGAIN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login", "POST"), + SIGN_OUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "logout", "POST"), + + // RoomApi + GET_PUBLIC_ROOMS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms", "POST"), + CREATE_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom", "POST"), + GET_ROOM_MESSAGES_FROM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages", "GET"), + GET_MEMBERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/members", "GET"), + SEND_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}", "PUT"), + GET_CONTEXT_OF_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}", "GET"), + GET_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}", "GET"), + SEND_READ_MARKER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers", "POST"), + INVITE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite", "POST"), + INVITE_USING_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite", "POST"), + SEND_STATE_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}", "PUT"), + SEND_STATE_EVENT_WITH_STATE_KEY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}", "PUT"), + GET_ROOM_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state", "GET"), + SEND_RELATION(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}", "POST"), + GET_RELATIONS(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}", "GET"), + JOIN_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}", "POST"), + LEAVE_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave", "POST"), + BAN_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban", "POST"), + UNBAN_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban", "POST"), + KICK_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick", "POST"), + REDACT_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}", "PUT"), + REPORT_CONTENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}", "POST"), + GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases", "GET"), + SEND_TYPING_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}", "PUT"), + PUT_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "PUT"), + DELETE_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "DELETE"), + + // SyncApi + SYNC(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync", "GET"), + + // ThirdPartyApi + THIRD_PARTY_PROTOCOLS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols", "GET"), + THIRD_PARTY_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}", "GET"), + + // SearchUserApi + SEARCH_USERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search", "POST"), + + // AccountDataApi + SET_ACCOUNT_DATA(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}", "PUT") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt index 4da1662681..d9bf5cfd13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt @@ -39,6 +39,8 @@ interface PushRuleService { fun removePushRuleListener(listener: PushRuleListener) + fun getActions(event: Event): List + // fun fulfilledBingRule(event: Event, rules: List): PushRule? interface PushRuleListener { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt new file mode 100644 index 0000000000..c8ccc4c8a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.query + +enum class RoomCategoryFilter { + ONLY_DM, + ONLY_ROOMS, + ONLY_WITH_NOTIFICATIONS, + ALL +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt new file mode 100644 index 0000000000..613916bc18 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.query + +data class RoomTagQueryFilter( + val isFavorite: Boolean?, + val isLowPriority: Boolean?, + val isServerNotice: Boolean? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 7a24ccac11..a15799d862 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService @@ -68,6 +69,7 @@ interface Session : SignOutService, FilterService, TermsService, + EventService, ProfileService, PushRuleService, PushersService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt index f5d2a7df3e..5ebeaad3de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt @@ -17,9 +17,7 @@ package org.matrix.android.sdk.api.session.accountdata import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional interface AccountDataService { @@ -48,5 +46,5 @@ interface AccountDataService { /** * Update the account data with the provided type and the provided account data content */ - fun updateAccountData(type: String, content: Content, callback: MatrixCallback? = null): Cancelable + suspend fun updateAccountData(type: String, content: Content) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt index 2413786ea9..54a1e896ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.crypto.verification -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.LocalEcho @@ -79,8 +78,7 @@ interface VerificationService { transactionId: String, roomId: String, otherUserId: String, - otherDeviceId: String, - callback: MatrixCallback?): String? + otherDeviceId: String): String /** * Returns false if the request is unknown diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt old mode 100755 new mode 100644 similarity index 56% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt index fe6b3a74bb..297f277497 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,16 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto.model +package org.matrix.android.sdk.api.session.events -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event -class MXQueuedEncryption { +interface EventService { /** - * The data to encrypt. + * Ask the homeserver for an event content. The SDK will try to decrypt it if it is possible + * The result will not be stored into cache */ - var eventContent: Content? = null - var eventType: String? = null - - /** - * the asynchronous callback - */ - var apiCallback: MatrixCallback? = null + suspend fun getEvent(roomId: String, + eventId: String): Event } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 844e8dbbab..89b873febb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -289,3 +289,7 @@ fun Event.getRelationContent(): RelationDefaultContent? { fun Event.isReply(): Boolean { return getRelationContent()?.inReplyTo?.eventId != null } + +fun Event.isEdition(): Boolean { + return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index bcdb5ea257..adfdc2498e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -17,11 +17,9 @@ package org.matrix.android.sdk.api.session.file import android.net.Uri -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import java.io.File @@ -41,20 +39,17 @@ interface FileService { * Download a file. * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. */ - fun downloadFile(fileName: String, + suspend fun downloadFile(fileName: String, mimeType: String?, url: String?, - elementToDecrypt: ElementToDecrypt?, - callback: MatrixCallback): Cancelable + elementToDecrypt: ElementToDecrypt?): File - fun downloadFile(messageContent: MessageWithAttachmentContent, - callback: MatrixCallback): Cancelable = + suspend fun downloadFile(messageContent: MessageWithAttachmentContent): File = downloadFile( fileName = messageContent.getFileName(), mimeType = messageContent.mimeType, url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), - callback = callback + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() ) fun isFileInCache(mxcUrl: String?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt index aedb813735..8f8967e8fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.identity -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * Provides access to the identity server configuration and services identity server can provide */ @@ -40,55 +37,55 @@ interface IdentityService { * See https://matrix.org/docs/spec/identity_service/latest#status-check * RiotX SDK only supports identity server API v2 */ - fun isValidIdentityServer(url: String, callback: MatrixCallback): Cancelable + suspend fun isValidIdentityServer(url: String) /** * Update the identity server url. * If successful, any previous identity server will be disconnected. * In case of error, any previous identity server will remain configured. * @param url the new url. - * @param callback will notify the user if change is successful. The String will be the final url of the identity server. + * @return The String will be the final url of the identity server. * The SDK can prepend "https://" for instance. */ - fun setNewIdentityServer(url: String, callback: MatrixCallback): Cancelable + suspend fun setNewIdentityServer(url: String): String /** * Disconnect (logout) from the current identity server */ - fun disconnect(callback: MatrixCallback): Cancelable + suspend fun disconnect() /** * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid */ - fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + suspend fun startBindThreePid(threePid: ThreePid) /** * This will cancel a pending binding of threePid. */ - fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + suspend fun cancelBindThreePid(threePid: ThreePid) /** * This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid */ - fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable + suspend fun sendAgainValidationCode(threePid: ThreePid) /** * Submit the code that the identity server has sent to the user (in email or SMS) * Once successful, you will have to call [finalizeBindThreePid] * @param code the code sent to the user */ - fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable + suspend fun submitValidationToken(threePid: ThreePid, code: String) /** * This will perform the actual association of ThreePid and Matrix account */ - fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + suspend fun finalizeBindThreePid(threePid: ThreePid) /** * Unbind a threePid * The request will actually be done on the homeserver */ - fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + suspend fun unbindThreePid(threePid: ThreePid) /** * Search MatrixId of users providing email and phone numbers @@ -96,7 +93,7 @@ interface IdentityService { * Application has to explicitly ask for the user consent, and the answer can be stored using [setUserConsent] * Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details. */ - fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable + suspend fun lookUp(threePids: List): List /** * Return the current user consent for the current identity server, which has been stored using [setUserConsent]. @@ -120,9 +117,9 @@ interface IdentityService { * A lookup will be performed, but also pending binding state will be restored * * @param threePids the list of threePid the user owns (retrieved form the homeserver) - * @param callback onSuccess will be called with a map of ThreePid -> SharedState + * @return a map of ThreePid -> SharedState */ - fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable + suspend fun getShareStatus(threePids: List): Map fun addListener(listener: IdentityServiceListener) fun removeListener(listener: IdentityServiceListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index a4d5b665c6..e493adeaf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -19,10 +19,8 @@ package org.matrix.android.sdk.api.session.profile import android.net.Uri import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -41,14 +39,14 @@ interface ProfileService { * @param userId the userId param to look for * */ - fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable + suspend fun getDisplayName(userId: String): Optional /** * Update the display name for this user * @param userId the userId to update the display name of * @param newDisplayName the new display name of the user */ - fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback): Cancelable + suspend fun setDisplayName(userId: String, newDisplayName: String) /** * Update the avatar for this user @@ -56,14 +54,14 @@ interface ProfileService { * @param newAvatarUri the new avatar uri of the user * @param fileName the fileName of selected image */ - fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable + suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) /** * Return the current avatarUrl for this user. * @param userId the userId param to look for * */ - fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable + suspend fun getAvatarUrl(userId: String): Optional /** * Get the combined profile information for this user. @@ -71,7 +69,7 @@ interface ProfileService { * @param userId the userId param to look for * */ - fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable + suspend fun getProfile(userId: String): JsonDict /** * Get the current user 3Pids @@ -97,28 +95,26 @@ interface ProfileService { /** * Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list. */ - fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable + suspend fun addThreePid(threePid: ThreePid) /** * Validate a code received by text message */ - fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback): Cancelable + suspend fun submitSmsCode(threePid: ThreePid.Msisdn, code: String) /** * Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid */ - fun finalizeAddingThreePid(threePid: ThreePid, - userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, - matrixCallback: MatrixCallback): Cancelable + suspend fun finalizeAddingThreePid(threePid: ThreePid, + userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) /** * Cancel adding a threepid. It will remove locally stored data about this ThreePid */ - fun cancelAddingThreePid(threePid: ThreePid, - matrixCallback: MatrixCallback): Cancelable + suspend fun cancelAddingThreePid(threePid: ThreePid) /** * Remove a 3Pid from the Matrix account. */ - fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable + suspend fun deleteThreePid(threePid: ThreePid) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index 3993422b1d..9ea820f5b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -16,8 +16,6 @@ package org.matrix.android.sdk.api.session.pushers import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable import java.util.UUID interface PushersService { @@ -75,16 +73,15 @@ interface PushersService { * @param callback callback to know if the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. * In case of error, PusherRejected failure can happen. In this case it means that the pushkey is not valid. */ - fun testPush(url: String, - appId: String, - pushkey: String, - eventId: String, - callback: MatrixCallback): Cancelable + suspend fun testPush(url: String, + appId: String, + pushkey: String, + eventId: String) /** * Remove the http pusher */ - fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback): Cancelable + suspend fun removeHttpPusher(pushkey: String, appId: String) /** * Get the current pushers, as a LiveData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index cb6690b5c5..257c83564e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService @@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional /** @@ -86,12 +84,11 @@ interface Room : * @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned. * @param callback Callback to get the search result */ - fun search(searchTerm: String, + suspend fun search(searchTerm: String, nextBatch: String?, orderByRecent: Boolean, limit: Int, beforeLimit: Int, afterLimit: Int, - includeProfile: Boolean, - callback: MatrixCallback): Cancelable + includeProfile: Boolean): SearchResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 5f02b77a1e..8c833644ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState @@ -24,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription @@ -178,4 +180,29 @@ interface RoomService { * This call will try to gather some information on this room, but it could fail and get nothing more */ fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) + + /** + * TODO Doc + */ + fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config = defaultPagedListConfig): LiveData> + + /** + * TODO Doc + */ + fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config = defaultPagedListConfig): UpdatableFilterLivePageResult + + /** + * TODO Doc + */ + fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount + + private val defaultPagedListConfig + get() = PagedList.Config.Builder() + .setPageSize(10) + .setInitialLoadSizeHint(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(10) + .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index f859d74a6f..7e04ebb5f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.api.session.room import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.session.room.model.Membership fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { @@ -31,7 +33,9 @@ data class RoomSummaryQueryParams( val roomId: QueryStringValue, val displayName: QueryStringValue, val canonicalAlias: QueryStringValue, - val memberships: List + val memberships: List, + val roomCategoryFilter: RoomCategoryFilter?, + val roomTagQueryFilter: RoomTagQueryFilter? ) { class Builder { @@ -40,12 +44,16 @@ data class RoomSummaryQueryParams( var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var memberships: List = Membership.all() + var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL + var roomTagQueryFilter: RoomTagQueryFilter? = null fun build() = RoomSummaryQueryParams( roomId = roomId, displayName = displayName, canonicalAlias = canonicalAlias, - memberships = memberships + memberships = memberships, + roomCategoryFilter = roomCategoryFilter, + roomTagQueryFilter = roomTagQueryFilter ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeRoomListDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt similarity index 60% rename from vector/src/main/java/im/vector/app/features/home/HomeRoomListDataSource.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt index 6bcd6f01eb..71b3c665e7 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeRoomListDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,12 +14,14 @@ * limitations under the License. */ -package im.vector.app.features.home +package org.matrix.android.sdk.api.session.room -import im.vector.app.core.utils.BehaviorDataSource +import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.session.room.model.RoomSummary -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class HomeRoomListDataSource @Inject constructor() : BehaviorDataSource>() +interface UpdatableFilterLivePageResult { + val livePagedList: LiveData> + + fun updateQuery(queryParams: RoomSummaryQueryParams) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt index 2c3ffac687..198d6677a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt @@ -17,10 +17,8 @@ package org.matrix.android.sdk.api.session.room.members import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.util.Cancelable /** * This interface defines methods to handling membership. It's implemented at the room level. @@ -29,9 +27,8 @@ interface MembershipService { /** * This methods load all room members if it was done yet. - * @return a [Cancelable] */ - fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable + suspend fun loadRoomMembersIfNeeded() /** * Return the roomMember with userId or null. @@ -60,47 +57,35 @@ interface MembershipService { /** * Invite a user in the room */ - fun invite(userId: String, - reason: String? = null, - callback: MatrixCallback): Cancelable + suspend fun invite(userId: String, reason: String? = null) /** * Invite a user with email or phone number in the room */ - fun invite3pid(threePid: ThreePid, - callback: MatrixCallback): Cancelable + suspend fun invite3pid(threePid: ThreePid) /** * Ban a user from the room */ - fun ban(userId: String, - reason: String? = null, - callback: MatrixCallback): Cancelable + suspend fun ban(userId: String, reason: String? = null) /** * Unban a user from the room */ - fun unban(userId: String, - reason: String? = null, - callback: MatrixCallback): Cancelable + suspend fun unban(userId: String, reason: String? = null) /** * Kick a user from the room */ - fun kick(userId: String, - reason: String? = null, - callback: MatrixCallback): Cancelable + suspend fun kick(userId: String, reason: String? = null) /** * Join the room, or accept an invitation. */ - fun join(reason: String? = null, - viaServers: List = emptyList(), - callback: MatrixCallback): Cancelable + suspend fun join(reason: String? = null, viaServers: List = emptyList()) /** * Leave the room, or reject an invitation. */ - fun leave(reason: String? = null, - callback: MatrixCallback): Cancelable + suspend fun leave(reason: String? = null) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt index 5844aead8d..a5d0f63722 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt @@ -23,24 +23,13 @@ import com.squareup.moshi.JsonClass * Represents the membership of a user on a room */ @JsonClass(generateAdapter = false) -enum class Membership(val value: String) { - - NONE("none"), - - @Json(name = "invite") - INVITE("invite"), - - @Json(name = "join") - JOIN("join"), - - @Json(name = "knock") - KNOCK("knock"), - - @Json(name = "leave") - LEAVE("leave"), - - @Json(name = "ban") - BAN("ban"); +enum class Membership { + NONE, + @Json(name = "invite") INVITE, + @Json(name = "join") JOIN, + @Json(name = "knock") KNOCK, + @Json(name = "leave") LEAVE, + @Json(name = "ban") BAN; fun isLeft(): Boolean { return this == KNOCK || this == LEAVE || this == BAN diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt index 99b035d30e..0760c6f1b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import timber.log.Timber /** * Class representing the EventType.STATE_ROOM_GUEST_ACCESS state event content @@ -26,14 +27,20 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class RoomGuestAccessContent( // Required. Whether guests can join the room. One of: ["can_join", "forbidden"] - @Json(name = "guest_access") val guestAccess: GuestAccess? = null -) + @Json(name = "guest_access") val _guestAccess: String? = null +) { + val guestAccess: GuestAccess? = when (_guestAccess) { + "can_join" -> GuestAccess.CanJoin + "forbidden" -> GuestAccess.Forbidden + else -> { + Timber.w("Invalid value for GuestAccess: `$_guestAccess`") + null + } + } +} @JsonClass(generateAdapter = false) -enum class GuestAccess(val value: String) { - @Json(name = "can_join") - CanJoin("can_join"), - - @Json(name = "forbidden") - Forbidden("forbidden") +enum class GuestAccess { + @Json(name = "can_join") CanJoin, + @Json(name = "forbidden") Forbidden } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt index 31493be7ea..3ac14e48de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt @@ -18,8 +18,20 @@ package org.matrix.android.sdk.api.session.room.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import timber.log.Timber @JsonClass(generateAdapter = true) data class RoomHistoryVisibilityContent( - @Json(name = "history_visibility") val historyVisibility: RoomHistoryVisibility? = null -) + @Json(name = "history_visibility") val _historyVisibility: String? = null +) { + val historyVisibility: RoomHistoryVisibility? = when (_historyVisibility) { + "world_readable" -> RoomHistoryVisibility.WORLD_READABLE + "shared" -> RoomHistoryVisibility.SHARED + "invited" -> RoomHistoryVisibility.INVITED + "joined" -> RoomHistoryVisibility.JOINED + else -> { + Timber.w("Invalid value for RoomHistoryVisibility: `$_historyVisibility`") + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt index 09aacfabbe..f3e8d357f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt @@ -24,17 +24,9 @@ import com.squareup.moshi.JsonClass * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules */ @JsonClass(generateAdapter = false) -enum class RoomJoinRules(val value: String) { - - @Json(name = "public") - PUBLIC("public"), - - @Json(name = "invite") - INVITE("invite"), - - @Json(name = "knock") - KNOCK("knock"), - - @Json(name = "private") - PRIVATE("private") +enum class RoomJoinRules { + @Json(name = "public") PUBLIC, + @Json(name = "invite") INVITE, + @Json(name = "knock") KNOCK, + @Json(name = "private") PRIVATE } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt index 3be2d38be7..8082486b22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt @@ -19,11 +19,23 @@ package org.matrix.android.sdk.api.session.room.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import timber.log.Timber /** * Class representing the EventType.STATE_ROOM_JOIN_RULES state event content */ @JsonClass(generateAdapter = true) data class RoomJoinRulesContent( - @Json(name = "join_rule") val joinRules: RoomJoinRules? = null -) + @Json(name = "join_rule") val _joinRules: String? = null +) { + val joinRules: RoomJoinRules? = when (_joinRules) { + "public" -> RoomJoinRules.PUBLIC + "invite" -> RoomJoinRules.INVITE + "knock" -> RoomJoinRules.KNOCK + "private" -> RoomJoinRules.PRIVATE + else -> { + Timber.w("Invalid value for RoomJoinRules: `$_joinRules`") + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index a2b4e135d1..c96a800ee5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -35,5 +35,5 @@ object MessageType { const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" const val MSGTYPE_CONFETTI = "nic.custom.confetti" - const val MSGTYPE_SNOW = "nic.custom.snow" + const val MSGTYPE_SNOW = "io.element.effect.snowfall" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt index 4f44c9a912..b037a3f366 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.api.session.room.read import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.util.Optional @@ -35,17 +34,17 @@ interface ReadService { /** * Force the read marker to be set on the latest event. */ - fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, callback: MatrixCallback) + suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH) /** * Set the read receipt on the event with provided eventId. */ - fun setReadReceipt(eventId: String, callback: MatrixCallback) + suspend fun setReadReceipt(eventId: String) /** * Set the read marker on the event with provided eventId. */ - fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + suspend fun setReadMarker(fullyReadEventId: String) /** * Check if an event is already read, ie. your read receipt is set on a more recent event. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt new file mode 100644 index 0000000000..066178b1ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.summary + +data class RoomAggregateNotificationCount( + val notificationCount: Int, + val highlightCount: Int +) { + val totalCount = notificationCount + highlightCount + val isHighlight = highlightCount > 0 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 8932d0734e..06c88db831 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -95,12 +95,6 @@ interface Timeline { */ fun getTimelineEventWithId(eventId: String?): TimelineEvent? - /** - * Returns the first displayable events starting from eventId. - * It does depend on the provided [TimelineSettings]. - */ - fun getFirstDisplayableEventId(eventId: String): String? - interface Listener { /** * Call when the timeline has been updated through pagination or sync. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt index 25c63d6fbc..ceffedb234 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt @@ -24,10 +24,6 @@ data class TimelineSettings( * The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet. */ val initialSize: Int, - /** - * Filters for timeline event - */ - val filters: TimelineEventFilters = TimelineEventFilters(), /** * If true, will build read receipts for each event. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt index 37ecf99f9a..721a2bc8af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.securestorage -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -43,13 +42,12 @@ interface SharedSecretStorageService { * @param keyName a human readable name * @param keySigner Used to add a signature to the key (client should check key signature before storing secret) * - * @param callback Get key creation info + * @return key creation info */ - fun generateKey(keyId: String, - key: SsssKeySpec?, - keyName: String, - keySigner: KeySigner?, - callback: MatrixCallback) + suspend fun generateKey(keyId: String, + key: SsssKeySpec?, + keyName: String, + keySigner: KeySigner?): SsssKeyCreationInfo /** * Generates a SSSS key using the given passphrase. @@ -61,14 +59,13 @@ interface SharedSecretStorageService { * @param keySigner Used to add a signature to the key (client should check key signature before retrieving secret) * @param progressListener The derivation of the passphrase may take long depending on the device, use this to report progress * - * @param callback Get key creation info + * @return key creation info */ - fun generateKeyWithPassphrase(keyId: String, - keyName: String, - passphrase: String, - keySigner: KeySigner, - progressListener: ProgressListener?, - callback: MatrixCallback) + suspend fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?): SsssKeyCreationInfo fun getKey(keyId: String): KeyInfoResult @@ -80,7 +77,7 @@ interface SharedSecretStorageService { */ fun getDefaultKey(): KeyInfoResult - fun setDefaultKey(keyId: String, callback: MatrixCallback) + suspend fun setDefaultKey(keyId: String) /** * Check whether we have a key with a given ID. @@ -98,7 +95,7 @@ interface SharedSecretStorageService { * @param secret The secret contents. * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. */ - fun storeSecret(name: String, secretBase64: String, keys: List, callback: MatrixCallback) + suspend fun storeSecret(name: String, secretBase64: String, keys: List) /** * Use this call to determine which SSSSKeySpec to use for requesting secret @@ -113,7 +110,7 @@ interface SharedSecretStorageService { * @param secretKey the secret key to use (@see #RawBytesKeySpec) * */ - fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback) + suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String /** * Return true if SSSS is configured diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt index ab85f979bf..cd4fb216d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt @@ -18,9 +18,7 @@ package org.matrix.android.sdk.api.session.user import androidx.lifecycle.LiveData import androidx.paging.PagedList -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.user.model.User -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional /** @@ -38,17 +36,16 @@ interface UserService { /** * Try to resolve user from known users, or using profile api */ - fun resolveUser(userId: String, callback: MatrixCallback) + suspend fun resolveUser(userId: String): User /** * Search list of users on server directory. * @param search the searched term * @param limit the max number of users to return * @param excludedUserIds the user ids to filter from the search - * @param callback the async callback * @return Cancelable */ - fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set, callback: MatrixCallback>): Cancelable + suspend fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set): List /** * Observe a live user from a userId @@ -79,10 +76,10 @@ interface UserService { /** * Ignore users */ - fun ignoreUserIds(userIds: List, callback: MatrixCallback): Cancelable + suspend fun ignoreUserIds(userIds: List) /** * Un-ignore some users */ - fun unIgnoreUserIds(userIds: List, callback: MatrixCallback): Cancelable + suspend fun unIgnoreUserIds(userIds: List) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt index bf3ff8959d..8f35ff0e4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt @@ -17,10 +17,8 @@ package org.matrix.android.sdk.api.session.widgets import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.session.widgets.model.Widget /** @@ -107,20 +105,16 @@ interface WidgetService { * @param roomId the room where you want to create the widget. * @param widgetId the widget to create. * @param content the content of the widget - * @param callback the matrix callback to listen for result. - * @return Cancelable */ - fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback): Cancelable + suspend fun createRoomWidget(roomId: String, widgetId: String, content: Content): Widget /** * Deactivate a widget in a room. It makes sure you have the rights to handle this. * * @param roomId: the room where you want to deactivate the widget. * @param widgetId: the widget to deactivate. - * @param callback the matrix callback to listen for result. - * @return Cancelable */ - fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback): Cancelable + suspend fun destroyRoomWidget(roomId: String, widgetId: String) /** * Returns true if you can add/remove widgets. It goes through diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt index f92ae7e0ee..f93f285c6e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.auth.data.Availability import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams import org.matrix.android.sdk.internal.auth.data.RiotConfig @@ -29,12 +30,12 @@ import org.matrix.android.sdk.internal.auth.registration.SuccessResult import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query import retrofit2.http.Url /** @@ -45,26 +46,32 @@ internal interface AuthAPI { * Get a Riot config file, using the name including the domain */ @GET("config.{domain}.json") - fun getRiotConfigDomain(@Path("domain") domain: String): Call + suspend fun getRiotConfigDomain(@Path("domain") domain: String): RiotConfig /** * Get a Riot config file */ @GET("config.json") - fun getRiotConfig(): Call + suspend fun getRiotConfig(): RiotConfig /** * Get the version information of the homeserver */ @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") - fun versions(): Call + suspend fun versions(): Versions /** * Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") - fun register(@Body registrationParams: RegistrationParams): Call + suspend fun register(@Body registrationParams: RegistrationParams): Credentials + + /** + * Checks to see if a username is available, and valid, for the server. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/available") + suspend fun registerAvailable(@Query("username") username: String): Availability /** * Add 3Pid during registration @@ -72,22 +79,22 @@ internal interface AuthAPI { * https://github.com/matrix-org/matrix-doc/pull/2290 */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken") - fun add3Pid(@Path("threePid") threePid: String, - @Body params: AddThreePidRegistrationParams): Call + suspend fun add3Pid(@Path("threePid") threePid: String, + @Body params: AddThreePidRegistrationParams): AddThreePidRegistrationResponse /** * Validate 3pid */ @POST - fun validate3Pid(@Url url: String, - @Body params: ValidationCodeBody): Call + suspend fun validate3Pid(@Url url: String, + @Body params: ValidationCodeBody): SuccessResult /** * Get the supported login flow * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") - fun getLoginFlows(): Call + suspend fun getLoginFlows(): LoginFlowResponse /** * Pass params to the server for the current login phase. @@ -97,22 +104,22 @@ internal interface AuthAPI { */ @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") - fun login(@Body loginParams: PasswordLoginParams): Call + suspend fun login(@Body loginParams: PasswordLoginParams): Credentials // Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") - fun login(@Body loginParams: TokenLoginParams): Call + suspend fun login(@Body loginParams: TokenLoginParams): Credentials /** * Ask the homeserver to reset the password associated with the provided email. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken") - fun resetPassword(@Body params: AddThreePidRegistrationParams): Call + suspend fun resetPassword(@Body params: AddThreePidRegistrationParams): AddThreePidRegistrationResponse /** * Ask the homeserver to reset the password with the provided new password once the email is validated. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") - fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call + suspend fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 4f3451cf30..e26286ad2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.RiotConfig import org.matrix.android.sdk.internal.auth.db.PendingSessionData import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard @@ -172,8 +171,8 @@ internal class DefaultAuthenticationService @Inject constructor( // First check the homeserver version return runCatching { - executeRequest(null) { - apiCall = authAPI.versions() + executeRequest(null) { + authAPI.versions() } } .map { versions -> @@ -204,8 +203,8 @@ internal class DefaultAuthenticationService @Inject constructor( // Ok, try to get the config.domain.json file of a RiotWeb client return runCatching { - executeRequest(null) { - apiCall = authAPI.getRiotConfigDomain(domain) + executeRequest(null) { + authAPI.getRiotConfigDomain(domain) } } .map { riotConfig -> @@ -232,8 +231,8 @@ internal class DefaultAuthenticationService @Inject constructor( // Ok, try to get the config.json file of a RiotWeb client return runCatching { - executeRequest(null) { - apiCall = authAPI.getRiotConfig() + executeRequest(null) { + authAPI.getRiotConfig() } } .map { riotConfig -> @@ -265,8 +264,8 @@ internal class DefaultAuthenticationService @Inject constructor( val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - val versions = executeRequest(null) { - apiCall = newAuthAPI.versions() + val versions = executeRequest(null) { + newAuthAPI.versions() } return getLoginFlowResult(newAuthAPI, versions, defaultHomeServerUrl) @@ -293,8 +292,8 @@ internal class DefaultAuthenticationService @Inject constructor( val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - val versions = executeRequest(null) { - apiCall = newAuthAPI.versions() + val versions = executeRequest(null) { + newAuthAPI.versions() } getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) @@ -305,8 +304,8 @@ internal class DefaultAuthenticationService @Inject constructor( private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { // Get the login flow - val loginFlowResponse = executeRequest(null) { - apiCall = authAPI.getLoginFlows() + val loginFlowResponse = executeRequest(null) { + authAPI.getLoginFlows() } return LoginFlowResult.Success( loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt index b8416d69bf..867cf46b8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt @@ -20,7 +20,6 @@ import dagger.Lazy import okhttp3.OkHttpClient import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.di.Unauthenticated import org.matrix.android.sdk.internal.network.RetrofitFactory import org.matrix.android.sdk.internal.network.executeRequest @@ -49,8 +48,8 @@ internal class DefaultIsValidClientServerApiTask @Inject constructor( .create(AuthAPI::class.java) return try { - executeRequest(null) { - apiCall = authAPI.getLoginFlows() + executeRequest(null) { + authAPI.getLoginFlows() } // We get a response, so the API is valid true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/Availability.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/Availability.kt new file mode 100644 index 0000000000..5ef3c0d06a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/Availability.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class Availability( + /** + * A flag to indicate that the username is available. This should always be true when the server replies with 200 OK. + */ + @Json(name = "available") + val available: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 4167875849..8b81f42e03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.auth.login import android.util.Patterns -import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.session.Session @@ -29,7 +28,6 @@ import org.matrix.android.sdk.internal.auth.data.ThreePidMedium import org.matrix.android.sdk.internal.auth.data.TokenLoginParams import org.matrix.android.sdk.internal.auth.db.PendingSessionData import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams -import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask import org.matrix.android.sdk.internal.network.executeRequest @@ -49,8 +47,8 @@ internal class DefaultLoginWizard( } else { PasswordLoginParams.userIdentifier(login, password, deviceName) } - val credentials = executeRequest(null) { - apiCall = authAPI.login(loginParams) + val credentials = executeRequest(null) { + authAPI.login(loginParams) } return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) @@ -63,8 +61,8 @@ internal class DefaultLoginWizard( val loginParams = TokenLoginParams( token = loginToken ) - val credentials = executeRequest(null) { - apiCall = authAPI.login(loginParams) + val credentials = executeRequest(null) { + authAPI.login(loginParams) } return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) @@ -80,8 +78,8 @@ internal class DefaultLoginWizard( pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) .also { pendingSessionStore.savePendingSessionData(it) } - val result = executeRequest(null) { - apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param)) + val result = executeRequest(null) { + authAPI.resetPassword(AddThreePidRegistrationParams.from(param)) } pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result)) @@ -98,8 +96,8 @@ internal class DefaultLoginWizard( safeResetPasswordData.newPassword ) - executeRequest(null) { - apiCall = authAPI.resetPasswordMailConfirmed(param) + executeRequest(null) { + authAPI.resetPasswordMailConfirmed(param) } // Set to null? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt index be6ff38931..77bbb8096f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.auth.login import dagger.Lazy -import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session @@ -59,19 +58,16 @@ internal class DefaultDirectLoginTask @Inject constructor( val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) val credentials = try { - executeRequest(null) { - apiCall = authAPI.login(loginParams) + executeRequest(null) { + authAPI.login(loginParams) } } catch (throwable: Throwable) { - when (throwable) { - is UnrecognizedCertificateException -> { - throw Failure.UnrecognizedCertificateFailure( - homeServerUrl, - throwable.fingerprint - ) - } - else -> - throw throwable + throw when (throwable) { + is UnrecognizedCertificateException -> Failure.UnrecognizedCertificateFailure( + homeServerUrl, + throwable.fingerprint + ) + else -> throwable } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 91e414e689..4a3d53a8fc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.auth.registration import kotlinx.coroutines.delay import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.toFlowResult @@ -40,9 +41,10 @@ internal class DefaultRegistrationWizard( private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") - private val registerTask = DefaultRegisterTask(authAPI) - private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI) - private val validateCodeTask = DefaultValidateCodeTask(authAPI) + private val registerTask: RegisterTask = DefaultRegisterTask(authAPI) + private val registerAvailableTask: RegisterAvailableTask = DefaultRegisterAvailableTask(authAPI) + private val registerAddThreePidTask: RegisterAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI) + private val validateCodeTask: ValidateCodeTask = DefaultValidateCodeTask(authAPI) override val currentThreePid: String? get() { @@ -203,4 +205,8 @@ internal class DefaultRegistrationWizard( val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) return RegistrationResult.Success(session) } + + override suspend fun registrationAvailable(userName: String): RegistrationAvailability { + return registerAvailableTask.execute(RegisterAvailableTask.Params(userName)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt index 57c4b72b8a..54a8ba0e6c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt @@ -35,7 +35,7 @@ internal class DefaultRegisterAddThreePidTask( override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse { return executeRequest(null) { - apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params)) + authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAvailableTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAvailableTask.kt new file mode 100644 index 0000000000..314a24dad4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAvailableTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.isRegistrationAvailabilityError +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface RegisterAvailableTask : Task { + data class Params( + val userName: String + ) +} + +internal class DefaultRegisterAvailableTask(private val authAPI: AuthAPI) : RegisterAvailableTask { + override suspend fun execute(params: RegisterAvailableTask.Params): RegistrationAvailability { + return try { + executeRequest(null) { + authAPI.registerAvailable(params.userName) + } + RegistrationAvailability.Available + } catch (exception: Throwable) { + if (exception.isRegistrationAvailabilityError()) { + RegistrationAvailability.NotAvailable(exception as Failure.ServerError) + } else { + throw exception + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt index bf5d899276..45668cb8ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt @@ -36,7 +36,7 @@ internal class DefaultRegisterTask( override suspend fun execute(params: RegisterTask.Params): Credentials { try { return executeRequest(null) { - apiCall = authAPI.register(params.registrationParams) + authAPI.register(params.registrationParams) } } catch (throwable: Throwable) { throw throwable.toRegistrationFlowResponse() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt index b297c9849d..d68b7cd9eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt @@ -33,7 +33,7 @@ internal class DefaultValidateCodeTask( override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult { return executeRequest(null) { - apiCall = authAPI.validate3Pid(params.url, params.body) + authAPI.validate3Pid(params.url, params.body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 6b91c0b859..697711d051 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -67,8 +67,9 @@ internal class MXMegolmEncryption( init { // restore existing outbound session if any - outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId) + outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId) } + // Default rotation periods // TODO: Make it configurable via parameters // Session rotation periods @@ -125,6 +126,7 @@ internal class MXMegolmEncryption( Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") } + /** * Prepare a new session. * @@ -240,6 +242,7 @@ internal class MXMegolmEncryption( val contentMap = MXUsersDevicesMap() var haveTargets = false val userIds = results.userIds + val noOlmToNotify = mutableListOf() for (userId in userIds) { val devicesToShareWith = devicesByUser[userId] for ((deviceID) in devicesToShareWith!!) { @@ -251,13 +254,7 @@ internal class MXMegolmEncryption( // MSC 2399 // send withheld m.no_olm: an olm session could not be established. // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. - notifyKeyWithHeld( - listOf(UserDevice(userId, deviceID)), - session.sessionId, - olmDevice.deviceCurve25519Key, - WithHeldCode.NO_OLM - ) - + noOlmToNotify.add(UserDevice(userId, deviceID)) continue } Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") @@ -277,14 +274,14 @@ internal class MXMegolmEncryption( session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) gossipingEventBuffer.add( Event( - type = EventType.ROOM_KEY, - senderId = this.userId, - content = submap.apply { - this["session_key"] = "" - // we add a fake key for trail - this["_dest"] = "$userId|$deviceId" - } - )) + type = EventType.ROOM_KEY, + senderId = this.userId, + content = submap.apply { + this["session_key"] = "" + // we add a fake key for trail + this["_dest"] = "$userId|$deviceId" + } + )) } } @@ -304,6 +301,16 @@ internal class MXMegolmEncryption( } else { Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") } + + if (noOlmToNotify.isNotEmpty()) { + // XXX offload?, as they won't read the message anyhow? + notifyKeyWithHeld( + noOlmToNotify, + session.sessionId, + olmDevice.deviceCurve25519Key, + WithHeldCode.NO_OLM + ) + } } private suspend fun notifyKeyWithHeld(targets: List, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt index 5604e97152..cef86e8b5e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.HTTP @@ -46,14 +45,14 @@ internal interface CryptoApi { * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") - fun getDevices(): Call + suspend fun getDevices(): DevicesListResponse /** * Get the device info by id * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}") - fun getDeviceInfo(@Path("deviceId") deviceId: String): Call + suspend fun getDeviceInfo(@Path("deviceId") deviceId: String): DeviceInfo /** * Upload device and/or one-time keys. @@ -62,7 +61,7 @@ internal interface CryptoApi { * @param body the keys to be sent. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload") - fun uploadKeys(@Body body: KeysUploadBody): Call + suspend fun uploadKeys(@Body body: KeysUploadBody): KeysUploadResponse /** * Download device keys. @@ -71,7 +70,7 @@ internal interface CryptoApi { * @param params the params. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query") - fun downloadKeysForUsers(@Body params: KeysQueryBody): Call + suspend fun downloadKeysForUsers(@Body params: KeysQueryBody): KeysQueryResponse /** * CrossSigning - Uploading signing keys @@ -79,7 +78,7 @@ internal interface CryptoApi { * This endpoint requires UI Auth. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload") - fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call + suspend fun uploadSigningKeys(@Body params: UploadSigningKeysBody): KeysQueryResponse /** * CrossSigning - Uploading signatures @@ -98,7 +97,7 @@ internal interface CryptoApi { * However, signatures made for other users' keys, made by her user-signing key, will not be included. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload") - fun uploadSignatures(@Body params: Map?): Call + suspend fun uploadSignatures(@Body params: Map?): SignatureUploadResponse /** * Claim one-time keys. @@ -107,7 +106,7 @@ internal interface CryptoApi { * @param params the params. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim") - fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): Call + suspend fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): KeysClaimResponse /** * Send an event to a specific list of devices @@ -118,9 +117,9 @@ internal interface CryptoApi { * @param body the body */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}") - fun sendToDevice(@Path("eventType") eventType: String, - @Path("txnId") transactionId: String, - @Body body: SendToDeviceBody): Call + suspend fun sendToDevice(@Path("eventType") eventType: String, + @Path("txnId") transactionId: String, + @Body body: SendToDeviceBody) /** * Delete a device. @@ -130,8 +129,8 @@ internal interface CryptoApi { * @param params the deletion parameters */ @HTTP(path = NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", method = "DELETE", hasBody = true) - fun deleteDevice(@Path("device_id") deviceId: String, - @Body params: DeleteDeviceParams): Call + suspend fun deleteDevice(@Path("device_id") deviceId: String, + @Body params: DeleteDeviceParams) /** * Update the device information. @@ -141,8 +140,8 @@ internal interface CryptoApi { * @param params the params */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}") - fun updateDeviceInfo(@Path("device_id") deviceId: String, - @Body params: UpdateDeviceInfoBody): Call + suspend fun updateDeviceInfo(@Path("device_id") deviceId: String, + @Body params: UpdateDeviceInfoBody) /** * Get the update devices list from two sync token. @@ -152,6 +151,6 @@ internal interface CryptoApi { * @param newToken the up-to token. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes") - fun getKeyChanges(@Query("from") oldToken: String, - @Query("to") newToken: String): Call + suspend fun getKeyChanges(@Query("from") oldToken: String, + @Query("to") newToken: String): KeyChangesResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt index 3f8333528f..eb4c55a3e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -48,14 +47,14 @@ internal interface RoomKeysApi { * @param createKeysBackupVersionBody the body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version") - fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): Call + suspend fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): KeysVersion /** * Get the key backup last version * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version") - fun getKeysBackupLastVersion(): Call + suspend fun getKeysBackupLastVersion(): KeysVersionResult /** * Get information about the given version. @@ -64,7 +63,7 @@ internal interface RoomKeysApi { * @param version version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") - fun getKeysBackupVersion(@Path("version") version: String): Call + suspend fun getKeysBackupVersion(@Path("version") version: String): KeysVersionResult /** * Update information about the given version. @@ -72,8 +71,8 @@ internal interface RoomKeysApi { * @param updateKeysBackupVersionBody the body */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") - fun updateKeysBackupVersion(@Path("version") version: String, - @Body keysBackupVersionBody: UpdateKeysBackupVersionBody): Call + suspend fun updateKeysBackupVersion(@Path("version") version: String, + @Body keysBackupVersionBody: UpdateKeysBackupVersionBody) /* ========================================================================================== * Storing keys @@ -94,10 +93,10 @@ internal interface RoomKeysApi { * @param keyBackupData the data to send */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") - fun storeRoomSessionData(@Path("roomId") roomId: String, - @Path("sessionId") sessionId: String, - @Query("version") version: String, - @Body keyBackupData: KeyBackupData): Call + suspend fun storeRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String, + @Body keyBackupData: KeyBackupData): BackupKeysResult /** * Store several keys for the given room, using the given backup version. @@ -107,9 +106,9 @@ internal interface RoomKeysApi { * @param roomKeysBackupData the data to send */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") - fun storeRoomSessionsData(@Path("roomId") roomId: String, - @Query("version") version: String, - @Body roomKeysBackupData: RoomKeysBackupData): Call + suspend fun storeRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String, + @Body roomKeysBackupData: RoomKeysBackupData): BackupKeysResult /** * Store several keys, using the given backup version. @@ -118,8 +117,8 @@ internal interface RoomKeysApi { * @param keysBackupData the data to send */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") - fun storeSessionsData(@Query("version") version: String, - @Body keysBackupData: KeysBackupData): Call + suspend fun storeSessionsData(@Query("version") version: String, + @Body keysBackupData: KeysBackupData): BackupKeysResult /* ========================================================================================== * Retrieving keys @@ -133,9 +132,9 @@ internal interface RoomKeysApi { * @param version the version of the backup, or empty String to retrieve the last version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") - fun getRoomSessionData(@Path("roomId") roomId: String, - @Path("sessionId") sessionId: String, - @Query("version") version: String): Call + suspend fun getRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): KeyBackupData /** * Retrieve all the keys for the given room from the backup. @@ -144,8 +143,8 @@ internal interface RoomKeysApi { * @param version the version of the backup, or empty String to retrieve the last version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") - fun getRoomSessionsData(@Path("roomId") roomId: String, - @Query("version") version: String): Call + suspend fun getRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): RoomKeysBackupData /** * Retrieve all the keys from the backup. @@ -153,7 +152,7 @@ internal interface RoomKeysApi { * @param version the version of the backup, or empty String to retrieve the last version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") - fun getSessionsData(@Query("version") version: String): Call + suspend fun getSessionsData(@Query("version") version: String): KeysBackupData /* ========================================================================================== * Deleting keys @@ -163,22 +162,22 @@ internal interface RoomKeysApi { * Deletes keys from the backup. */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") - fun deleteRoomSessionData(@Path("roomId") roomId: String, - @Path("sessionId") sessionId: String, - @Query("version") version: String): Call + suspend fun deleteRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String) /** * Deletes keys from the backup. */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") - fun deleteRoomSessionsData(@Path("roomId") roomId: String, - @Query("version") version: String): Call + suspend fun deleteRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String) /** * Deletes keys from the backup. */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") - fun deleteSessionsData(@Query("version") version: String): Call + suspend fun deleteSessionsData(@Query("version") version: String) /* ========================================================================================== * Deleting backup @@ -188,5 +187,5 @@ internal interface RoomKeysApi { * Deletes a backup. */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") - fun deleteBackup(@Path("version") version: String): Call + suspend fun deleteBackup(@Path("version") version: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt index 5c59cfd80e..62610a0b7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt @@ -33,7 +33,7 @@ internal class DefaultCreateKeysBackupVersionTask @Inject constructor( override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.createKeysBackupVersion(params) + roomKeysApi.createKeysBackupVersion(params) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt index ec09da7240..7ee6f2358d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt @@ -35,7 +35,7 @@ internal class DefaultDeleteBackupTask @Inject constructor( override suspend fun execute(params: DeleteBackupTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.deleteBackup(params.version) + roomKeysApi.deleteBackup(params.version) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt index 9c477efb78..7f1b03b932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -37,7 +37,7 @@ internal class DefaultDeleteRoomSessionDataTask @Inject constructor( override suspend fun execute(params: DeleteRoomSessionDataTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.deleteRoomSessionData( + roomKeysApi.deleteRoomSessionData( params.roomId, params.sessionId, params.version) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt index 82d022f3ab..394cc861d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt @@ -36,7 +36,7 @@ internal class DefaultDeleteRoomSessionsDataTask @Inject constructor( override suspend fun execute(params: DeleteRoomSessionsDataTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.deleteRoomSessionsData( + roomKeysApi.deleteRoomSessionsData( params.roomId, params.version) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt index e4df379963..808c6c9956 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt @@ -35,7 +35,7 @@ internal class DefaultDeleteSessionsDataTask @Inject constructor( override suspend fun execute(params: DeleteSessionsDataTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.deleteSessionsData(params.version) + roomKeysApi.deleteSessionsData(params.version) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt index 3566ff0e68..54dbf85e30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt @@ -32,7 +32,7 @@ internal class DefaultGetKeysBackupLastVersionTask @Inject constructor( override suspend fun execute(params: Unit): KeysVersionResult { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.getKeysBackupLastVersion() + roomKeysApi.getKeysBackupLastVersion() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt index 13c99fb0f4..390873eb68 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -32,7 +32,7 @@ internal class DefaultGetKeysBackupVersionTask @Inject constructor( override suspend fun execute(params: String): KeysVersionResult { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.getKeysBackupVersion(params) + roomKeysApi.getKeysBackupVersion(params) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt index 168020d9cd..ff515ed80f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -38,7 +38,7 @@ internal class DefaultGetRoomSessionDataTask @Inject constructor( override suspend fun execute(params: GetRoomSessionDataTask.Params): KeyBackupData { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.getRoomSessionData( + roomKeysApi.getRoomSessionData( params.roomId, params.sessionId, params.version) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt index 95d5ef2e53..1b4fe2d966 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt @@ -37,7 +37,7 @@ internal class DefaultGetRoomSessionsDataTask @Inject constructor( override suspend fun execute(params: GetRoomSessionsDataTask.Params): RoomKeysBackupData { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.getRoomSessionsData( + roomKeysApi.getRoomSessionsData( params.roomId, params.version) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt index e41a13e3eb..707125f4cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt @@ -36,7 +36,7 @@ internal class DefaultGetSessionsDataTask @Inject constructor( override suspend fun execute(params: GetSessionsDataTask.Params): KeysBackupData { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.getSessionsData(params.version) + roomKeysApi.getSessionsData(params.version) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt index 3954277e39..180aaecf82 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt @@ -40,7 +40,7 @@ internal class DefaultStoreRoomSessionDataTask @Inject constructor( override suspend fun execute(params: StoreRoomSessionDataTask.Params): BackupKeysResult { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.storeRoomSessionData( + roomKeysApi.storeRoomSessionData( params.roomId, params.sessionId, params.version, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt index 4e209b4abc..d1aa9d2eb0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt @@ -39,7 +39,7 @@ internal class DefaultStoreRoomSessionsDataTask @Inject constructor( override suspend fun execute(params: StoreRoomSessionsDataTask.Params): BackupKeysResult { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.storeRoomSessionsData( + roomKeysApi.storeRoomSessionsData( params.roomId, params.version, params.roomKeysBackupData) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt index a607477d21..3dbeafe9de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -38,7 +38,7 @@ internal class DefaultStoreSessionsDataTask @Inject constructor( override suspend fun execute(params: StoreSessionsDataTask.Params): BackupKeysResult { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.storeSessionsData( + roomKeysApi.storeSessionsData( params.version, params.keysBackupData) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt index f012cd13eb..2b3d044ab7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt @@ -37,7 +37,7 @@ internal class DefaultUpdateKeysBackupVersionTask @Inject constructor( override suspend fun execute(params: UpdateKeysBackupVersionTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) + roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 82b5185fe8..1f80ce2c81 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto.secrets -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.accountdata.AccountDataService @@ -43,10 +42,9 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.matrix.olm.OlmPkMessage import java.security.SecureRandom import javax.crypto.Cipher @@ -64,21 +62,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor( private val cryptoCoroutineScope: CoroutineScope ) : SharedSecretStorageService { - override fun generateKey(keyId: String, - key: SsssKeySpec?, - keyName: String, - keySigner: KeySigner?, - callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val bytes = try { - (key as? RawBytesKeySpec)?.privateKey - ?: ByteArray(32).also { - SecureRandom().nextBytes(it) - } - } catch (failure: Throwable) { - callback.onFailure(failure) - return@launch - } + override suspend fun generateKey(keyId: String, + key: SsssKeySpec?, + keyName: String, + keySigner: KeySigner?): SsssKeyCreationInfo { + return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) { + val bytes = (key as? RawBytesKeySpec)?.privateKey + ?: ByteArray(32).also { + SecureRandom().nextBytes(it) + } val storageKeyContent = SecretStorageKeyContent( name = keyName, @@ -92,34 +84,22 @@ internal class DefaultSharedSecretStorageService @Inject constructor( ) } ?: storageKeyContent - accountDataService.updateAccountData( - "$KEY_ID_BASE.$keyId", - signedContent.toContent(), - object : MatrixCallback { - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - - override fun onSuccess(data: Unit) { - callback.onSuccess(SsssKeyCreationInfo( - keyId = keyId, - content = storageKeyContent, - recoveryKey = computeRecoveryKey(bytes), - keySpec = RawBytesKeySpec(bytes) - )) - } - } + accountDataService.updateAccountData("$KEY_ID_BASE.$keyId", signedContent.toContent()) + SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(bytes), + keySpec = RawBytesKeySpec(bytes) ) } } - override fun generateKeyWithPassphrase(keyId: String, - keyName: String, - passphrase: String, - keySigner: KeySigner, - progressListener: ProgressListener?, - callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + override suspend fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?): SsssKeyCreationInfo { + return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) { val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) val storageKeyContent = SecretStorageKeyContent( @@ -135,21 +115,13 @@ internal class DefaultSharedSecretStorageService @Inject constructor( accountDataService.updateAccountData( "$KEY_ID_BASE.$keyId", - signedContent.toContent(), - object : MatrixCallback { - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - - override fun onSuccess(data: Unit) { - callback.onSuccess(SsssKeyCreationInfo( - keyId = keyId, - content = storageKeyContent, - recoveryKey = computeRecoveryKey(privatePart.privateKey), - keySpec = RawBytesKeySpec(privatePart.privateKey) - )) - } - } + signedContent.toContent() + ) + SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(privatePart.privateKey), + keySpec = RawBytesKeySpec(privatePart.privateKey) ) } } @@ -168,15 +140,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor( } ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId)) } - override fun setDefaultKey(keyId: String, callback: MatrixCallback) { + override suspend fun setDefaultKey(keyId: String) { val existingKey = getKey(keyId) if (existingKey is KeyInfoResult.Success) { - accountDataService.updateAccountData(DEFAULT_KEY_ID, - mapOf("key" to keyId), - callback - ) + accountDataService.updateAccountData(DEFAULT_KEY_ID, mapOf("key" to keyId)) } else { - callback.onFailure(SharedSecretStorageError.UnknownKey(keyId)) + throw SharedSecretStorageError.UnknownKey(keyId) } } @@ -188,42 +157,31 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return getKey(keyId) } - override fun storeSecret(name: String, secretBase64: String, keys: List, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + override suspend fun storeSecret(name: String, secretBase64: String, keys: List) { + withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) { val encryptedContents = HashMap() - try { - keys.forEach { - val keyId = it.keyId - // encrypt the content - when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) { - is KeyInfoResult.Success -> { - if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) { - encryptAesHmacSha2(it.keySpec!!, name, secretBase64).let { - encryptedContents[key.keyInfo.id] = it - } - } else { - // Unknown algorithm - callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) - return@launch + keys.forEach { + val keyId = it.keyId + // encrypt the content + when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) { + is KeyInfoResult.Success -> { + if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) { + encryptAesHmacSha2(it.keySpec!!, name, secretBase64).let { + encryptedContents[key.keyInfo.id] = it } - } - is KeyInfoResult.Error -> { - callback.onFailure(key.error) - return@launch + } else { + // Unknown algorithm + throw SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "") } } + is KeyInfoResult.Error -> throw key.error } - - accountDataService.updateAccountData( - type = name, - content = mapOf( - "encrypted" to encryptedContents - ), - callback = callback - ) - } catch (failure: Throwable) { - callback.onFailure(failure) } + + accountDataService.updateAccountData( + type = name, + content = mapOf("encrypted" to encryptedContents) + ) } } @@ -344,57 +302,40 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return results } - override fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback) { - val accountData = accountDataService.getAccountDataEvent(name) ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.UnknownSecret(name)) - } - val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.SecretNotEncrypted(name)) - } - val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.UnknownKey(name)) - } + override suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String { + val accountData = accountDataService.getAccountDataEvent(name) ?: throw SharedSecretStorageError.UnknownSecret(name) + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: throw SharedSecretStorageError.SecretNotEncrypted(name) + val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success + ?: throw SharedSecretStorageError.UnknownKey(name) - val encryptedForKey = encryptedContent[key.keyInfo.id] ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id)) - } + val encryptedForKey = encryptedContent[key.keyInfo.id] ?: throw SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id) val secretContent = EncryptedSecretContent.fromJson(encryptedForKey) - ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.ParsingError) - } + ?: throw SharedSecretStorageError.ParsingError val algorithm = key.keyInfo.content if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { - val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.BadKeyFormat) - } - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - runCatching { - // decrypt from recovery key - withOlmDecryption { olmPkDecryption -> - olmPkDecryption.setPrivateKey(keySpec.privateKey) - olmPkDecryption.decrypt(OlmPkMessage() - .apply { - mCipherText = secretContent.ciphertext - mEphemeralKey = secretContent.ephemeral - mMac = secretContent.mac - } - ) - } - }.foldToCallback(callback) + val keySpec = secretKey as? RawBytesKeySpec ?: throw SharedSecretStorageError.BadKeyFormat + return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) { + // decrypt from recovery key + withOlmDecryption { olmPkDecryption -> + olmPkDecryption.setPrivateKey(keySpec.privateKey) + olmPkDecryption.decrypt(OlmPkMessage() + .apply { + mCipherText = secretContent.ciphertext + mEphemeralKey = secretContent.ephemeral + mMac = secretContent.mac + } + ) + } } } else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) { - val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { - callback.onFailure(SharedSecretStorageError.BadKeyFormat) - } - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - runCatching { - decryptAesHmacSha2(keySpec, name, secretContent) - }.foldToCallback(callback) + val keySpec = secretKey as? RawBytesKeySpec ?: throw SharedSecretStorageError.BadKeyFormat + return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) { + decryptAesHmacSha2(keySpec, name, secretContent) } } else { - callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) + throw SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt index 3df6312adb..d5cf749db7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.MXKey import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody -import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -42,8 +41,8 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor( override suspend fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): MXUsersDevicesMap { val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) - val keysClaimResponse = executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body) + val keysClaimResponse = executeRequest(globalErrorReceiver) { + cryptoApi.claimOneTimeKeysForUsersDevices(body) } val map = MXUsersDevicesMap() keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 61596bb5b6..bdb8e8d137 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -42,8 +42,8 @@ internal class DefaultDeleteDeviceTask @Inject constructor( override suspend fun execute(params: DeleteDeviceTask.Params) { try { - executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) + executeRequest(globalErrorReceiver) { + cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) } } catch (throwable: Throwable) { if (params.userInteractiveAuthInterceptor == null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 5eb24b116a..86f02866ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -16,17 +16,25 @@ package org.matrix.android.sdk.internal.crypto.tasks +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.computeBestChunkSize import javax.inject.Inject internal interface DownloadKeysForUsersTask : Task { data class Params( - // the list of users to get keys for. + // the list of users to get keys for. The list MUST NOT be empty val userIds: List, // the up-to token val token: String? @@ -39,15 +47,68 @@ internal class DefaultDownloadKeysForUsers @Inject constructor( ) : DownloadKeysForUsersTask { override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { - val downloadQuery = params.userIds.associateWith { emptyList() } + val bestChunkSize = computeBestChunkSize(params.userIds.size, LIMIT) + val token = params.token?.takeIf { token -> token.isNotEmpty() } - val body = KeysQueryBody( - deviceKeys = downloadQuery, - token = params.token?.takeIf { it.isNotEmpty() } - ) + return if (bestChunkSize.shouldChunk()) { + // Store server results in these mutable maps + val deviceKeys = mutableMapOf>() + val failures = mutableMapOf>() + val masterKeys = mutableMapOf() + val selfSigningKeys = mutableMapOf() + val userSigningKeys = mutableMapOf() - return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.downloadKeysForUsers(body) + val mutex = Mutex() + + // Split network request into smaller request (#2925) + coroutineScope { + params.userIds + .chunked(bestChunkSize.chunkSize) + .map { + KeysQueryBody( + deviceKeys = it.associateWith { emptyList() }, + token = token + ) + } + .map { body -> + async { + val result = executeRequest(globalErrorReceiver) { + cryptoApi.downloadKeysForUsers(body) + } + + mutex.withLock { + deviceKeys.putAll(result.deviceKeys.orEmpty()) + failures.putAll(result.failures.orEmpty()) + masterKeys.putAll(result.masterKeys.orEmpty()) + selfSigningKeys.putAll(result.selfSigningKeys.orEmpty()) + userSigningKeys.putAll(result.userSigningKeys.orEmpty()) + } + } + } + .joinAll() + } + + KeysQueryResponse( + deviceKeys = deviceKeys, + failures = failures, + masterKeys = masterKeys, + selfSigningKeys = selfSigningKeys, + userSigningKeys = userSigningKeys + ) + } else { + // No need to chunk, direct request + executeRequest(globalErrorReceiver) { + cryptoApi.downloadKeysForUsers( + KeysQueryBody( + deviceKeys = params.userIds.associateWith { emptyList() }, + token = token + ) + ) + } } } + + companion object { + const val LIMIT = 250 + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt index 5f6d2e344f..9f20ea598d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt @@ -34,7 +34,7 @@ internal class DefaultGetDeviceInfoTask @Inject constructor( override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo { return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.getDeviceInfo(params.deviceId) + cryptoApi.getDeviceInfo(params.deviceId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt index ea33a918bc..52f9f73299 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt @@ -32,7 +32,7 @@ internal class DefaultGetDevicesTask @Inject constructor( override suspend fun execute(params: Unit): DevicesListResponse { return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.getDevices() + cryptoApi.getDevices() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt index 4cc9ab2fcb..6e524c7fbe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt @@ -39,7 +39,7 @@ internal class DefaultGetKeyChangesTask @Inject constructor( override suspend fun execute(params: GetKeyChangesTask.Params): KeyChangesResponse { return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.getKeyChanges(params.from, params.to) + cryptoApi.getKeyChanges(params.from, params.to) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt index 5226e52b33..d6a7f3c6a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -36,14 +35,14 @@ internal class DefaultRedactEventTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver) : RedactEventTask { override suspend fun execute(params: RedactEventTask.Params): String { - val executeRequest = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.redactEvent( + val response = executeRequest(globalErrorReceiver) { + roomAPI.redactEvent( txId = params.txID, roomId = params.roomId, eventId = params.eventId, reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) ) } - return executeRequest.eventId + return response.eventId } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 573f2c3a54..e1e297767b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -52,8 +51,8 @@ internal class DefaultSendEventTask @Inject constructor( val event = handleEncryption(params) val localId = event.eventId!! localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING) - val executeRequest = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.send( + val response = executeRequest(globalErrorReceiver) { + roomAPI.send( localId, roomId = event.roomId ?: "", content = event.content, @@ -61,7 +60,7 @@ internal class DefaultSendEventTask @Inject constructor( ) } localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT) - return executeRequest.eventId + return response.eventId } catch (e: Throwable) { // localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED) throw e diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt index d2af91601b..41a5118be0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt @@ -46,14 +46,16 @@ internal class DefaultSendToDeviceTask @Inject constructor( messages = params.contentMap.map ) - return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.sendToDevice( + return executeRequest( + globalErrorReceiver, + canRetry = true, + maxRetriesCount = 3 + ) { + cryptoApi.sendToDevice( params.eventType, params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), sendToDeviceBody ) - isRetryable = true - maxRetryCount = 3 } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index ab125135bb..d8b9d3cd86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -45,8 +44,8 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( try { localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) - val executeRequest = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.send( + val response = executeRequest(globalErrorReceiver) { + roomAPI.send( localId, roomId = event.roomId ?: "", content = event.content, @@ -54,7 +53,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( ) } localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT) - return executeRequest.eventId + return response.eventId } catch (e: Throwable) { localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED) throw e diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt index b835d46236..4bedb1f393 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt @@ -42,7 +42,7 @@ internal class DefaultSetDeviceNameTask @Inject constructor( displayName = params.deviceName ) return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) + cryptoApi.updateDeviceInfo(params.deviceId, body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt index eb53bbbf8d..cac4dadd93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt @@ -50,7 +50,7 @@ internal class DefaultUploadKeysTask @Inject constructor( Timber.i("## Uploading device keys -> $body") return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.uploadKeys(body) + cryptoApi.uploadKeys(body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt index c50faf37b1..e03e353cb1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -36,10 +35,12 @@ internal class DefaultUploadSignaturesTask @Inject constructor( override suspend fun execute(params: UploadSignaturesTask.Params) { try { - val response = executeRequest(globalErrorReceiver) { - this.isRetryable = true - this.maxRetryCount = 10 - this.apiCall = cryptoApi.uploadSignatures(params.signatures) + val response = executeRequest( + globalErrorReceiver, + canRetry = true, + maxRetriesCount = 10 + ) { + cryptoApi.uploadSignatures(params.signatures) } if (response.failures?.isNotEmpty() == true) { throw Throwable(response.failures.toString()) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt index 14fad2ea38..08c767ba34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody import org.matrix.android.sdk.internal.crypto.model.toRest @@ -61,8 +60,8 @@ internal class DefaultUploadSigningKeysTask @Inject constructor( } private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) { - val keysQueryResponse = executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.uploadSigningKeys(uploadQuery) + val keysQueryResponse = executeRequest(globalErrorReceiver) { + cryptoApi.uploadSigningKeys(uploadQuery) } if (keysQueryResponse.failures?.isNotEmpty() == true) { throw UploadSigningKeys(keysQueryResponse.failures) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index a92f5c5bf1..d9da88770c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification import android.os.Handler import android.os.Looper import dagger.Lazy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -1293,8 +1292,7 @@ internal class DefaultVerificationService @Inject constructor( transactionId: String, roomId: String, otherUserId: String, - otherDeviceId: String, - callback: MatrixCallback?): String? { + otherDeviceId: String): String { if (method == VerificationMethod.SAS) { val tx = DefaultOutgoingSASDefaultVerificationTransaction( setDeviceVerificationAction, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index c7fe7ab447..1daae906f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -17,22 +17,27 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm +import io.realm.FieldAttribute import io.realm.RealmMigration +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields +import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import timber.log.Timber import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 8L + const val SESSION_STORE_SCHEMA_VERSION = 9L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -46,6 +51,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 5) migrateTo6(realm) if (oldVersion <= 6) migrateTo7(realm) if (oldVersion <= 7) migrateTo8(realm) + if (oldVersion <= 8) migrateTo9(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -149,4 +155,43 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.removeField("sourceLocalEchoEvents") ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema) } + + fun migrateTo9(realm: DynamicRealm) { + Timber.d("Step 8 -> 9") + + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Long::class.java, FieldAttribute.INDEXED) + ?.setNullable(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, true) + ?.addIndex(RoomSummaryEntityFields.MEMBERSHIP_STR) + ?.addIndex(RoomSummaryEntityFields.IS_DIRECT) + ?.addIndex(RoomSummaryEntityFields.VERSIONING_STATE_STR) + + ?.addField(RoomSummaryEntityFields.IS_FAVOURITE, Boolean::class.java) + ?.addIndex(RoomSummaryEntityFields.IS_FAVOURITE) + ?.addField(RoomSummaryEntityFields.IS_LOW_PRIORITY, Boolean::class.java) + ?.addIndex(RoomSummaryEntityFields.IS_LOW_PRIORITY) + ?.addField(RoomSummaryEntityFields.IS_SERVER_NOTICE, Boolean::class.java) + ?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE) + + ?.transform { obj -> + + val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any { + it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE + } + obj.setBoolean(RoomSummaryEntityFields.IS_FAVOURITE, isFavorite) + + val isLowPriority = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any { + it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_LOW_PRIORITY + } + + obj.setBoolean(RoomSummaryEntityFields.IS_LOW_PRIORITY, isLowPriority) + +// XXX migrate last message origin server ts + obj.getObject(RoomSummaryEntityFields.LATEST_PREVIEWABLE_EVENT.`$`) + ?.getObject(TimelineEventEntityFields.ROOT.`$`) + ?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let { + obj.setLong(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, it) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 2e54a4cd52..6dc70b60fc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -26,7 +26,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa private val typingUsersTracker: DefaultTypingUsersTracker) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { - val tags = roomSummaryEntity.tags.map { + val tags = roomSummaryEntity.tags().map { RoomTag(it.tagName, it.tagOrder) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt index a2b36ce590..f3bea68c26 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.database.model.TimelineEventEntity @@ -25,9 +24,9 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { - fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List? = null): TimelineEvent { + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true): TimelineEvent { val readReceipts = if (buildReadReceipts) { - correctedReadReceipts ?: timelineEventEntity.readReceipts + timelineEventEntity.readReceipts ?.let { readReceiptsSummaryMapper.map(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt index a48b081f02..e970fab397 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt @@ -39,5 +39,7 @@ internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = membershipStr = value.name } + fun getBestName() = displayName?.takeIf { it.isNotBlank() } ?: userId + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index 37696c9082..c87ac15a78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -16,61 +16,217 @@ package org.matrix.android.sdk.internal.database.model +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.VersioningState -import io.realm.RealmList -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag internal open class RoomSummaryEntity( - @PrimaryKey var roomId: String = "", - var displayName: String? = "", - var avatarUrl: String? = "", - var name: String? = "", - var topic: String? = "", - var latestPreviewableEvent: TimelineEventEntity? = null, - var heroes: RealmList = RealmList(), - var joinedMembersCount: Int? = 0, - var invitedMembersCount: Int? = 0, - var isDirect: Boolean = false, - var directUserId: String? = null, - var otherMemberIds: RealmList = RealmList(), - var notificationCount: Int = 0, - var highlightCount: Int = 0, - var readMarkerId: String? = null, - var hasUnreadMessages: Boolean = false, - var tags: RealmList = RealmList(), - var userDrafts: UserDraftsEntity? = null, - var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS, - var canonicalAlias: String? = null, - var aliases: RealmList = RealmList(), - // this is required for querying - var flatAliases: String = "", - var isEncrypted: Boolean = false, - var encryptionEventTs: Long? = 0, - var roomEncryptionTrustLevelStr: String? = null, - var inviterId: String? = null, - var hasFailedSending: Boolean = false + @PrimaryKey var roomId: String = "" ) : RealmObject() { + var displayName: String? = "" + set(value) { + if (value != field) field = value + } + var avatarUrl: String? = "" + set(value) { + if (value != field) field = value + } + var name: String? = "" + set(value) { + if (value != field) field = value + } + var topic: String? = "" + set(value) { + if (value != field) field = value + } + + var latestPreviewableEvent: TimelineEventEntity? = null + set(value) { + if (value != field) field = value + } + + @Index + var lastActivityTime: Long? = null + set(value) { + if (value != field) field = value + } + + var heroes: RealmList = RealmList() + + var joinedMembersCount: Int? = 0 + set(value) { + if (value != field) field = value + } + + var invitedMembersCount: Int? = 0 + set(value) { + if (value != field) field = value + } + + @Index + var isDirect: Boolean = false + set(value) { + if (value != field) field = value + } + + var directUserId: String? = null + set(value) { + if (value != field) field = value + } + + var otherMemberIds: RealmList = RealmList() + + var notificationCount: Int = 0 + set(value) { + if (value != field) field = value + } + + var highlightCount: Int = 0 + set(value) { + if (value != field) field = value + } + + var readMarkerId: String? = null + set(value) { + if (value != field) field = value + } + + var hasUnreadMessages: Boolean = false + set(value) { + if (value != field) field = value + } + + private var tags: RealmList = RealmList() + + fun tags(): List = tags + + fun updateTags(newTags: List>) { + val toDelete = mutableListOf() + tags.forEach { existingTag -> + val updatedTag = newTags.firstOrNull { it.first == existingTag.tagName } + if (updatedTag == null) { + toDelete.add(existingTag) + } else { + existingTag.tagOrder = updatedTag.second + } + } + toDelete.forEach { it.deleteFromRealm() } + newTags.forEach { newTag -> + if (tags.all { it.tagName != newTag.first }) { + // we must add it + tags.add( + RoomTagEntity(newTag.first, newTag.second) + ) + } + } + + isFavourite = newTags.any { it.first == RoomTag.ROOM_TAG_FAVOURITE } + isLowPriority = newTags.any { it.first == RoomTag.ROOM_TAG_LOW_PRIORITY } + isServerNotice = newTags.any { it.first == RoomTag.ROOM_TAG_SERVER_NOTICE } + } + + @Index + var isFavourite: Boolean = false + set(value) { + if (value != field) field = value + } + + @Index + var isLowPriority: Boolean = false + set(value) { + if (value != field) field = value + } + + @Index + var isServerNotice: Boolean = false + set(value) { + if (value != field) field = value + } + + var userDrafts: UserDraftsEntity? = null + set(value) { + if (value != field) field = value + } + + var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS + set(value) { + if (value != field) field = value + } + + var canonicalAlias: String? = null + set(value) { + if (value != field) field = value + } + + var aliases: RealmList = RealmList() + + fun updateAliases(newAliases: List) { + // only update underlying field if there is a diff + if (newAliases.distinct().sorted() != aliases.distinct().sorted()) { + aliases.clear() + aliases.addAll(newAliases) + flatAliases = newAliases.joinToString(separator = "|", prefix = "|") + } + } + + // this is required for querying + var flatAliases: String = "" + + var isEncrypted: Boolean = false + set(value) { + if (value != field) field = value + } + + var encryptionEventTs: Long? = 0 + set(value) { + if (value != field) field = value + } + + var roomEncryptionTrustLevelStr: String? = null + set(value) { + if (value != field) field = value + } + + var inviterId: String? = null + set(value) { + if (value != field) field = value + } + + var hasFailedSending: Boolean = false + set(value) { + if (value != field) field = value + } + + @Index private var membershipStr: String = Membership.NONE.name + var membership: Membership get() { return Membership.valueOf(membershipStr) } set(value) { - membershipStr = value.name + if (value.name != membershipStr) { + membershipStr = value.name + } } + @Index private var versioningStateStr: String = VersioningState.NONE.name var versioningState: VersioningState get() { return VersioningState.valueOf(versioningStateStr) } set(value) { - versioningStateStr = value.name + if (value.name != versioningStateStr) { + versioningStateStr = value.name + } } var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? @@ -84,7 +240,9 @@ internal open class RoomSummaryEntity( } } set(value) { - roomEncryptionTrustLevelStr = value?.name + if (value?.name != roomEncryptionTrustLevelStr) { + roomEncryptionTrustLevelStr = value?.name + } } companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index a3c741ad55..5423025823 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -38,16 +38,21 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration, Realm.getInstance(realmConfiguration).use { realm -> val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use val eventToCheck = liveChunk.timelineEvents.find(eventId) - isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) { - true - } else { - val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: return@use - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex - ?: Int.MIN_VALUE - val eventToCheckIndex = eventToCheck.displayIndex + isEventRead = when { + eventToCheck == null -> { + // This can happen in case of fast lane Event + false + } + eventToCheck.root?.sender == userId -> true + else -> { + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: return@use + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck.displayIndex - eventToCheckIndex <= readReceiptIndex + eventToCheckIndex <= readReceiptIndex + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt index 7430b7822f..2af5dcf0ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt @@ -48,9 +48,15 @@ internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: Strin return where(realm, roomId).findFirst() ?: realm.createObject(roomId) } -internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { +internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm, + excludeRoomIds: Set? = null): RealmResults { return RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .apply { + if (!excludeRoomIds.isNullOrEmpty()) { + not().`in`(RoomSummaryEntityFields.ROOM_ID, excludeRoomIds.toTypedArray()) + } + } .findAll() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index f4688411ff..0d0892b608 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingIntercept import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import org.matrix.android.sdk.internal.network.ApiInterceptor import java.util.concurrent.TimeUnit @Module @@ -63,7 +64,8 @@ internal object NetworkModule { timeoutInterceptor: TimeOutInterceptor, userAgentInterceptor: UserAgentInterceptor, httpLoggingInterceptor: HttpLoggingInterceptor, - curlLoggingInterceptor: CurlLoggingInterceptor): OkHttpClient { + curlLoggingInterceptor: CurlLoggingInterceptor, + apiInterceptor: ApiInterceptor): OkHttpClient { return OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) @@ -76,6 +78,7 @@ internal object NetworkModule { .addInterceptor(timeoutInterceptor) .addInterceptor(userAgentInterceptor) .addInterceptor(httpLoggingInterceptor) + .addInterceptor(apiInterceptor) .apply { if (BuildConfig.LOG_PRIVATE_DATA) { addInterceptor(curlLoggingInterceptor) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt index 8786321464..2ce0534b49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt @@ -21,7 +21,6 @@ import arrow.core.Success import arrow.core.Try import arrow.core.TryOf import arrow.core.fix -import org.matrix.android.sdk.api.MatrixCallback inline fun TryOf.onError(f: (Throwable) -> Unit): Try = fix() .fold( @@ -32,10 +31,6 @@ inline fun TryOf.onError(f: (Throwable) -> Unit): Try = fix() { Success(it) } ) -fun Try.foldToCallback(callback: MatrixCallback): Unit = fold( - { callback.onFailure(it) }, - { callback.onSuccess(it) }) - /** * Same as doOnNext for Observables */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/FederationAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/FederationAPI.kt index 1816616336..c37392494f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/FederationAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/FederationAPI.kt @@ -18,10 +18,9 @@ package org.matrix.android.sdk.internal.federation import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.GET internal interface FederationAPI { @GET(NetworkConstants.URI_FEDERATION_PATH + "version") - fun getVersion(): Call + suspend fun getVersion(): FederationGetVersionResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/GetFederationVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/GetFederationVersionTask.kt index ce35e48f6b..b7f73a606c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/GetFederationVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/federation/GetFederationVersionTask.kt @@ -28,8 +28,8 @@ internal class DefaultGetFederationVersionTask @Inject constructor( ) : GetFederationVersionTask { override suspend fun execute(params: Unit): FederationVersion { - val result = executeRequest(null) { - apiCall = federationAPI.getVersion() + val result = executeRequest(null) { + federationAPI.getVersion() } return FederationVersion( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ApiInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ApiInterceptor.kt new file mode 100644 index 0000000000..1dd6e75ea5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ApiInterceptor.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.Interceptor +import okhttp3.Response +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.network.ApiInterceptorListener +import org.matrix.android.sdk.api.network.ApiPath +import org.matrix.android.sdk.internal.di.MatrixScope +import timber.log.Timber +import javax.inject.Inject + +/** + * Interceptor class for provided api paths. + */ +@MatrixScope +internal class ApiInterceptor @Inject constructor() : Interceptor { + + init { + Timber.d("ApiInterceptor.init") + } + + private val apiResponseListenersMap = mutableMapOf>() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val path = request.url.encodedPath.replaceFirst("/", "") + val method = request.method + + val response = chain.proceed(request) + + synchronized(apiResponseListenersMap) { + findApiPath(path, method)?.let { apiPath -> + response.peekBody(Long.MAX_VALUE).string().let { networkResponse -> + apiResponseListenersMap[apiPath]?.forEach { listener -> + tryOrNull("Error in the implementation") { + listener.onApiResponse(apiPath, networkResponse) + } + } + } + } + } + + return response + } + + private fun findApiPath(path: String, method: String): ApiPath? { + return apiResponseListenersMap + .keys + .find { apiPath -> + apiPath.method === method && isTheSamePath(apiPath.path, path) + } + } + + private fun isTheSamePath(pattern: String, path: String): Boolean { + val patternSegments = pattern.split("/") + val pathSegments = path.split("/") + + if (patternSegments.size != pathSegments.size) return false + + return patternSegments.indices.all { i -> + patternSegments[i] == pathSegments[i] || patternSegments[i].startsWith("{") + } + } + + /** + * Adds listener to send intercepted api responses through. + */ + fun addListener(path: ApiPath, listener: ApiInterceptorListener) { + synchronized(apiResponseListenersMap) { + apiResponseListenersMap.getOrPut(path) { mutableListOf() } + .add(listener) + } + } + + /** + * Remove listener to send intercepted api responses through. + */ + fun removeListener(path: ApiPath, listener: ApiInterceptorListener) { + synchronized(apiResponseListenersMap) { + apiResponseListenersMap[path]?.remove(listener) + if (apiResponseListenersMap[path]?.isEmpty() == true) { + apiResponseListenersMap.remove(path) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index 442029127d..0246bae024 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -19,38 +19,49 @@ package org.matrix.android.sdk.internal.network import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.failure.getRetryDelay import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.internal.network.ssl.CertUtil -import retrofit2.Call -import retrofit2.awaitResponse +import retrofit2.HttpException import timber.log.Timber import java.io.IOException -internal suspend inline fun executeRequest(globalErrorReceiver: GlobalErrorReceiver?, - block: Request.() -> Unit) = Request(globalErrorReceiver).apply(block).execute() +/** + * Execute a request from the requestBlock and handle some of the Exception it could generate + * Ref: https://github.com/matrix-org/matrix-js-sdk/blob/develop/src/scheduler.js#L138-L175 + * + * @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError] + * @param canRetry if set to true, the request will be executed again in case of error, after a delay + * @param maxDelayBeforeRetry the max delay to wait before a retry + * @param maxRetriesCount the max number of retries + * @param requestBlock a suspend lambda to perform the network request + */ +internal suspend inline fun executeRequest(globalErrorReceiver: GlobalErrorReceiver?, + canRetry: Boolean = false, + maxDelayBeforeRetry: Long = 32_000L, + maxRetriesCount: Int = 4, + noinline requestBlock: suspend () -> DATA): DATA { + var currentRetryCount = 0 + var currentDelay = 1_000L -internal class Request(private val globalErrorReceiver: GlobalErrorReceiver?) { - - var isRetryable = false - var initialDelay: Long = 100L - var maxDelay: Long = 10_000L - var maxRetryCount = Int.MAX_VALUE - private var currentRetryCount = 0 - private var currentDelay = initialDelay - lateinit var apiCall: Call - - suspend fun execute(): DATA { - return try { - val response = apiCall.clone().awaitResponse() - if (response.isSuccessful) { - response.body() - ?: throw IllegalStateException("The request returned a null body") - } else { - throw response.toFailure(globalErrorReceiver) + while (true) { + try { + return requestBlock() + } catch (throwable: Throwable) { + val exception = when (throwable) { + is KotlinNullPointerException -> IllegalStateException("The request returned a null body") + is HttpException -> throwable.toFailure(globalErrorReceiver) + else -> throwable + } + + // Log some details about the request which has failed. + val request = (throwable as? HttpException)?.response()?.raw()?.request + if (request == null) { + Timber.e("Exception when executing request") + } else { + Timber.e("Exception when executing request ${request.method} ${request.url.toString().substringBefore("?")}") } - } catch (exception: Throwable) { - // Log some details about the request which has failed - Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}") // Check if this is a certificateException CertUtil.getCertificateException(exception) @@ -61,10 +72,18 @@ internal class Request(private val globalErrorReceiver: GlobalErrorR // } ?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException } - if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) { + currentRetryCount++ + + if (exception is Failure.ServerError + && exception.httpCode == 429 + && exception.error.code == MatrixError.M_LIMIT_EXCEEDED + && currentRetryCount < maxRetriesCount) { + // 429, we can retry + delay(exception.getRetryDelay(1_000)) + } else if (canRetry && currentRetryCount < maxRetriesCount && exception.shouldBeRetried()) { delay(currentDelay) - currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) - return execute() + currentDelay = currentDelay.times(2L).coerceAtMost(maxDelayBeforeRetry) + // Try again (loop) } else { throw when (exception) { is IOException -> Failure.NetworkConnection(exception) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt index dd5a69dd3c..7132b4ff7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.internal.di.MoshiProvider import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.ResponseBody +import retrofit2.HttpException import retrofit2.Response import timber.log.Timber import java.io.IOException @@ -57,6 +58,13 @@ internal fun Response.toFailure(globalErrorReceiver: GlobalErrorReceiver? return toFailure(errorBody(), code(), globalErrorReceiver) } +/** + * Convert a HttpException to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun HttpException.toFailure(globalErrorReceiver: GlobalErrorReceiver?): Failure { + return toFailure(response()?.errorBody(), code(), globalErrorReceiver) +} + /** * Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt index 16633d90ef..d0e2534e7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.raw import com.zhuinden.monarchy.Monarchy -import okhttp3.ResponseBody import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.internal.database.model.RawCacheEntity import org.matrix.android.sdk.internal.database.query.get @@ -58,8 +57,8 @@ internal class DefaultGetUrlTask @Inject constructor( } private suspend fun doRequest(url: String): String { - return executeRequest(null) { - apiCall = rawAPI.getUrl(url) + return executeRequest(null) { + rawAPI.getUrl(url) } .string() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawAPI.kt index 4b08afd711..338d94781b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawAPI.kt @@ -18,11 +18,10 @@ package org.matrix.android.sdk.internal.raw import okhttp3.ResponseBody -import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Url internal interface RawAPI { @GET - fun getUrl(@Url url: String): Call + suspend fun getUrl(@Url url: String): ResponseBody } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 07cde3da60..d05ee48c1b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -20,26 +20,20 @@ import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap import androidx.core.content.FileProvider -import arrow.core.Try -import kotlinx.coroutines.launch +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.completeWith import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.file.FileService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER -import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.md5 -import org.matrix.android.sdk.internal.util.toCancelable import org.matrix.android.sdk.internal.util.writeToFile import timber.log.Timber import java.io.File @@ -53,14 +47,15 @@ internal class DefaultFileService @Inject constructor( private val contentUrlResolver: ContentUrlResolver, @UnauthenticatedWithCertificateWithProgress private val okHttpClient: OkHttpClient, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : FileService { // Legacy folder, will be deleted private val legacyFolder = File(sessionCacheDirectory, "MF") + // Folder to store downloaded files (not decrypted) private val downloadFolder = File(sessionCacheDirectory, "F") + // Folder to store decrypted files private val decryptedFolder = File(downloadFolder, "D") @@ -73,134 +68,113 @@ internal class DefaultFileService @Inject constructor( * Retain ongoing downloads to avoid re-downloading and already downloading file * map of mxCurl to callbacks */ - private val ongoing = mutableMapOf>>() + private val ongoing = mutableMapOf>() /** * Download file in the cache folder, and eventually decrypt it * TODO looks like files are copied 3 times */ - override fun downloadFile(fileName: String, - mimeType: String?, - url: String?, - elementToDecrypt: ElementToDecrypt?, - callback: MatrixCallback): Cancelable { - url ?: return NoOpCancellable.also { - callback.onFailure(IllegalArgumentException("url is null")) - } + override suspend fun downloadFile(fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?): File { + url ?: throw IllegalArgumentException("url is null") Timber.v("## FileService downloadFile $url") - synchronized(ongoing) { + // TODO: Remove use of `synchronized` in suspend function. + val existingDownload = synchronized(ongoing) { val existing = ongoing[url] if (existing != null) { Timber.v("## FileService downloadFile is already downloading.. ") - existing.add(callback) - return NoOpCancellable + existing } else { // mark as tracked - ongoing[url] = ArrayList() + ongoing[url] = CompletableDeferred() // and proceed to download + null } } - return taskExecutor.executorScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.io) { - Try { - if (!decryptedFolder.exists()) { - decryptedFolder.mkdirs() - } - // ensure we use unique file name by using URL (mapped to suitable file name) - // Also we need to add extension for the FileProvider, if not it lot's of app that it's - // shared with will not function well (even if mime type is passed in the intent) - getFiles(url, fileName, mimeType, elementToDecrypt != null) - }.flatMap { cachedFiles -> - if (!cachedFiles.file.exists()) { - val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) + if (existingDownload != null) { + // FIXME If the first downloader cancels then we'll unfortunately be cancelled too. + return existingDownload.await() + } - val request = Request.Builder() - .url(resolvedUrl) - .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) - .build() - - val response = try { - okHttpClient.newCall(request).execute() - } catch (e: Throwable) { - return@flatMap Try.Failure(e) - } - - if (!response.isSuccessful) { - return@flatMap Try.Failure(IOException()) - } - - val source = response.body?.source() - ?: return@flatMap Try.Failure(IOException()) - - Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") - - // Write the file to cache (encrypted version if the file is encrypted) - writeToFile(source.inputStream(), cachedFiles.file) - response.close() - } else { - Timber.v("## FileService: cache hit for $url") - } - - Try.just(cachedFiles) + val result = runCatching { + val cachedFiles = withContext(coroutineDispatchers.io) { + if (!decryptedFolder.exists()) { + decryptedFolder.mkdirs() } - }.flatMap { cachedFiles -> - // Decrypt if necessary - if (cachedFiles.decryptedFile != null) { - if (!cachedFiles.decryptedFile.exists()) { - Timber.v("## FileService: decrypt file") - // Ensure the parent folder exists - cachedFiles.decryptedFile.parentFile?.mkdirs() - val decryptSuccess = cachedFiles.file.inputStream().use { inputStream -> - cachedFiles.decryptedFile.outputStream().buffered().use { outputStream -> - MXEncryptedAttachments.decryptAttachment( - inputStream, - elementToDecrypt, - outputStream - ) - } - } - if (!decryptSuccess) { - return@flatMap Try.Failure(IllegalStateException("Decryption error")) - } - } else { - Timber.v("## FileService: cache hit for decrypted file") + + // ensure we use unique file name by using URL (mapped to suitable file name) + // Also we need to add extension for the FileProvider, if not it lot's of app that it's + // shared with will not function well (even if mime type is passed in the intent) + val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null) + + if (!cachedFiles.file.exists()) { + val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null") + + val request = Request.Builder() + .url(resolvedUrl) + .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) + .build() + + val response = okHttpClient.newCall(request).execute() + + if (!response.isSuccessful) { + throw IOException() } - Try.just(cachedFiles.decryptedFile) + + val source = response.body?.source() ?: throw IOException() + + Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") + + // Write the file to cache (encrypted version if the file is encrypted) + writeToFile(source.inputStream(), cachedFiles.file) + response.close() } else { - // Clear file - Try.just(cachedFiles.file) + Timber.v("## FileService: cache hit for $url") } - }.fold( - { throwable -> - callback.onFailure(throwable) - // notify concurrent requests - val toNotify = synchronized(ongoing) { - ongoing[url]?.also { - ongoing.remove(url) - } - } - toNotify?.forEach { otherCallbacks -> - tryOrNull { otherCallbacks.onFailure(throwable) } - } - }, - { file -> - callback.onSuccess(file) - // notify concurrent requests - val toNotify = synchronized(ongoing) { - ongoing[url]?.also { - ongoing.remove(url) - } - } - Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") - toNotify?.forEach { otherCallbacks -> - tryOrNull { otherCallbacks.onSuccess(file) } + cachedFiles + } + + // Decrypt if necessary + if (cachedFiles.decryptedFile != null) { + if (!cachedFiles.decryptedFile.exists()) { + Timber.v("## FileService: decrypt file") + // Ensure the parent folder exists + cachedFiles.decryptedFile.parentFile?.mkdirs() + val decryptSuccess = cachedFiles.file.inputStream().use { inputStream -> + cachedFiles.decryptedFile.outputStream().buffered().use { outputStream -> + MXEncryptedAttachments.decryptAttachment( + inputStream, + elementToDecrypt, + outputStream + ) } } - ) - }.toCancelable() + if (!decryptSuccess) { + throw IllegalStateException("Decryption error") + } + } else { + Timber.v("## FileService: cache hit for decrypted file") + } + cachedFiles.decryptedFile + } else { + // Clear file + cachedFiles.file + } + } + + // notify concurrent requests + val toNotify = synchronized(ongoing) { ongoing.remove(url) } + result.onSuccess { + Timber.v("## FileService additional to notify is > 0 ") + } + toNotify?.completeWith(result) + + return result.getOrThrow() } fun storeDataFor(mxcUrl: String, @@ -325,6 +299,7 @@ internal class DefaultFileService @Inject constructor( companion object { private const val ENCRYPTED_FILENAME = "encrypted.bin" + // The extension would be added from the mimetype private const val DEFAULT_FILENAME = "file" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 45fcc5af2d..821a9cba8c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService @@ -114,6 +115,7 @@ internal class DefaultSession @Inject constructor( private val accountDataService: Lazy, private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, + private val eventService: Lazy, private val defaultIdentityService: DefaultIdentityService, private val integrationManagerService: IntegrationManagerService, private val thirdPartyService: Lazy, @@ -129,6 +131,7 @@ internal class DefaultSession @Inject constructor( FilterService by filterService.get(), PushRuleService by pushRuleService.get(), PushersService by pushersService.get(), + EventService by eventService.get(), TermsService by termsService.get(), InitialSyncProgressService by initialSyncProgressService.get(), SecureStorageService by secureStorageService.get(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index f10eb67921..e61e4ecd89 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -32,10 +32,11 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService @@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.network.token.HomeserverAccessTokenProvider import org.matrix.android.sdk.internal.session.call.CallEventProcessor import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor +import org.matrix.android.sdk.internal.session.events.DefaultEventService import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService @@ -357,6 +359,9 @@ internal abstract class SessionModule { @Binds abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService + @Binds + abstract fun bindEventService(service: DefaultEventService): EventService + @Binds abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt index 1db9d121a6..a04d0f2686 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.account import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST @@ -28,7 +27,7 @@ internal interface AccountAPI { * @param params parameters to change password. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") - fun changePassword(@Body params: ChangePasswordParams): Call + suspend fun changePassword(@Body params: ChangePasswordParams) /** * Deactivate the user account @@ -36,5 +35,5 @@ internal interface AccountAPI { * @param params the deactivate account params */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate") - fun deactivate(@Body params: DeactivateAccountParams): Call + suspend fun deactivate(@Body params: DeactivateAccountParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt index 1f043b0a9d..02c3735998 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt @@ -39,8 +39,8 @@ internal class DefaultChangePasswordTask @Inject constructor( override suspend fun execute(params: ChangePasswordTask.Params) { val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword) try { - executeRequest(globalErrorReceiver) { - apiCall = accountAPI.changePassword(changePasswordParams) + executeRequest(globalErrorReceiver) { + accountAPI.changePassword(changePasswordParams) } } catch (throwable: Throwable) { val registrationFlowResponse = throwable.toRegistrationFlowResponse() @@ -49,8 +49,8 @@ internal class DefaultChangePasswordTask @Inject constructor( /* Avoid infinite loop */ && changePasswordParams.auth?.session == null) { // Retry with authentication - executeRequest(globalErrorReceiver) { - apiCall = accountAPI.changePassword( + executeRequest(globalErrorReceiver) { + accountAPI.changePassword( changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = registrationFlowResponse.session)) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt index ca6b0554a9..1a8e80ab68 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -46,8 +46,8 @@ internal class DefaultDeactivateAccountTask @Inject constructor( val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData) val canCleanup = try { - executeRequest(globalErrorReceiver) { - apiCall = accountAPI.deactivate(deactivateAccountParams) + executeRequest(globalErrorReceiver) { + accountAPI.deactivate(deactivateAccountParams) } true } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index 4887351709..a190ff62ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -21,9 +21,11 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber import javax.inject.Inject +@SessionScope internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler) : EventInsertLiveProcessor { @@ -51,6 +53,15 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH eventsToPostProcess.add(event) } + fun shouldProcessFastLane(eventType: String): Boolean { + return eventType == EventType.CALL_INVITE + } + + suspend fun processFastLane(event: Event) { + eventsToPostProcess.add(event) + onPostProcess() + } + override suspend fun onPostProcess() { eventsToPostProcess.forEach { dispatchToCallSignalingHandlerIfNeeded(it) @@ -60,7 +71,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) { val now = System.currentTimeMillis() - // TODO might check if an invite is not closed (hangup/answsered) in the same event batch? + // TODO might check if an invite is not closed (hangup/answered) in the same event batch? event.roomId ?: return Unit.also { Timber.w("Event with no room id ${event.eventId}") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 7e54301f63..8d7e9e819a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -56,25 +56,25 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa fun onCallEvent(event: Event) { when (event.getClearType()) { - EventType.CALL_ANSWER -> { + EventType.CALL_ANSWER -> { handleCallAnswerEvent(event) } - EventType.CALL_INVITE -> { + EventType.CALL_INVITE -> { handleCallInviteEvent(event) } - EventType.CALL_HANGUP -> { + EventType.CALL_HANGUP -> { handleCallHangupEvent(event) } - EventType.CALL_REJECT -> { + EventType.CALL_REJECT -> { handleCallRejectEvent(event) } - EventType.CALL_CANDIDATES -> { + EventType.CALL_CANDIDATES -> { handleCallCandidatesEvent(event) } EventType.CALL_SELECT_ANSWER -> { handleCallSelectAnswerEvent(event) } - EventType.CALL_NEGOTIATE -> { + EventType.CALL_NEGOTIATE -> { handleCallNegotiateEvent(event) } } @@ -168,6 +168,14 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa return } val content = event.getClearContent().toModel() ?: return + + content.callId ?: return + if (activeCallHandler.getCallWithId(content.callId) != null) { + // Call is already known, maybe due to fast lane. Ignore + Timber.d("Ignoring already known call invite") + return + } + val incomingCall = mxCallFactory.createIncomingCall( roomId = event.roomId, opponentUserId = event.senderId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt index b21ec1113a..d53ddb7371 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt @@ -31,7 +31,7 @@ internal class DefaultGetTurnServerTask @Inject constructor(private val voipAPI: override suspend fun execute(params: Params): TurnServerResponse { return executeRequest(globalErrorReceiver) { - apiCall = voipAPI.getTurnServer() + voipAPI.getTurnServer() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt index 72c6c58f27..469faaae74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt @@ -18,11 +18,10 @@ package org.matrix.android.sdk.internal.session.call import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.GET internal interface VoipApi { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer") - fun getTurnServer(): Call + suspend fun getTurnServer(): TurnServerResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt index 6a50f3ee37..19bc7e1908 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.directory import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasBody import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -33,7 +32,7 @@ internal interface DirectoryAPI { * @param roomAlias the room alias. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") - fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call + suspend fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): RoomAliasDescription /** * Get the room directory visibility. @@ -41,7 +40,7 @@ internal interface DirectoryAPI { * @param roomId the room id. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}") - fun getRoomDirectoryVisibility(@Path("roomId") roomId: String): Call + suspend fun getRoomDirectoryVisibility(@Path("roomId") roomId: String): RoomDirectoryVisibilityJson /** * Set the room directory visibility. @@ -50,21 +49,21 @@ internal interface DirectoryAPI { * @param body the body containing the new directory visibility */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}") - fun setRoomDirectoryVisibility(@Path("roomId") roomId: String, - @Body body: RoomDirectoryVisibilityJson): Call + suspend fun setRoomDirectoryVisibility(@Path("roomId") roomId: String, + @Body body: RoomDirectoryVisibilityJson) /** * Add alias to the room. * @param roomAlias the room alias. */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") - fun addRoomAlias(@Path("roomAlias") roomAlias: String, - @Body body: AddRoomAliasBody): Call + suspend fun addRoomAlias(@Path("roomAlias") roomAlias: String, + @Body body: AddRoomAliasBody) /** * Delete a room alias * @param roomAlias the room alias. */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") - fun deleteRoomAlias(@Path("roomAlias") roomAlias: String): Call + suspend fun deleteRoomAlias(@Path("roomAlias") roomAlias: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt new file mode 100644 index 0000000000..d7e9ef2ee0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.events + +import org.matrix.android.sdk.api.session.events.EventService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.call.CallEventProcessor +import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask +import javax.inject.Inject + +internal class DefaultEventService @Inject constructor( + private val getEventTask: GetEventTask, + private val callEventProcessor: CallEventProcessor +) : EventService { + + override suspend fun getEvent(roomId: String, eventId: String): Event { + val event = getEventTask.execute(GetEventTask.Params(roomId, eventId)) + + // Fast lane to the call event processors: try to make the incoming call ring faster + if (callEventProcessor.shouldProcessFastLane(event.getClearType())) { + callEventProcessor.processFastLane(event) + } + + return event + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt new file mode 100644 index 0000000000..91e709e464 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.events + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent + +internal fun Event.getFixedRoomMemberContent(): RoomMemberContent? { + val content = content.toModel() + // if user is leaving, we should grab his last name and avatar from prevContent + return if (content?.membership?.isLeft() == true) { + val prevContent = resolvedPrevContent().toModel() + content.copy( + displayName = prevContent?.displayName, + avatarUrl = prevContent?.avatarUrl + ) + } else { + content + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt index 285bd51d38..2809dea23b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.filter import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -32,8 +31,8 @@ internal interface FilterApi { * @param body the Json representation of a FilterBody object */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") - fun uploadFilter(@Path("userId") userId: String, - @Body body: Filter): Call + suspend fun uploadFilter(@Path("userId") userId: String, + @Body body: Filter): FilterResponse /** * Gets a filter with a given filterId from the homeserver @@ -43,6 +42,6 @@ internal interface FilterApi { * @return Filter */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") - fun getFilterById(@Path("userId") userId: String, - @Path("filterId") filterId: String): Call + suspend fun getFilterById(@Path("userId") userId: String, + @Path("filterId") filterId: String): Filter } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt index d42962d54a..3cac89ce28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt @@ -59,9 +59,9 @@ internal class DefaultSaveFilterTask @Inject constructor( } val updated = filterRepository.storeFilter(filterBody, roomFilter) if (updated) { - val filterResponse = executeRequest(globalErrorReceiver) { + val filterResponse = executeRequest(globalErrorReceiver) { // TODO auto retry - apiCall = filterAPI.uploadFilter(userId, filterBody) + filterAPI.uploadFilter(userId, filterBody) } filterRepository.storeFilterId(filterBody, filterResponse.filterId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt index 9836164aec..4e0ee3422b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt @@ -64,14 +64,14 @@ internal class DefaultGetGroupDataTask @Inject constructor( } Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}") val data = groupIds.map { groupId -> - val groupSummary = executeRequest(globalErrorReceiver) { - apiCall = groupAPI.getSummary(groupId) + val groupSummary = executeRequest(globalErrorReceiver) { + groupAPI.getSummary(groupId) } - val groupRooms = executeRequest(globalErrorReceiver) { - apiCall = groupAPI.getRooms(groupId) + val groupRooms = executeRequest(globalErrorReceiver) { + groupAPI.getRooms(groupId) } - val groupUsers = executeRequest(globalErrorReceiver) { - apiCall = groupAPI.getUsers(groupId) + val groupUsers = executeRequest(globalErrorReceiver) { + groupAPI.getUsers(groupId) } GroupData(groupId, groupSummary, groupRooms, groupUsers) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt index 004112578c..58dcc57dd6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.group.model.GroupRooms import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse import org.matrix.android.sdk.internal.session.group.model.GroupUsers -import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path @@ -32,7 +31,7 @@ internal interface GroupAPI { * @param groupId the group id */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary") - fun getSummary(@Path("groupId") groupId: String): Call + suspend fun getSummary(@Path("groupId") groupId: String): GroupSummaryResponse /** * Request the rooms list. @@ -40,7 +39,7 @@ internal interface GroupAPI { * @param groupId the group id */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/rooms") - fun getRooms(@Path("groupId") groupId: String): Call + suspend fun getRooms(@Path("groupId") groupId: String): GroupRooms /** * Request the users list. @@ -48,5 +47,5 @@ internal interface GroupAPI { * @param groupId the group id */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/users") - fun getUsers(@Path("groupId") groupId: String): Call + suspend fun getUsers(@Path("groupId") groupId: String): GroupUsers } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt index 8242edac84..7de0cc9592 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.homeserver import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.GET internal interface CapabilitiesAPI { @@ -26,17 +25,17 @@ internal interface CapabilitiesAPI { * Request the homeserver capabilities */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") - fun getCapabilities(): Call + suspend fun getCapabilities(): GetCapabilitiesResult /** * Request the versions */ @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") - fun getVersions(): Call + suspend fun getVersions(): Versions /** * Ping the homeserver. We do not care about the returned data, so there is no use to parse them */ @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") - fun ping(): Call + suspend fun ping() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 84c9132d61..740370123f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -71,20 +71,20 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } val capabilities = runCatching { - executeRequest(globalErrorReceiver) { - apiCall = capabilitiesAPI.getCapabilities() + executeRequest(globalErrorReceiver) { + capabilitiesAPI.getCapabilities() } }.getOrNull() val mediaConfig = runCatching { - executeRequest(globalErrorReceiver) { - apiCall = mediaAPI.getMediaConfig() + executeRequest(globalErrorReceiver) { + mediaAPI.getMediaConfig() } }.getOrNull() val versions = runCatching { - executeRequest(null) { - apiCall = capabilitiesAPI.getVersions() + executeRequest(null) { + capabilitiesAPI.getVersions() } }.getOrNull() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt index 522097acbf..bb526adf4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt @@ -34,8 +34,8 @@ internal class HomeServerPinger @Inject constructor(private val taskExecutor: Ta suspend fun canReachHomeServer(): Boolean { return try { - executeRequest(null) { - apiCall = capabilitiesAPI.ping() + executeRequest(null) { + capabilitiesAPI.ping() } true } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index 948e387cb1..f5391d6cdb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import dagger.Lazy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure @@ -33,8 +32,6 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.extensions.observeNotNull @@ -49,8 +46,6 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentitySe import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.ensureProtocol import kotlinx.coroutines.withContext @@ -83,8 +78,7 @@ internal class DefaultIdentityService @Inject constructor( private val identityApiProvider: IdentityApiProvider, private val accountDataDataSource: AccountDataDataSource, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, - private val sessionParams: SessionParams, - private val taskExecutor: TaskExecutor + private val sessionParams: SessionParams ) : IdentityService, SessionLifecycleObserver { private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } @@ -136,101 +130,81 @@ internal class DefaultIdentityService @Inject constructor( return identityStore.getIdentityData()?.identityServerUrl } - override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + override suspend fun startBindThreePid(threePid: ThreePid) { if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { - callback.onFailure(IdentityServiceError.OutdatedHomeServer) - return NoOpCancellable + throw IdentityServiceError.OutdatedHomeServer } - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) - } + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) } - override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - identityStore.deletePendingBinding(threePid) - } + override suspend fun cancelBindThreePid(threePid: ThreePid) { + identityStore.deletePendingBinding(threePid) } - override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) - } + override suspend fun sendAgainValidationCode(threePid: ThreePid) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) } - override fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + override suspend fun finalizeBindThreePid(threePid: ThreePid) { if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { - callback.onFailure(IdentityServiceError.OutdatedHomeServer) - return NoOpCancellable + throw IdentityServiceError.OutdatedHomeServer } - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - bindThreePidsTask.execute(BindThreePidsTask.Params(threePid)) - } + bindThreePidsTask.execute(BindThreePidsTask.Params(threePid)) } - override fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - submitTokenForBindingTask.execute(IdentitySubmitTokenForBindingTask.Params(threePid, code)) - } + override suspend fun submitValidationToken(threePid: ThreePid, code: String) { + submitTokenForBindingTask.execute(IdentitySubmitTokenForBindingTask.Params(threePid, code)) } - override fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + override suspend fun unbindThreePid(threePid: ThreePid) { if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { - callback.onFailure(IdentityServiceError.OutdatedHomeServer) - return NoOpCancellable + throw IdentityServiceError.OutdatedHomeServer } - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid)) - } + unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid)) } - override fun isValidIdentityServer(url: String, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + override suspend fun isValidIdentityServer(url: String) { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) - identityPingTask.execute(IdentityPingTask.Params(api)) - } + identityPingTask.execute(IdentityPingTask.Params(api)) } - override fun disconnect(callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - identityDisconnectTask.execute(Unit) + override suspend fun disconnect() { + identityDisconnectTask.execute(Unit) - identityStore.setUrl(null) - updateIdentityAPI(null) - updateAccountData(null) - } + identityStore.setUrl(null) + updateIdentityAPI(null) + updateAccountData(null) } - override fun setNewIdentityServer(url: String, callback: MatrixCallback): Cancelable { + override suspend fun setNewIdentityServer(url: String): String { val urlCandidate = url.ensureProtocol() - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val current = getCurrentIdentityServerUrl() - if (urlCandidate == current) { - // Nothing to do - Timber.d("Same URL, nothing to do") - } else { - // Disconnect previous one if any, first, because the token will change. - // In case of error when configuring the new identity server, this is not a big deal, - // we will ask for a new token on the previous Identity server - runCatching { identityDisconnectTask.execute(Unit) } - .onFailure { Timber.w(it, "Unable to disconnect identity server") } + val current = getCurrentIdentityServerUrl() + if (urlCandidate == current) { + // Nothing to do + Timber.d("Same URL, nothing to do") + } else { + // Disconnect previous one if any, first, because the token will change. + // In case of error when configuring the new identity server, this is not a big deal, + // we will ask for a new token on the previous Identity server + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } - // Try to get a token - val token = getNewIdentityServerToken(urlCandidate) + // Try to get a token + val token = getNewIdentityServerToken(urlCandidate) - identityStore.setUrl(urlCandidate) - identityStore.setToken(token) - updateIdentityAPI(urlCandidate) + identityStore.setUrl(urlCandidate) + identityStore.setToken(token) + updateIdentityAPI(urlCandidate) - updateAccountData(urlCandidate) - } - urlCandidate + updateAccountData(urlCandidate) } + + return urlCandidate } private suspend fun updateAccountData(url: String?) { @@ -252,45 +226,38 @@ internal class DefaultIdentityService @Inject constructor( identityStore.setUserConsent(newValue) } - override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { + override suspend fun lookUp(threePids: List): List { if (!getUserConsent()) { - callback.onFailure(IdentityServiceError.UserConsentNotProvided) - return NoOpCancellable + throw IdentityServiceError.UserConsentNotProvided } if (threePids.isEmpty()) { - callback.onSuccess(emptyList()) - return NoOpCancellable + return emptyList() } - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - lookUpInternal(true, threePids) - } + return lookUpInternal(true, threePids) } - override fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable { + override suspend fun getShareStatus(threePids: List): Map { // Note: we do not require user consent here, because it is used for emails and phone numbers that the user has already sent // to the home server, and not emails and phone numbers from the contact book of the user if (threePids.isEmpty()) { - callback.onSuccess(emptyMap()) - return NoOpCancellable + return emptyMap() } - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val lookupResult = lookUpInternal(true, threePids) + val lookupResult = lookUpInternal(true, threePids) - threePids.associateWith { threePid -> - // If not in lookup result, check if there is a pending binding - if (lookupResult.firstOrNull { it.threePid == threePid } == null) { - if (identityStore.getPendingBinding(threePid) == null) { - SharedState.NOT_SHARED - } else { - SharedState.BINDING_IN_PROGRESS - } + return threePids.associateWith { threePid -> + // If not in lookup result, check if there is a pending binding + if (lookupResult.firstOrNull { it.threePid == threePid } == null) { + if (identityStore.getPendingBinding(threePid) == null) { + SharedState.NOT_SHARED } else { - SharedState.SHARED + SharedState.BINDING_IN_PROGRESS } + } else { + SharedState.SHARED } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt index 7e2702e70d..e9e4d17e3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt @@ -26,7 +26,6 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwn import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -43,20 +42,20 @@ internal interface IdentityAPI { * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-account */ @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "account") - fun getAccount(): Call + suspend fun getAccount(): IdentityAccountResponse /** * Logs out the access token, preventing it from being used to authenticate future requests to the server. */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout") - fun logout(): Call + suspend fun logout() /** * Request the hash detail to request a bunch of 3PIDs * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-hash-details */ @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details") - fun hashDetails(): Call + suspend fun hashDetails(): IdentityHashDetailResponse /** * Request a bunch of 3PIDs @@ -65,7 +64,7 @@ internal interface IdentityAPI { * @param body the body request */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup") - fun lookup(@Body body: IdentityLookUpParams): Call + suspend fun lookup(@Body body: IdentityLookUpParams): IdentityLookUpResponse /** * Create a session to change the bind status of an email to an identity server @@ -75,7 +74,7 @@ internal interface IdentityAPI { * @return the sid */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken") - fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): Call + suspend fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): IdentityRequestTokenResponse /** * Create a session to change the bind status of an phone number to an identity server @@ -85,7 +84,7 @@ internal interface IdentityAPI { * @return the sid */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken") - fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): Call + suspend fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): IdentityRequestTokenResponse /** * Validate ownership of an email address, or a phone number. @@ -94,6 +93,6 @@ internal interface IdentityAPI { * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-email-submittoken */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken") - fun submitToken(@Path("medium") medium: String, - @Body body: IdentityRequestOwnershipParams): Call + suspend fun submitToken(@Path("medium") medium: String, + @Body body: IdentityRequestOwnershipParams): SuccessResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt index fd6e1163ef..1671859585 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.identity import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -40,18 +39,18 @@ internal interface IdentityAuthAPI { * @return 200 in case of success */ @GET(NetworkConstants.URI_IDENTITY_PREFIX_PATH) - fun ping(): Call + suspend fun ping() /** * Ping v1 will be used to check outdated Identity server */ @GET("_matrix/identity/api/v1") - fun pingV1(): Call + suspend fun pingV1() /** * Exchanges an OpenID token from the homeserver for an access token to access the identity server. * The request body is the same as the values returned by /openid/request_token in the Client-Server API. */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") - fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call + suspend fun register(@Body openIdToken: RequestOpenIdTokenResponse): IdentityRegisterResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index 67f3b2aa56..4f6e906766 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -83,7 +83,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return try { LookUpData(hashedAddresses, executeRequest(null) { - apiCall = identityAPI.lookup(IdentityLookUpParams( + identityAPI.lookup(IdentityLookUpParams( hashedAddresses, IdentityHashDetailResponse.ALGORITHM_SHA256, hashDetailResponse.pepper @@ -126,7 +126,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { return executeRequest(null) { - apiCall = identityAPI.hashDetails() + identityAPI.hashDetails() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt index 50e24f1245..fc84a144fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt @@ -42,8 +42,8 @@ internal class DefaultIdentityDisconnectTask @Inject constructor( return } - executeRequest(null) { - apiCall = identityAPI.logout() + executeRequest(null) { + identityAPI.logout() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt index b0d33811bd..fca9408d9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt @@ -33,14 +33,14 @@ internal class DefaultIdentityPingTask @Inject constructor() : IdentityPingTask override suspend fun execute(params: IdentityPingTask.Params) { try { - executeRequest(null) { - apiCall = params.identityAuthAPI.ping() + executeRequest(null) { + params.identityAuthAPI.ping() } } catch (throwable: Throwable) { if (throwable is Failure.ServerError && throwable.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { // Check if API v1 is available - executeRequest(null) { - apiCall = params.identityAuthAPI.pingV1() + executeRequest(null) { + params.identityAuthAPI.pingV1() } // API V1 is responding, but not V2 -> Outdated throw IdentityServiceError.OutdatedIdentityServer diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt index 19215f353a..8cc854bd94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt @@ -33,7 +33,7 @@ internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegis override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { return executeRequest(null) { - apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + params.identityAuthAPI.register(params.openIdTokenResponse) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt index bd4cd763f0..9c89048176 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBind import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody -import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse import org.matrix.android.sdk.internal.task.Task import java.util.UUID import javax.inject.Inject @@ -56,8 +55,8 @@ internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( val clientSecret = identityPendingBinding?.clientSecret ?: UUID.randomUUID().toString() val sendAttempt = identityPendingBinding?.sendAttempt?.inc() ?: 1 - val tokenResponse = executeRequest(null) { - apiCall = when (params.threePid) { + val tokenResponse = executeRequest(null) { + when (params.threePid) { is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( clientSecret = clientSecret, sendAttempt = sendAttempt, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt index ebc71c715d..f884e2816d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.identity import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.toMedium -import org.matrix.android.sdk.internal.auth.registration.SuccessResult import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.identity.data.IdentityStore @@ -44,8 +43,8 @@ internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError - val tokenResponse = executeRequest(null) { - apiCall = identityAPI.submitToken( + val tokenResponse = executeRequest(null) { + identityAPI.submitToken( params.threePid.toMedium(), IdentityRequestOwnershipParams( clientSecret = identityPendingBinding.clientSecret, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt index d3aecce381..d06b157f22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt @@ -18,14 +18,13 @@ package org.matrix.android.sdk.internal.session.identity import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.identity.model.IdentityAccountResponse internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI { val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured // Always check that we have access to the service (regarding terms) - val identityAccountResponse = executeRequest(null) { - apiCall = identityAPI.getAccount() + val identityAccountResponse = executeRequest(null) { + identityAPI.getAccount() } assert(userId == identityAccountResponse.userId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt index d85e471f1d..e707c2351c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -65,8 +65,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor( } private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData { - return executeRequest(globalErrorReceiver) { - apiCall = mediaAPI.getPreviewUrlData(url, timestamp) + return executeRequest(globalErrorReceiver) { + mediaAPI.getPreviewUrlData(url, timestamp) } .toPreviewUrlData(url) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt index 32305cd4e4..fd906f0dc8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt @@ -36,7 +36,7 @@ internal class DefaultGetRawPreviewUrlTask @Inject constructor( override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict { return executeRequest(globalErrorReceiver) { - apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp) + mediaAPI.getPreviewUrlData(params.url, params.timestamp) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt index bbb4f1e06a..9ee1d26cdc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.media import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Query @@ -28,7 +27,7 @@ internal interface MediaAPI { * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config */ @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") - fun getMediaConfig(): Call + suspend fun getMediaConfig(): GetMediaConfigResult /** * Get information about a URL for the client. Typically this is called when a client @@ -39,5 +38,5 @@ internal interface MediaAPI { * if it does not have the requested version available. */ @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url") - fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call + suspend fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): JsonDict } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt index e00d2ff26c..38f6b08b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.notification import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.Action import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.pushrules.RuleSetKey import org.matrix.android.sdk.api.pushrules.getActions import org.matrix.android.sdk.api.pushrules.rest.PushRule @@ -45,6 +47,7 @@ internal class DefaultPushRuleService @Inject constructor( private val addPushRuleTask: AddPushRuleTask, private val updatePushRuleActionsTask: UpdatePushRuleActionsTask, private val removePushRuleTask: RemovePushRuleTask, + private val pushRuleFinder: PushRuleFinder, private val taskExecutor: TaskExecutor, @SessionDatabase private val monarchy: Monarchy ) : PushRuleService { @@ -130,6 +133,12 @@ internal class DefaultPushRuleService @Inject constructor( } } + override fun getActions(event: Event): List { + val rules = getPushRules(RuleScope.GLOBAL).getAllRules() + + return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty() + } + // fun processEvents(events: List) { // var hasDoneSomething = false // events.forEach { event -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 54883b51e6..0ece07fc15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -16,9 +16,7 @@ package org.matrix.android.sdk.internal.session.notification -import org.matrix.android.sdk.api.pushrules.ConditionResolver import org.matrix.android.sdk.api.pushrules.rest.PushRule -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse @@ -35,7 +33,7 @@ internal interface ProcessEventForPushTask : Task - fulfilledBingRule(event, params.rules)?.let { + pushRuleFinder.fulfilledBingRule(event, params.rules)?.let { Timber.v("[PushRules] Rule $it match for event ${event.eventId}") defaultPushRuleService.dispatchBing(event, it) } @@ -94,13 +92,4 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchFinish() } - - private fun fulfilledBingRule(event: Event, rules: List): PushRule? { - return rules.firstOrNull { rule -> - // All conditions must hold true for an event in order to apply the action for the event. - rule.enabled && rule.conditions?.all { - it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false - } ?: false - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/PushRuleFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/PushRuleFinder.kt new file mode 100644 index 0000000000..6e302d373d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/PushRuleFinder.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.notification + +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.session.events.model.Event +import javax.inject.Inject + +internal class PushRuleFinder @Inject constructor( + private val conditionResolver: ConditionResolver +) { + fun fulfilledBingRule(event: Event, rules: List): PushRule? { + return rules.firstOrNull { rule -> + // All conditions must hold true for an event in order to apply the action for the event. + rule.enabled && rule.conditions?.all { + it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false + } ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt index f83c6b770a..8481a6ab93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt @@ -31,7 +31,7 @@ internal class DefaultGetOpenIdTokenTask @Inject constructor( override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { return executeRequest(globalErrorReceiver) { - apiCall = openIdAPI.openIdToken(userId) + openIdAPI.openIdToken(userId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt index 4614d82453..ed090b845d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.openid import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Path @@ -34,6 +33,6 @@ internal interface OpenIdAPI { * @param userId the user id */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") - fun openIdToken(@Path("userId") userId: String, - @Body body: JsonDict = emptyMap()): Call + suspend fun openIdToken(@Path("userId") userId: String, + @Body body: JsonDict = emptyMap()): RequestOpenIdTokenResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt index 6d6d70bb0d..678d399428 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt @@ -50,13 +50,14 @@ internal class DefaultAddThreePidTask @Inject constructor( val clientSecret = UUID.randomUUID().toString() val sendAttempt = 1 - val result = executeRequest(globalErrorReceiver) { - val body = AddEmailBody( - clientSecret = clientSecret, - email = threePid.email, - sendAttempt = sendAttempt - ) - apiCall = profileAPI.addEmail(body) + val body = AddEmailBody( + clientSecret = clientSecret, + email = threePid.email, + sendAttempt = sendAttempt + ) + + val result = executeRequest(globalErrorReceiver) { + profileAPI.addEmail(body) } // Store as a pending three pid @@ -84,14 +85,15 @@ internal class DefaultAddThreePidTask @Inject constructor( val countryCode = parsedNumber.countryCode val country = phoneNumberUtil.getRegionCodeForCountryCode(countryCode) - val result = executeRequest(globalErrorReceiver) { - val body = AddMsisdnBody( - clientSecret = clientSecret, - country = country, - phoneNumber = parsedNumber.nationalNumber.toString(), - sendAttempt = sendAttempt - ) - apiCall = profileAPI.addMsisdn(body) + val body = AddMsisdnBody( + clientSecret = clientSecret, + country = country, + phoneNumber = parsedNumber.nationalNumber.toString(), + sendAttempt = sendAttempt + ) + + val result = executeRequest(globalErrorReceiver) { + profileAPI.addMsisdn(body) } // Store as a pending three pid diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt index a37e5380bc..87e51181e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt @@ -43,8 +43,8 @@ internal class DefaultBindThreePidsTask @Inject constructor(private val profileA val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError - executeRequest(globalErrorReceiver) { - apiCall = profileAPI.bindThreePid( + executeRequest(globalErrorReceiver) { + profileAPI.bindThreePid( BindThreePidBody( clientSecret = identityPendingBinding.clientSecret, identityServerUrlWithoutProtocol = identityServerUrlWithoutProtocol, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index b3216d744d..386fec8256 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -21,11 +21,10 @@ import android.net.Uri import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import io.realm.kotlin.where -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.profile.ProfileService -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional @@ -36,7 +35,6 @@ import org.matrix.android.sdk.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject @@ -55,64 +53,38 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto private val userStore: UserStore, private val fileUploader: FileUploader) : ProfileService { - override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { + override suspend fun getDisplayName(userId: String): Optional { val params = GetProfileInfoTask.Params(userId) - return getProfileInfoTask - .configureWith(params) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: JsonDict) { - val displayName = data[ProfileService.DISPLAY_NAME_KEY] as? String - matrixCallback.onSuccess(Optional.from(displayName)) - } - - override fun onFailure(failure: Throwable) { - matrixCallback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) + val data = getProfileInfoTask.execute(params) + val displayName = data[ProfileService.DISPLAY_NAME_KEY] as? String + return Optional.from(displayName) } - override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.io, matrixCallback) { + override suspend fun setDisplayName(userId: String, newDisplayName: String) { + withContext(coroutineDispatchers.io) { setDisplayNameTask.execute(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) userStore.updateDisplayName(userId, newDisplayName) } } - override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) { + override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) { + withContext(coroutineDispatchers.main) { val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg) setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) userStore.updateAvatar(userId, response.contentUri) } } - override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable { + override suspend fun getAvatarUrl(userId: String): Optional { val params = GetProfileInfoTask.Params(userId) - return getProfileInfoTask - .configureWith(params) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: JsonDict) { - val avatarUrl = data[ProfileService.AVATAR_URL_KEY] as? String - matrixCallback.onSuccess(Optional.from(avatarUrl)) - } - - override fun onFailure(failure: Throwable) { - matrixCallback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) + val data = getProfileInfoTask.execute(params) + val avatarUrl = data[ProfileService.AVATAR_URL_KEY] as? String + return Optional.from(avatarUrl) } - override fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable { + override suspend fun getProfile(userId: String): JsonDict { val params = GetProfileInfoTask.Params(userId) - return getProfileInfoTask - .configureWith(params) { - this.callback = matrixCallback - } - .executeBy(taskExecutor) + return getProfileInfoTask.execute(params) } override fun getThreePids(): List { @@ -154,70 +126,38 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto ) } - override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable { - return addThreePidTask - .configureWith(AddThreePidTask.Params(threePid)) { - callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun addThreePid(threePid: ThreePid) { + addThreePidTask.execute(AddThreePidTask.Params(threePid)) } - override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback): Cancelable { - return validateSmsCodeTask - .configureWith(ValidateSmsCodeTask.Params(threePid, code)) { - callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun submitSmsCode(threePid: ThreePid.Msisdn, code: String) { + validateSmsCodeTask.execute(ValidateSmsCodeTask.Params(threePid, code)) } - override fun finalizeAddingThreePid(threePid: ThreePid, - userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, - matrixCallback: MatrixCallback): Cancelable { - return finalizeAddingThreePidTask - .configureWith(FinalizeAddingThreePidTask.Params( + override suspend fun finalizeAddingThreePid(threePid: ThreePid, + userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { + finalizeAddingThreePidTask + .execute(FinalizeAddingThreePidTask.Params( threePid = threePid, userInteractiveAuthInterceptor = userInteractiveAuthInterceptor, userWantsToCancel = false - )) { - callback = alsoRefresh(matrixCallback) - } - .executeBy(taskExecutor) + )) + refreshThreePids() } - override fun cancelAddingThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable { - return finalizeAddingThreePidTask - .configureWith(FinalizeAddingThreePidTask.Params( + override suspend fun cancelAddingThreePid(threePid: ThreePid) { + finalizeAddingThreePidTask + .execute(FinalizeAddingThreePidTask.Params( threePid = threePid, userInteractiveAuthInterceptor = null, userWantsToCancel = true - )) { - callback = alsoRefresh(matrixCallback) - } - .executeBy(taskExecutor) + )) + refreshThreePids() } - /** - * Wrap the callback to fetch 3Pids from the server in case of success - */ - private fun alsoRefresh(callback: MatrixCallback): MatrixCallback { - return object : MatrixCallback { - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - - override fun onSuccess(data: Unit) { - refreshThreePids() - callback.onSuccess(data) - } - } - } - - override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable { - return deleteThreePidTask - .configureWith(DeleteThreePidTask.Params(threePid)) { - callback = alsoRefresh(matrixCallback) - } - .executeBy(taskExecutor) + override suspend fun deleteThreePid(threePid: ThreePid) { + deleteThreePidTask.execute(DeleteThreePidTask.Params(threePid)) + refreshThreePids() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt index 3549f3613f..7b7617aa80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt @@ -34,12 +34,12 @@ internal class DefaultDeleteThreePidTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver) : DeleteThreePidTask() { override suspend fun execute(params: Params) { - executeRequest(globalErrorReceiver) { - val body = DeleteThreePidBody( - medium = params.threePid.toMedium(), - address = params.threePid.value - ) - apiCall = profileAPI.deleteThreePid(body) + val body = DeleteThreePidBody( + medium = params.threePid.toMedium(), + address = params.threePid.value + ) + executeRequest(globalErrorReceiver) { + profileAPI.deleteThreePid(body) } // We do not really care about the result for the moment diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt index c2a38af093..5f063365e0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -61,13 +61,13 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor( ?: throw IllegalArgumentException("unknown threepid") try { - executeRequest(globalErrorReceiver) { + executeRequest(globalErrorReceiver) { val body = FinalizeAddThreePidBody( clientSecret = pendingThreePids.clientSecret, sid = pendingThreePids.sid, auth = params.userAuthParam?.asMap() ) - apiCall = profileAPI.finalizeAddThreePid(body) + profileAPI.finalizeAddThreePid(body) } true } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt index ed60c4a368..fed4288f84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt @@ -34,7 +34,7 @@ internal class DefaultGetProfileInfoTask @Inject constructor(private val profile override suspend fun execute(params: Params): JsonDict { return executeRequest(globalErrorReceiver) { - apiCall = profileAPI.getProfile(params.userId) + profileAPI.getProfile(params.userId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt index 7794f578b0..5113b821e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -21,7 +21,6 @@ import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.registration.SuccessResult import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -37,70 +36,70 @@ internal interface ProfileAPI { * @param userId the user id to fetch profile info */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") - fun getProfile(@Path("userId") userId: String): Call + suspend fun getProfile(@Path("userId") userId: String): JsonDict /** * List all 3PIDs linked to the Matrix user account. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid") - fun getThreePIDs(): Call + suspend fun getThreePIDs(): AccountThreePidsResponse /** * Change user display name */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname") - fun setDisplayName(@Path("userId") userId: String, - @Body body: SetDisplayNameBody): Call + suspend fun setDisplayName(@Path("userId") userId: String, + @Body body: SetDisplayNameBody) /** * Change user avatar url. */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url") - fun setAvatarUrl(@Path("userId") userId: String, - @Body body: SetAvatarUrlBody): Call + suspend fun setAvatarUrl(@Path("userId") userId: String, + @Body body: SetAvatarUrlBody) /** * Bind a threePid * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind */ @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind") - fun bindThreePid(@Body body: BindThreePidBody): Call + suspend fun bindThreePid(@Body body: BindThreePidBody) /** * Unbind a threePid * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-unbind */ @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind") - fun unbindThreePid(@Body body: UnbindThreePidBody): Call + suspend fun unbindThreePid(@Body body: UnbindThreePidBody): UnbindThreePidResponse /** * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken") - fun addEmail(@Body body: AddEmailBody): Call + suspend fun addEmail(@Body body: AddEmailBody): AddEmailResponse /** * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-msisdn-requesttoken */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken") - fun addMsisdn(@Body body: AddMsisdnBody): Call + suspend fun addMsisdn(@Body body: AddMsisdnBody): AddMsisdnResponse /** * Validate Msisdn code (same model than for Identity server API) */ @POST - fun validateMsisdn(@Url url: String, - @Body params: ValidationCodeBody): Call + suspend fun validateMsisdn(@Url url: String, + @Body params: ValidationCodeBody): SuccessResult /** * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add") - fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call + suspend fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody) /** * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete") - fun deleteThreePid(@Body body: DeleteThreePidBody): Call + suspend fun deleteThreePid(@Body body: DeleteThreePidBody): DeleteThreePidResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt index 552ad874ee..8a064b4fd1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt @@ -33,8 +33,8 @@ internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val p private val globalErrorReceiver: GlobalErrorReceiver) : RefreshUserThreePidsTask() { override suspend fun execute(params: Unit) { - val accountThreePidsResponse = executeRequest(globalErrorReceiver) { - apiCall = profileAPI.getThreePIDs() + val accountThreePidsResponse = executeRequest(globalErrorReceiver) { + profileAPI.getThreePIDs() } Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt index b29153d665..a7d116d919 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt @@ -33,11 +33,11 @@ internal class DefaultSetAvatarUrlTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver) : SetAvatarUrlTask() { override suspend fun execute(params: Params) { + val body = SetAvatarUrlBody( + avatarUrl = params.newAvatarUrl + ) return executeRequest(globalErrorReceiver) { - val body = SetAvatarUrlBody( - avatarUrl = params.newAvatarUrl - ) - apiCall = profileAPI.setAvatarUrl(params.userId, body) + profileAPI.setAvatarUrl(params.userId, body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt index 3f236bc589..61d3042310 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt @@ -33,11 +33,11 @@ internal class DefaultSetDisplayNameTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver) : SetDisplayNameTask() { override suspend fun execute(params: Params) { + val body = SetDisplayNameBody( + displayName = params.newDisplayName + ) return executeRequest(globalErrorReceiver) { - val body = SetDisplayNameBody( - displayName = params.newDisplayName - ) - apiCall = profileAPI.setDisplayName(params.userId, body) + profileAPI.setDisplayName(params.userId, body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt index 3439f6f840..df8a1c97ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt @@ -39,8 +39,8 @@ internal class DefaultUnbindThreePidsTask @Inject constructor(private val profil val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured - return executeRequest(globalErrorReceiver) { - apiCall = profileAPI.unbindThreePid( + return executeRequest(globalErrorReceiver) { + profileAPI.unbindThreePid( UnbindThreePidBody( identityServerUrlWithoutProtocol, params.threePid.toMedium(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt index efb6c6e836..c898fc6c5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.profile import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.internal.auth.registration.SuccessResult import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.di.SessionDatabase @@ -58,8 +57,8 @@ internal class DefaultValidateSmsCodeTask @Inject constructor( sid = pendingThreePids.sid, code = params.code ) - val result = executeRequest(globalErrorReceiver) { - apiCall = profileAPI.validateMsisdn(url, body) + val result = executeRequest(globalErrorReceiver) { + profileAPI.validateMsisdn(url, body) } if (!result.isSuccess()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt index d0f7cbfca3..c9d7ad2193 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt @@ -81,8 +81,8 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) } private suspend fun setPusher(pusher: JsonPusher) { - executeRequest(globalErrorReceiver) { - apiCall = pushersAPI.setPusher(pusher) + executeRequest(globalErrorReceiver) { + pushersAPI.setPusher(pusher) } monarchy.awaitTransaction { realm -> val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt index 03748b1528..b217687168 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt @@ -36,7 +36,7 @@ internal class DefaultAddPushRuleTask @Inject constructor( override suspend fun execute(params: AddPushRuleTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = pushRulesApi.addRule(params.kind.value, params.pushRule.ruleId, params.pushRule) + pushRulesApi.addRule(params.kind.value, params.pushRule.ruleId, params.pushRule) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index d290bb1a03..a772cf5ebb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -18,10 +18,8 @@ package org.matrix.android.sdk.internal.session.pushers import androidx.lifecycle.LiveData import androidx.work.BackoffPolicy import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushers.PushersService -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.PusherEntity import org.matrix.android.sdk.internal.database.query.where @@ -47,16 +45,11 @@ internal class DefaultPushersService @Inject constructor( private val taskExecutor: TaskExecutor ) : PushersService { - override fun testPush(url: String, - appId: String, - pushkey: String, - eventId: String, - callback: MatrixCallback): Cancelable { - return pushGatewayNotifyTask - .configureWith(PushGatewayNotifyTask.Params(url, appId, pushkey, eventId)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun testPush(url: String, + appId: String, + pushkey: String, + eventId: String) { + pushGatewayNotifyTask.execute(PushGatewayNotifyTask.Params(url, appId, pushkey, eventId)) } override fun refreshPushers() { @@ -102,14 +95,9 @@ internal class DefaultPushersService @Inject constructor( return request.id } - override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback): Cancelable { + override suspend fun removeHttpPusher(pushkey: String, appId: String) { val params = RemovePusherTask.Params(pushkey, appId) - return removePusherTask - .configureWith(params) { - this.callback = callback - } - // .enableRetry() ?? - .executeBy(taskExecutor) + removePusherTask.execute(params) } override fun getPushersLive(): LiveData> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt index 9fb2d51664..8cf861d285 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt @@ -15,7 +15,6 @@ */ package org.matrix.android.sdk.internal.session.pushers -import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -35,8 +34,8 @@ internal class DefaultGetPushRulesTask @Inject constructor( ) : GetPushRulesTask { override suspend fun execute(params: GetPushRulesTask.Params) { - val response = executeRequest(globalErrorReceiver) { - apiCall = pushRulesApi.getAllRules() + val response = executeRequest(globalErrorReceiver) { + pushRulesApi.getAllRules() } savePushRulesTask.execute(SavePushRulesTask.Params(response)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt index 125c8f0022..ba413a34db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt @@ -36,8 +36,8 @@ internal class DefaultGetPushersTask @Inject constructor( ) : GetPushersTask { override suspend fun execute(params: Unit) { - val response = executeRequest(globalErrorReceiver) { - apiCall = pushersAPI.getPushers() + val response = executeRequest(globalErrorReceiver) { + pushersAPI.getPushers() } monarchy.awaitTransaction { realm -> // clear existings? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt index cbcb7d2b37..daf9397ce8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.pushers import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -30,7 +29,7 @@ internal interface PushRulesApi { * Get all push rules */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/") - fun getAllRules(): Call + suspend fun getAllRules(): GetPushRulesResponse /** * Update the ruleID enable status @@ -40,10 +39,9 @@ internal interface PushRulesApi { * @param enable the new enable status */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled") - fun updateEnableRuleStatus(@Path("kind") kind: String, - @Path("ruleId") ruleId: String, - @Body enable: Boolean?) - : Call + suspend fun updateEnableRuleStatus(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body enable: Boolean?) /** * Update the ruleID action @@ -54,10 +52,9 @@ internal interface PushRulesApi { * @param actions the actions */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions") - fun updateRuleActions(@Path("kind") kind: String, - @Path("ruleId") ruleId: String, - @Body actions: Any) - : Call + suspend fun updateRuleActions(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body actions: Any) /** * Delete a rule @@ -66,9 +63,8 @@ internal interface PushRulesApi { * @param ruleId the ruleId */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") - fun deleteRule(@Path("kind") kind: String, - @Path("ruleId") ruleId: String) - : Call + suspend fun deleteRule(@Path("kind") kind: String, + @Path("ruleId") ruleId: String) /** * Add the ruleID enable status @@ -78,8 +74,7 @@ internal interface PushRulesApi { * @param rule the rule to add. */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") - fun addRule(@Path("kind") kind: String, - @Path("ruleId") ruleId: String, - @Body rule: PushRule) - : Call + suspend fun addRule(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body rule: PushRule) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt index ed4fb73e1b..0afea6996d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.pushers import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -29,7 +28,7 @@ internal interface PushersAPI { * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers") - fun getPushers(): Call + suspend fun getPushers(): GetPushersResponse /** * This endpoint allows the creation, modification and deletion of pushers for this user ID. @@ -38,5 +37,5 @@ internal interface PushersAPI { * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set") - fun setPusher(@Body jsonPusher: JsonPusher): Call + suspend fun setPusher(@Body jsonPusher: JsonPusher) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt index ff3122f566..23d0515f41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt @@ -36,7 +36,7 @@ internal class DefaultRemovePushRuleTask @Inject constructor( override suspend fun execute(params: RemovePushRuleTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId) + pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt index e3f4fdb789..3a2ebf40c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt @@ -62,8 +62,8 @@ internal class DefaultRemovePusherTask @Inject constructor( data = JsonPusherData(existing.data.url, existing.data.format), append = false ) - executeRequest(globalErrorReceiver) { - apiCall = pushersAPI.setPusher(deleteBody) + executeRequest(globalErrorReceiver) { + pushersAPI.setPusher(deleteBody) } monarchy.awaitTransaction { PusherEntity.where(it, params.pushKey).findFirst()?.deleteFromRealm() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt index a5c220e662..2a24aee892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt @@ -38,8 +38,8 @@ internal class DefaultUpdatePushRuleActionsTask @Inject constructor( override suspend fun execute(params: UpdatePushRuleActionsTask.Params) { if (params.oldPushRule.enabled != params.newPushRule.enabled) { // First change enabled state - executeRequest(globalErrorReceiver) { - apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled) + executeRequest(globalErrorReceiver) { + pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled) } } @@ -47,8 +47,8 @@ internal class DefaultUpdatePushRuleActionsTask @Inject constructor( // Also ensure the actions are up to date val body = mapOf("actions" to params.newPushRule.actions) - executeRequest(globalErrorReceiver) { - apiCall = pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body) + executeRequest(globalErrorReceiver) { + pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt index f36b5c55fb..9d7a46bede 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt @@ -35,7 +35,7 @@ internal class DefaultUpdatePushRuleEnableStatusTask @Inject constructor( override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.pushRule.ruleId, params.enabled) + pushRulesApi.updateEnableRuleStatus(params.kind.value, params.pushRule.ruleId, params.enabled) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayAPI.kt index d95587fc22..4333d6c7b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayAPI.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.pushers.gateway import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST @@ -27,5 +26,5 @@ internal interface PushGatewayAPI { * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#post-matrix-push-v1-notify */ @POST(NetworkConstants.URI_PUSH_GATEWAY_PREFIX_PATH + "notify") - fun notify(@Body body: PushGatewayNotifyBody): Call + suspend fun notify(@Body body: PushGatewayNotifyBody): PushGatewayNotifyResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayNotifyTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayNotifyTask.kt index df6f46fa81..316e221b32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayNotifyTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/gateway/PushGatewayNotifyTask.kt @@ -45,8 +45,8 @@ internal class DefaultPushGatewayNotifyTask @Inject constructor( ) .create(PushGatewayAPI::class.java) - val response = executeRequest(null) { - apiCall = sygnalApi.notify( + val response = executeRequest(null) { + sygnalApi.notify( PushGatewayNotifyBody( PushGatewayNotification( eventId = params.eventId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 8e817ec31a..1d8eb6c95e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.Room @@ -37,14 +36,11 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.search.SearchTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException import javax.inject.Inject @@ -66,7 +62,6 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val relationService: RelationService, private val roomMembersService: MembershipService, private val roomPushRuleService: RoomPushRuleService, - private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask, private val searchTask: SearchTask) : Room, @@ -133,16 +128,15 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, } } - override fun search(searchTerm: String, - nextBatch: String?, - orderByRecent: Boolean, - limit: Int, - beforeLimit: Int, - afterLimit: Int, - includeProfile: Boolean, - callback: MatrixCallback): Cancelable { - return searchTask - .configureWith(SearchTask.Params( + override suspend fun search(searchTerm: String, + nextBatch: String?, + orderByRecent: Boolean, + limit: Int, + beforeLimit: Int, + afterLimit: Int, + includeProfile: Boolean): SearchResult { + return searchTask.execute( + SearchTask.Params( searchTerm = searchTerm, roomId = roomId, nextBatch = nextBatch, @@ -151,8 +145,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, beforeLimit = beforeLimit, afterLimit = afterLimit, includeProfile = includeProfile - )) { - this.callback = callback - }.executeBy(taskExecutor) + ) + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 383dd876d3..bd63ba480e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -96,6 +99,20 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummariesLive(queryParams) } + override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config) + : LiveData> { + return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig) + } + + override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config) + : UpdatableFilterLivePageResult { + return roomSummaryDataSource.getFilteredPagedRoomSummariesLive(queryParams, pagedListConfig) + } + + override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { + return roomSummaryDataSource.getNotificationCountForRooms(queryParams) + } + override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { return roomSummaryDataSource.getBreadcrumbs(queryParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 60440c6359..c7e09e5954 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -53,8 +53,9 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import timber.log.Timber import javax.inject.Inject -internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String) - : EventInsertLiveProcessor { +internal class EventRelationsAggregationProcessor @Inject constructor( + @UserId private val userId: String +) : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.MESSAGE, @@ -87,12 +88,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") - handleReaction(event, roomId, realm, userId, isLocalEcho) + handleReaction(realm, event, roomId, isLocalEcho) } EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { - Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") - handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() ?.let { @@ -108,7 +109,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr handleReplace(realm, event, content, roomId, isLocalEcho) } else if (content?.relatesTo?.type == RelationType.RESPONSE) { Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, userId, event, content, roomId, isLocalEcho) + handleResponse(realm, event, content, roomId, isLocalEcho) } } @@ -122,7 +123,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr Timber.v("## SAS REF in room $roomId for event ${event.eventId}") event.content.toModel()?.relatesTo?.let { if (it.type == RelationType.REFERENCE && it.eventId != null) { - handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + handleVerification(realm, event, roomId, isLocalEcho, it.eventId) } } } @@ -140,7 +141,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } } } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { @@ -154,10 +155,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") encryptedEventContent.relatesTo.eventId?.let { - handleVerification(realm, event, roomId, isLocalEcho, it, userId) + handleVerification(realm, event, roomId, isLocalEcho, it) } } } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) { + // Reaction + if (event.getClearType() == EventType.REACTION) { + // we got a reaction!! + Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}") + handleReaction(realm, event, roomId, isLocalEcho) + } } } EventType.REDACTION -> { @@ -172,11 +180,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr // was this event a m.replace val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { - handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + handleRedactionOfReplace(realm, eventToPrune, contentModel.relatesTo!!.eventId!!) } } EventType.REACTION -> { - handleReactionRedact(eventToPrune, realm, userId) + handleReactionRedact(realm, eventToPrune) } } } @@ -267,7 +275,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr } private fun handleResponse(realm: Realm, - userId: String, event: Event, content: MessageContent, roomId: String, @@ -354,7 +361,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent()) } - private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + private fun handleInitialAggregatedRelations(realm: Realm, + event: Event, + roomId: String, + aggregation: AggregatedAnnotation) { if (SHOULD_HANDLE_SERVER_AGREGGATION) { aggregation.chunk?.forEach { if (it.type == EventType.REACTION) { @@ -376,7 +386,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr } } - private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + private fun handleReaction(realm: Realm, + event: Event, + roomId: String, + isLocalEcho: Boolean) { val content = event.content.toModel() if (content == null) { Timber.e("Malformed reaction content ${event.content}") @@ -441,7 +454,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr /** * Called when an event is deleted */ - private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + private fun handleRedactionOfReplace(realm: Realm, + redacted: EventEntity, + relatedEventId: String) { Timber.d("Handle redaction of m.replace") val eventSummary = EventAnnotationsSummaryEntity.where(realm, redacted.roomId, relatedEventId).findFirst() if (eventSummary == null) { @@ -457,7 +472,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr sourceToDiscard.deleteFromRealm() } - private fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + private fun handleReactionRedact(realm: Realm, + eventToPrune: EventEntity) { Timber.v("REDACTION of reaction ${eventToPrune.eventId}") // delete a reaction, need to update the annotation summary if any val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() ?: return @@ -494,7 +510,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr } } - private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { + private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String) { val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId) val verifSummary = eventSummary.referencesSummaryEntity diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 20cb49ee8a..6fee630510 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse import org.matrix.android.sdk.api.util.JsonDict @@ -37,7 +38,6 @@ import org.matrix.android.sdk.internal.session.room.tags.TagBody import org.matrix.android.sdk.internal.session.room.timeline.EventContextResponse import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse import org.matrix.android.sdk.internal.session.room.typing.TypingBody -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -56,9 +56,9 @@ internal interface RoomAPI { * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-publicrooms */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms") - fun publicRooms(@Query("server") server: String?, - @Body publicRoomsParams: PublicRoomsParams - ): Call + suspend fun publicRooms(@Query("server") server: String?, + @Body publicRoomsParams: PublicRoomsParams + ): PublicRoomsResponse /** * Create a room. @@ -70,7 +70,7 @@ internal interface RoomAPI { */ @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom") - fun createRoom(@Body param: CreateRoomBody): Call + suspend fun createRoom(@Body param: CreateRoomBody): CreateRoomResponse /** * Get a list of messages starting from a reference. @@ -82,12 +82,12 @@ internal interface RoomAPI { * @param filter A JSON RoomEventFilter to filter returned events with. Optional. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages") - fun getRoomMessagesFrom(@Path("roomId") roomId: String, - @Query("from") from: String, - @Query("dir") dir: String, - @Query("limit") limit: Int, - @Query("filter") filter: String? - ): Call + suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String, + @Query("from") from: String, + @Query("dir") dir: String, + @Query("limit") limit: Int, + @Query("filter") filter: String? + ): PaginationResponse /** * Get all members of a room @@ -98,11 +98,11 @@ internal interface RoomAPI { * @param notMembership to exclude one type of membership (optional) */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/members") - fun getMembers(@Path("roomId") roomId: String, - @Query("at") syncToken: String?, - @Query("membership") membership: String?, - @Query("not_membership") notMembership: String? - ): Call + suspend fun getMembers(@Path("roomId") roomId: String, + @Query("at") syncToken: String?, + @Query("membership") membership: Membership?, + @Query("not_membership") notMembership: Membership? + ): RoomMembersResponse /** * Send an event to a room. @@ -113,11 +113,11 @@ internal interface RoomAPI { * @param content the event content */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") - fun send(@Path("txId") txId: String, - @Path("roomId") roomId: String, - @Path("eventType") eventType: String, - @Body content: Content? - ): Call + suspend fun send(@Path("txId") txId: String, + @Path("roomId") roomId: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): SendResponse /** * Get the context surrounding an event. @@ -128,10 +128,10 @@ internal interface RoomAPI { * @param filter A JSON RoomEventFilter to filter returned events with. Optional. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}") - fun getContextOfEvent(@Path("roomId") roomId: String, - @Path("eventId") eventId: String, - @Query("limit") limit: Int, - @Query("filter") filter: String? = null): Call + suspend fun getContextOfEvent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Query("limit") limit: Int, + @Query("filter") filter: String? = null): EventContextResponse /** * Retrieve an event from its room id / events id @@ -140,8 +140,8 @@ internal interface RoomAPI { * @param eventId the event Id */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}") - fun getEvent(@Path("roomId") roomId: String, - @Path("eventId") eventId: String): Call + suspend fun getEvent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String): Event /** * Send read markers. @@ -150,8 +150,16 @@ internal interface RoomAPI { * @param markers the read markers */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") - fun sendReadMarker(@Path("roomId") roomId: String, - @Body markers: Map): Call + suspend fun sendReadMarker(@Path("roomId") roomId: String, + @Body markers: Map) + + /** + * Send receipt to a room + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/receipt/{receiptType}/{eventId}") + suspend fun sendReceipt(@Path("roomId") roomId: String, + @Path("receiptType") receiptType: String, + @Path("eventId") eventId: String) /** * Invite a user to the given room. @@ -161,8 +169,8 @@ internal interface RoomAPI { * @param body a object that just contains a user id */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") - fun invite(@Path("roomId") roomId: String, - @Body body: InviteBody): Call + suspend fun invite(@Path("roomId") roomId: String, + @Body body: InviteBody) /** * Invite a user to a room, using a ThreePid @@ -170,8 +178,8 @@ internal interface RoomAPI { * @param roomId Required. The room identifier (not alias) to which to invite the user. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") - fun invite3pid(@Path("roomId") roomId: String, - @Body body: ThreePidInviteBody): Call + suspend fun invite3pid(@Path("roomId") roomId: String, + @Body body: ThreePidInviteBody) /** * Send a generic state event @@ -181,9 +189,9 @@ internal interface RoomAPI { * @param params the request parameters */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}") - fun sendStateEvent(@Path("roomId") roomId: String, - @Path("state_event_type") stateEventType: String, - @Body params: JsonDict): Call + suspend fun sendStateEvent(@Path("roomId") roomId: String, + @Path("state_event_type") stateEventType: String, + @Body params: JsonDict) /** * Send a generic state event @@ -194,17 +202,17 @@ internal interface RoomAPI { * @param params the request parameters */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}") - fun sendStateEvent(@Path("roomId") roomId: String, - @Path("state_event_type") stateEventType: String, - @Path("state_key") stateKey: String, - @Body params: JsonDict): Call + suspend fun sendStateEvent(@Path("roomId") roomId: String, + @Path("state_event_type") stateEventType: String, + @Path("state_key") stateKey: String, + @Body params: JsonDict) /** * Get state events of a room * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") - fun getRoomState(@Path("roomId") roomId: String) : Call> + suspend fun getRoomState(@Path("roomId") roomId: String): List /** * Send a relation event to a room. @@ -215,12 +223,12 @@ internal interface RoomAPI { * @param content the event content */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") - fun sendRelation(@Path("roomId") roomId: String, - @Path("parent_id") parentId: String, - @Path("relation_type") relationType: String, - @Path("event_type") eventType: String, - @Body content: Content? - ): Call + suspend fun sendRelation(@Path("roomId") roomId: String, + @Path("parent_id") parentId: String, + @Path("relation_type") relationType: String, + @Path("event_type") eventType: String, + @Body content: Content? + ): SendResponse /** * Paginate relations for event based in normal topological order @@ -229,11 +237,11 @@ internal interface RoomAPI { * @param eventType filter for this event type */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") - fun getRelations(@Path("roomId") roomId: String, - @Path("eventId") eventId: String, - @Path("relationType") relationType: String, - @Path("eventType") eventType: String - ): Call + suspend fun getRelations(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Path("relationType") relationType: String, + @Path("eventType") eventType: String + ): RelationsResponse /** * Join the given room. @@ -243,9 +251,9 @@ internal interface RoomAPI { * @param params the request body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") - fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, - @Query("server_name") viaServers: List, - @Body params: Map): Call + suspend fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, + @Query("server_name") viaServers: List, + @Body params: Map): JoinRoomResponse /** * Leave the given room. @@ -254,8 +262,8 @@ internal interface RoomAPI { * @param params the request body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") - fun leave(@Path("roomId") roomId: String, - @Body params: Map): Call + suspend fun leave(@Path("roomId") roomId: String, + @Body params: Map) /** * Ban a user from the given room. @@ -264,8 +272,8 @@ internal interface RoomAPI { * @param userIdAndReason the banned user object (userId and reason for ban) */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban") - fun ban(@Path("roomId") roomId: String, - @Body userIdAndReason: UserIdAndReason): Call + suspend fun ban(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason) /** * unban a user from the given room. @@ -274,8 +282,8 @@ internal interface RoomAPI { * @param userIdAndReason the unbanned user object (userId and reason for unban) */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban") - fun unban(@Path("roomId") roomId: String, - @Body userIdAndReason: UserIdAndReason): Call + suspend fun unban(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason) /** * Kick a user from the given room. @@ -284,8 +292,8 @@ internal interface RoomAPI { * @param userIdAndReason the kicked user object (userId and reason for kicking) */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick") - fun kick(@Path("roomId") roomId: String, - @Body userIdAndReason: UserIdAndReason): Call + suspend fun kick(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason) /** * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. @@ -298,12 +306,12 @@ internal interface RoomAPI { * @param reason json containing reason key {"reason": "Indecent material"} */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") - fun redactEvent( + suspend fun redactEvent( @Path("txnId") txId: String, @Path("roomId") roomId: String, @Path("eventId") eventId: String, @Body reason: Map - ): Call + ): SendResponse /** * Reports an event as inappropriate to the server, which may then notify the appropriate people. @@ -313,24 +321,24 @@ internal interface RoomAPI { * @param body body containing score and reason */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}") - fun reportContent(@Path("roomId") roomId: String, - @Path("eventId") eventId: String, - @Body body: ReportContentBody): Call + suspend fun reportContent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Body body: ReportContentBody) /** * Get a list of aliases maintained by the local server for the given room. * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-aliases */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases") - fun getAliases(@Path("roomId") roomId: String): Call + suspend fun getAliases(@Path("roomId") roomId: String): GetAliasesResponse /** * Inform that the user is starting to type or has stopped typing */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}") - fun sendTypingState(@Path("roomId") roomId: String, - @Path("userId") userId: String, - @Body body: TypingBody): Call + suspend fun sendTypingState(@Path("roomId") roomId: String, + @Path("userId") userId: String, + @Body body: TypingBody) /** * Room tagging @@ -340,16 +348,16 @@ internal interface RoomAPI { * Add a tag to a room. */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") - fun putTag(@Path("userId") userId: String, - @Path("roomId") roomId: String, - @Path("tag") tag: String, - @Body body: TagBody): Call + suspend fun putTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String, + @Body body: TagBody) /** * Delete a tag from a room. */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") - fun deleteTag(@Path("userId") userId: String, - @Path("roomId") roomId: String, - @Path("tag") tag: String): Call + suspend fun deleteTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index 99f9d3644d..60ad83ee05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -16,18 +16,19 @@ package org.matrix.android.sdk.internal.session.room +import io.realm.Realm +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent -import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import io.realm.Realm -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.query.where import javax.inject.Inject internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { @@ -39,24 +40,35 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId * @return the room avatar url, can be a fallback to a room member avatar or null */ fun resolve(realm: Realm, roomId: String): String? { - var res: String? - val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")?.root - res = ContentMapper.map(roomName?.content).toModel()?.avatarUrl - if (!res.isNullOrEmpty()) { - return res + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "") + ?.root + ?.asDomain() + ?.content + ?.toModel() + ?.avatarUrl + if (!roomName.isNullOrEmpty()) { + return roomName } val roomMembers = RoomMemberHelper(realm, roomId) val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) - val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false + val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect.orFalse() + if (isDirectRoom) { if (members.size == 1) { - res = members.firstOrNull()?.avatarUrl + // Use avatar of a left user + val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent() + .findAll() + .firstOrNull { !it.avatarUrl.isNullOrEmpty() } + ?.avatarUrl + + return firstLeftAvatarUrl ?: members.firstOrNull()?.avatarUrl } else if (members.size == 2) { val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() - res = firstOtherMember?.avatarUrl + return firstOtherMember?.avatarUrl } } - return res + + return null } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 63370a1ad8..90640b4700 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -36,7 +36,6 @@ import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineServ import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService import org.matrix.android.sdk.internal.session.search.SearchTask -import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject internal interface RoomFactory { @@ -60,7 +59,6 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, - private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask, private val searchTask: SearchTask) : RoomFactory { @@ -84,7 +82,6 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: relationService = relationServiceFactory.create(roomId), roomMembersService = membershipServiceFactory.create(roomId), roomPushRuleService = roomPushRuleServiceFactory.create(roomId), - taskExecutor = taskExecutor, sendStateTask = sendStateTask, searchTask = searchTask ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 66b7272360..5133f72932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -79,9 +79,11 @@ import org.matrix.android.sdk.internal.session.room.tags.DefaultDeleteTagFromRoo import org.matrix.android.sdk.internal.session.room.tags.DeleteTagFromRoomTask import org.matrix.android.sdk.internal.session.room.timeline.DefaultFetchTokenAndPaginateTask import org.matrix.android.sdk.internal.session.room.timeline.DefaultGetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultGetEventTask import org.matrix.android.sdk.internal.session.room.timeline.DefaultPaginationTask import org.matrix.android.sdk.internal.session.room.timeline.FetchTokenAndPaginateTask import org.matrix.android.sdk.internal.session.room.timeline.GetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import org.matrix.android.sdk.internal.session.room.timeline.PaginationTask import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask @@ -228,4 +230,7 @@ internal abstract class RoomModule { @Binds abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask + + @Binds + abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt index 9e4ec6f777..97ea1d6ad1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt @@ -45,8 +45,8 @@ internal class DefaultAddRoomAliasTask @Inject constructor( override suspend fun execute(params: AddRoomAliasTask.Params) { aliasAvailabilityChecker.check(params.aliasLocalPart) - executeRequest(globalErrorReceiver) { - apiCall = directoryAPI.addRoomAlias( + executeRequest(globalErrorReceiver) { + directoryAPI.addRoomAlias( roomAlias = params.aliasLocalPart.toFullLocalAlias(userId), body = AddRoomAliasBody( roomId = params.roomId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt index 6ad3db90a9..01ac3fcec8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt @@ -34,8 +34,8 @@ internal class DefaultDeleteRoomAliasTask @Inject constructor( ) : DeleteRoomAliasTask { override suspend fun execute(params: DeleteRoomAliasTask.Params) { - executeRequest(globalErrorReceiver) { - apiCall = directoryAPI.deleteRoomAlias( + executeRequest(globalErrorReceiver) { + directoryAPI.deleteRoomAlias( roomAlias = params.roomAlias ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt index a53ffc4fcd..71c8c9cd38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -52,8 +52,8 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor( Optional.from(null) } else { val description = tryOrNull("## Failed to get roomId from alias") { - executeRequest(globalErrorReceiver) { - apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias) + executeRequest(globalErrorReceiver) { + directoryAPI.getRoomIdByAlias(params.roomAlias) } } Optional.from(description) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt index 202cb1f6de..1ff4156ed3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt @@ -35,8 +35,8 @@ internal class DefaultGetRoomLocalAliasesTask @Inject constructor( override suspend fun execute(params: GetRoomLocalAliasesTask.Params): List { // We do not check for "org.matrix.msc2432", so the API may be missing - val response = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getAliases(roomId = params.roomId) + val response = executeRequest(globalErrorReceiver) { + roomAPI.getAliases(roomId = params.roomId) } return response.aliases diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt index 51a849a35e..9faf50dd8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -41,8 +41,8 @@ internal class RoomAliasAvailabilityChecker @Inject constructor( // Check alias availability val fullAlias = aliasLocalPart.toFullLocalAlias(userId) try { - executeRequest(globalErrorReceiver) { - apiCall = directoryAPI.getRoomIdByAlias(fullAlias) + executeRequest(globalErrorReceiver) { + directoryAPI.getRoomIdByAlias(fullAlias) } } catch (throwable: Throwable) { if (throwable is Failure.ServerError && throwable.httpCode == 404) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 9c16bd1b0f..bafe2b90ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -17,18 +17,19 @@ package org.matrix.android.sdk.internal.session.room.create import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -75,8 +76,8 @@ internal class DefaultCreateRoomTask @Inject constructor( val createRoomBody = createRoomBodyBuilder.build(params) val createRoomResponse = try { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.createRoom(createRoomBody) + executeRequest(globalErrorReceiver) { + roomAPI.createRoom(createRoomBody) } } catch (throwable: Throwable) { if (throwable is Failure.ServerError) { @@ -96,12 +97,18 @@ internal class DefaultCreateRoomTask @Inject constructor( // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout } + + Realm.getInstance(realmConfiguration).executeTransactionAsync { + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + } + if (otherUserId != null) { handleDirectChatCreation(roomId, otherUserId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt index edd8ae9b0d..4a6b0703c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt @@ -38,7 +38,7 @@ internal class DefaultGetPublicRoomTask @Inject constructor( override suspend fun execute(params: GetPublicRoomTask.Params): PublicRoomsResponse { return executeRequest(globalErrorReceiver) { - apiCall = roomAPI.publicRooms(params.server, params.publicRoomsParams) + roomAPI.publicRooms(params.server, params.publicRoomsParams) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt index 8d71001ef9..77492e429f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.directory.DirectoryAPI -import org.matrix.android.sdk.internal.session.directory.RoomDirectoryVisibilityJson import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -36,8 +35,8 @@ internal class DefaultGetRoomDirectoryVisibilityTask @Inject constructor( ) : GetRoomDirectoryVisibilityTask { override suspend fun execute(params: GetRoomDirectoryVisibilityTask.Params): RoomDirectoryVisibility { - return executeRequest(globalErrorReceiver) { - apiCall = directoryAPI.getRoomDirectoryVisibility(params.roomId) + return executeRequest(globalErrorReceiver) { + directoryAPI.getRoomDirectoryVisibility(params.roomId) } .visibility } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt index cbb0b6d5d1..f46d06bd5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt @@ -37,8 +37,8 @@ internal class DefaultSetRoomDirectoryVisibilityTask @Inject constructor( ) : SetRoomDirectoryVisibilityTask { override suspend fun execute(params: SetRoomDirectoryVisibilityTask.Params) { - executeRequest(globalErrorReceiver) { - apiCall = directoryAPI.setRoomDirectoryVisibility( + executeRequest(globalErrorReceiver) { + directoryAPI.setRoomDirectoryVisibility( params.roomId, RoomDirectoryVisibilityJson(visibility = params.roomDirectoryVisibility) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt index cd1c9bbbdd..41e891f78e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -21,13 +21,11 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields @@ -39,8 +37,6 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTas import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.fetchCopied import io.realm.Realm import io.realm.RealmQuery @@ -48,7 +44,6 @@ import io.realm.RealmQuery internal class DefaultMembershipService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, private val inviteThreePidTask: InviteThreePidTask, @@ -64,13 +59,9 @@ internal class DefaultMembershipService @AssistedInject constructor( fun create(roomId: String): DefaultMembershipService } - override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { + override suspend fun loadRoomMembersIfNeeded() { val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) - return loadRoomMembersTask - .configureWith(params) { - this.callback = matrixCallback - } - .executeBy(taskExecutor) + loadRoomMembersTask.execute(params) } override fun getRoomMember(userId: String): RoomMemberSummary? { @@ -120,66 +111,38 @@ internal class DefaultMembershipService @AssistedInject constructor( } } - override fun ban(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + override suspend fun ban(userId: String, reason: String?) { val params = MembershipAdminTask.Params(MembershipAdminTask.Type.BAN, roomId, userId, reason) - return membershipAdminTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + membershipAdminTask.execute(params) } - override fun unban(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + override suspend fun unban(userId: String, reason: String?) { val params = MembershipAdminTask.Params(MembershipAdminTask.Type.UNBAN, roomId, userId, reason) - return membershipAdminTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + membershipAdminTask.execute(params) } - override fun kick(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + override suspend fun kick(userId: String, reason: String?) { val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason) - return membershipAdminTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + membershipAdminTask.execute(params) } - override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + override suspend fun invite(userId: String, reason: String?) { val params = InviteTask.Params(roomId, userId, reason) - return inviteTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + inviteTask.execute(params) } - override fun invite3pid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + override suspend fun invite3pid(threePid: ThreePid) { val params = InviteThreePidTask.Params(roomId, threePid) - return inviteThreePidTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return inviteThreePidTask.execute(params) } - override fun join(reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { + override suspend fun join(reason: String?, viaServers: List) { val params = JoinRoomTask.Params(roomId, reason, viaServers) - return joinTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + joinTask.execute(params) } - override fun leave(reason: String?, callback: MatrixCallback): Cancelable { + override suspend fun leave(reason: String?) { val params = LeaveRoomTask.Params(roomId, reason) - return leaveRoomTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + leaveRoomTask.execute(params) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index cc491d1cd9..3d0f51b831 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -90,8 +90,8 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( val lastToken = syncTokenStore.getLastToken() val response = try { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) + executeRequest(globalErrorReceiver) { + roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership) } } catch (throwable: Throwable) { // Revert status to NONE diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 0e18e30b13..3aa812d93d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership import io.realm.Realm import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership @@ -51,14 +52,14 @@ internal class RoomDisplayNameResolver @Inject constructor( * @param roomId: the roomId to resolve the name of. * @return the room display name */ - fun resolve(realm: Realm, roomId: String): CharSequence { + fun resolve(realm: Realm, roomId: String): String { // this algorithm is the one defined in // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 // calculateRoomName(room, userId) // For Lazy Loaded room, see algorithm here: // https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn - var name: CharSequence? + var name: String? val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root name = ContentMapper.map(roomName?.content).toModel()?.name @@ -77,14 +78,14 @@ internal class RoomDisplayNameResolver @Inject constructor( if (roomEntity?.membership == Membership.INVITE) { val inviteMeEvent = roomMembers.getLastStateEvent(userId) val inviterId = inviteMeEvent?.sender - name = if (inviterId != null) { - activeMembers.where() - .equalTo(RoomMemberSummaryEntityFields.USER_ID, inviterId) - .findFirst() - ?.displayName - } else { - roomDisplayNameFallbackProvider.getNameForRoomInvite() - } + name = inviterId + ?.let { + activeMembers.where() + .equalTo(RoomMemberSummaryEntityFields.USER_ID, it) + .findFirst() + ?.getBestName() + } + ?: roomDisplayNameFallbackProvider.getNameForRoomInvite() } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val invitedCount = roomSummary?.invitedMembersCount ?: 0 @@ -105,10 +106,17 @@ internal class RoomDisplayNameResolver @Inject constructor( val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { 0 -> { - roomDisplayNameFallbackProvider.getNameForEmptyRoom() - // TODO (was xx and yyy) ... + // Get left members if any + val leftMembersNames = roomMembers.queryLeftRoomMembersEvent() + .findAll() + .map { it.getBestName() } + roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames) + } + 1 -> { + roomDisplayNameFallbackProvider.getNameFor1member( + resolveRoomMemberName(otherMembersSubset[0], roomMembers) + ) } - 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 2 -> { roomDisplayNameFallbackProvider.getNameFor2members( resolveRoomMemberName(otherMembersSubset[0], roomMembers), @@ -145,12 +153,11 @@ internal class RoomDisplayNameResolver @Inject constructor( } /** See [org.matrix.android.sdk.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */ - private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, - roomMemberHelper: RoomMemberHelper): String? { - if (roomMemberSummary == null) return null + private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity, + roomMemberHelper: RoomMemberHelper): String { val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) return if (isUnique) { - roomMemberSummary.displayName + roomMemberSummary.getBestName() } else { "${roomMemberSummary.displayName} (${roomMemberSummary.userId})" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt index 89fe2901c0..2ecacf335b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.session.room.membership +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.user.UserEntityFactory -import io.realm.Realm import javax.inject.Inject internal class RoomMemberEventHandler @Inject constructor() { @@ -31,7 +31,7 @@ internal class RoomMemberEventHandler @Inject constructor() { return false } val userId = event.stateKey ?: return false - val roomMember = event.content.toModel() + val roomMember = event.getFixedRoomMemberContent() return handle(realm, roomId, userId, roomMember) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt index 2a7c46bd42..9ce8db25a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt @@ -75,6 +75,11 @@ internal class RoomMemberHelper(private val realm: Realm, .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) } + fun queryLeftRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.LEAVE.name) + } + fun queryActiveRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() .beginGroup() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt index 4654a28536..d2c21f3520 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt @@ -41,8 +41,8 @@ internal class DefaultMembershipAdminTask @Inject constructor(private val roomAP override suspend fun execute(params: MembershipAdminTask.Params) { val userIdAndReason = UserIdAndReason(params.userId, params.reason) - executeRequest(null) { - apiCall = when (params.type) { + executeRequest(null) { + when (params.type) { MembershipAdminTask.Type.BAN -> roomAPI.ban(params.roomId, userIdAndReason) MembershipAdminTask.Type.UNBAN -> roomAPI.unban(params.roomId, userIdAndReason) MembershipAdminTask.Type.KICK -> roomAPI.kick(params.roomId, userIdAndReason) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt index 05503bd643..7e7b80baaf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt @@ -36,11 +36,13 @@ internal class DefaultInviteTask @Inject constructor( ) : InviteTask { override suspend fun execute(params: InviteTask.Params) { - return executeRequest(globalErrorReceiver) { - val body = InviteBody(params.userId, params.reason) - apiCall = roomAPI.invite(params.roomId, body) - isRetryable = true - maxRetryCount = 3 + val body = InviteBody(params.userId, params.reason) + return executeRequest( + globalErrorReceiver, + canRetry = true, + maxRetriesCount = 3 + ) { + roomAPI.invite(params.roomId, body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index 3b7639d42f..33776e4f6e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -16,21 +16,23 @@ package org.matrix.android.sdk.internal.session.room.membership.joining +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.task.Task -import io.realm.RealmConfiguration -import kotlinx.coroutines.TimeoutCancellationException -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -54,8 +56,8 @@ internal class DefaultJoinRoomTask @Inject constructor( override suspend fun execute(params: JoinRoomTask.Params) { roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining) val joinRoomResponse = try { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.join( + executeRequest(globalErrorReceiver) { + roomAPI.join( roomIdOrAlias = params.roomIdOrAlias, viaServers = params.viaServers.take(3), params = mapOf("reason" to params.reason) @@ -69,12 +71,18 @@ internal class DefaultJoinRoomTask @Inject constructor( val roomId = joinRoomResponse.roomId try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { throw JoinRoomFailure.JoinedWithTimeout } + + Realm.getInstance(realmConfiguration).executeTransactionAsync { + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + } + setReadMarkers(roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt index 37bb7570d1..1b836e36a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -68,8 +68,8 @@ internal class DefaultLeaveRoomTask @Inject constructor( leaveRoom(predecessorRoomId, reason) } try { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.leave(roomId, mapOf("reason" to reason)) + executeRequest(globalErrorReceiver) { + roomAPI.leave(roomId, mapOf("reason" to reason)) } } catch (failure: Throwable) { roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt index d237ec795e..fa0a2d608a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt @@ -59,7 +59,7 @@ internal class DefaultInviteThreePidTask @Inject constructor( medium = params.threePid.toMedium(), address = params.threePid.value ) - apiCall = roomAPI.invite3pid(params.roomId, body) + roomAPI.invite3pid(params.roomId, body) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt index dbec6b555c..64cbef23ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt @@ -36,7 +36,7 @@ internal class DefaultResolveRoomStateTask @Inject constructor( override suspend fun execute(params: ResolveRoomStateTask.Params): List { return executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getRoomState(params.roomId) + roomAPI.getRoomState(params.roomId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 3cf8cfe5ee..d4d03dca05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -22,7 +22,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.util.Optional @@ -36,7 +35,6 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultReadService @AssistedInject constructor( @Assisted private val roomId: String, @@ -52,35 +50,23 @@ internal class DefaultReadService @AssistedInject constructor( fun create(roomId: String): DefaultReadService } - override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback) { + override suspend fun markAsRead(params: ReadService.MarkAsReadParams) { val taskParams = SetReadMarkersTask.Params( roomId = roomId, forceReadMarker = params.forceReadMarker(), forceReadReceipt = params.forceReadReceipt() ) - setReadMarkersTask - .configureWith(taskParams) { - this.callback = callback - } - .executeBy(taskExecutor) + setReadMarkersTask.execute(taskParams) } - override fun setReadReceipt(eventId: String, callback: MatrixCallback) { + override suspend fun setReadReceipt(eventId: String) { val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) - setReadMarkersTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + setReadMarkersTask.execute(params) } - override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) { + override suspend fun setReadMarker(fullyReadEventId: String) { val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null) - setReadMarkersTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + setReadMarkersTask.execute(params) } override fun isEventRead(eventId: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt index 54d2307dd4..e4147d55b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -62,7 +62,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { - val markers = HashMap() + val markers = mutableMapOf() Timber.v("Execute set read marker with params: $params") val latestSyncedEventId = latestSyncedEventId(params.roomId) val fullyReadEventId = if (params.forceReadMarker) { @@ -96,9 +96,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor( updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) } if (markers.isNotEmpty()) { - executeRequest(globalErrorReceiver) { - isRetryable = true - apiCall = roomAPI.sendReadMarker(params.roomId, markers) + executeRequest( + globalErrorReceiver, + canRetry = true + ) { + if (markers[READ_MARKER] == null) { + if (readReceiptEventId != null) { + roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId) + } + } else { + // "m.fully_read" value is mandatory to make this call + roomAPI.sendReadMarker(params.roomId, markers) + } } } } @@ -108,7 +117,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId } - private suspend fun updateDatabase(roomId: String, markers: HashMap, shouldUpdateRoomSummary: Boolean) { + private suspend fun updateDatabase(roomId: String, markers: Map, shouldUpdateRoomSummary: Boolean) { monarchy.awaitTransaction { realm -> val readMarkerId = markers[READ_MARKER] val readReceiptId = markers[READ_RECEIPT] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt index f9fd5f9348..5f5c000171 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -40,8 +40,8 @@ internal class DefaultFetchEditHistoryTask @Inject constructor( override suspend fun execute(params: FetchEditHistoryTask.Params): List { val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) - val response = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getRelations( + val response = executeRequest(globalErrorReceiver) { + roomAPI.getRelations( roomId = params.roomId, eventId = params.eventId, relationType = RelationType.REPLACE, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt index 403aa274fe..5d0879d706 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import javax.inject.Inject @@ -84,8 +83,8 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) } private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.sendRelation( + executeRequest(globalErrorReceiver) { + roomAPI.sendRelation( roomId = roomId, parentId = relatedEventId, relationType = relationType, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt index 9c6e9907a4..29d507dfc5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt @@ -38,7 +38,7 @@ internal class DefaultReportContentTask @Inject constructor( override suspend fun execute(params: ReportContentTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) + roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index c901c7e18e..306f865408 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -55,8 +55,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) override suspend fun doSafeWork(params: Params): Result { val eventId = params.eventId return runCatching { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.redactEvent( + executeRequest(globalErrorReceiver) { + roomAPI.redactEvent( params.txID, params.roomId, eventId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index c1fc2fd9fe..d55dce57af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -91,7 +91,7 @@ internal class SendEventWorker(context: Context, if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED) - return Result.success() + Result.success() } else { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") Result.retry() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 2972332989..a5c09f5ff6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.failure.getRetryDelay import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable @@ -148,8 +149,7 @@ internal class EventSenderProcessorCoroutine @Inject constructor( task.markAsFailedOrRetry(exception, 0) } (exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { - val delay = exception.error.retryAfterMillis?.plus(100) ?: 3_000 - task.markAsFailedOrRetry(exception, delay) + task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000)) } exception is CancellationException -> { Timber.v("## $task has been cancelled, try next task") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index f2640fd1e7..615bc99096 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -27,10 +27,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent -import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules -import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes @@ -131,14 +129,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private if (joinRules != null) { sendStateEvent( eventType = EventType.STATE_ROOM_JOIN_RULES, - body = RoomJoinRulesContent(joinRules).toContent(), + body = mapOf("join_rule" to joinRules), stateKey = null ) } if (guestAccess != null) { sendStateEvent( eventType = EventType.STATE_ROOM_GUEST_ACCESS, - body = RoomGuestAccessContent(guestAccess).toContent(), + body = mapOf("guest_access" to guestAccess), stateKey = null ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt index 63691d9207..998e116a0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt @@ -39,7 +39,7 @@ internal class DefaultSendStateTask @Inject constructor( override suspend fun execute(params: SendStateTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = if (params.stateKey == null) { + if (params.stateKey == null) { roomAPI.sendStateEvent( roomId = params.roomId, stateEventType = params.eventType, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 107055b8c3..dd3fbe04b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -18,10 +18,18 @@ package org.matrix.android.sdk.internal.session.room.summary import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.Sort +import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper @@ -32,8 +40,6 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.util.fetchCopyMap -import io.realm.Realm -import io.realm.RealmQuery import javax.inject.Inject internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, @@ -98,6 +104,62 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) } + fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + roomSummariesQuery(realm, queryParams) + .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + } + val dataSourceFactory = realmDataSourceFactory.map { + roomSummaryMapper.map(it) + } + return monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, pagedListConfig) + ) + } + + fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config): UpdatableFilterLivePageResult { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + roomSummariesQuery(realm, queryParams) + .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + } + val dataSourceFactory = realmDataSourceFactory.map { + roomSummaryMapper.map(it) + } + + val mapped = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, pagedListConfig) + ) + + return object : UpdatableFilterLivePageResult { + override val livePagedList: LiveData> = mapped + + override fun updateQuery(queryParams: RoomSummaryQueryParams) { + realmDataSourceFactory.updateQuery { + roomSummariesQuery(it, queryParams) + .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + } + } + } + } + + fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { + var notificationCount: RoomAggregateNotificationCount? = null + monarchy.doWithRealm { realm -> + val roomSummariesQuery = roomSummariesQuery(realm, queryParams) + val notifCount = roomSummariesQuery.sum(RoomSummaryEntityFields.NOTIFICATION_COUNT).toInt() + val highlightCount = roomSummariesQuery.sum(RoomSummaryEntityFields.HIGHLIGHT_COUNT).toInt() + notificationCount = RoomAggregateNotificationCount( + notifCount, + highlightCount + ) + } + return notificationCount!! + } + private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { val query = RoomSummaryEntity.where(realm) query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) @@ -105,6 +167,28 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + + queryParams.roomCategoryFilter?.let { + when (it) { + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) + RoomCategoryFilter.ALL -> { + // nop + } + } + } + queryParams.roomTagQueryFilter?.let { + it.isFavorite?.let { fav -> + query.equalTo(RoomSummaryEntityFields.IS_FAVOURITE, fav) + } + it.isLowPriority?.let { lp -> + query.equalTo(RoomSummaryEntityFields.IS_LOW_PRIORITY, lp) + } + it.isServerNotice?.let { sn -> + query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, sn) + } + } return query } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index cd1bb69612..7913bf71a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -98,11 +98,16 @@ internal class RoomSummaryUpdater @Inject constructor( val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs + if (lastActivityFromEvent != null) { + roomSummaryEntity.lastActivityTime = lastActivityFromEvent + } + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 // avoid this call if we are sure there are unread events || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) - roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId).toString() + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel()?.name roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic @@ -112,9 +117,7 @@ internal class RoomSummaryUpdater @Inject constructor( val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases .orEmpty() - roomSummaryEntity.aliases.clear() - roomSummaryEntity.aliases.addAll(roomAliases) - roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.updateAliases(roomAliases) roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt index c3b5c3f78f..3e82d674ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt @@ -39,8 +39,8 @@ internal class DefaultAddTagToRoomTask @Inject constructor( ) : AddTagToRoomTask { override suspend fun execute(params: AddTagToRoomTask.Params) { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.putTag( + executeRequest(globalErrorReceiver) { + roomAPI.putTag( userId = userId, roomId = params.roomId, tag = params.tag, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt index d578d21fde..ae2a050659 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt @@ -38,8 +38,8 @@ internal class DefaultDeleteTagFromRoomTask @Inject constructor( ) : DeleteTagFromRoomTask { override suspend fun execute(params: DeleteTagFromRoomTask.Params) { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.deleteTag( + executeRequest(globalErrorReceiver) { + roomAPI.deleteTag( userId = userId, roomId = params.roomId, tag = params.tag diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 61f770b956..e230599f8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -70,14 +69,12 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val timelineInput: TimelineInput, private val eventDecryptor: TimelineEventDecryptor, private val realmSessionProvider: RealmSessionProvider, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler ) : Timeline, - TimelineHiddenReadReceipts.Delegate, TimelineInput.Listener, UIEchoManager.Listener { @@ -93,8 +90,7 @@ internal class DefaultTimeline( private val cancelableBag = CancelableBag() private val debouncer = Debouncer(mainHandler) - private lateinit var nonFilteredEvents: RealmResults - private lateinit var filteredEvents: RealmResults + private lateinit var timelineEvents: RealmResults private lateinit var sendingEvents: RealmResults private var prevDisplayIndex: Int? = null @@ -168,16 +164,9 @@ internal class DefaultTimeline( postSnapshot() } - nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() - filteredEvents = nonFilteredEvents.where() - .filterEventsWithSettings(settings) - .findAll() - nonFilteredEvents.addChangeListener(eventsChangeListener) + timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + timelineEvents.addChangeListener(eventsChangeListener) handleInitialLoad() - if (settings.shouldHandleHiddenReadReceipts()) { - hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) - } - loadRoomMembersTask .configureWith(LoadRoomMembersTask.Params(roomId)) { this.callback = NoOpMatrixCallback() @@ -205,10 +194,6 @@ internal class DefaultTimeline( } } - private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { - return buildReadReceipts && (filters.filterEdits || filters.filterTypes) - } - override fun dispose() { if (isStarted.compareAndSet(true, false)) { isReady.set(false) @@ -220,11 +205,8 @@ internal class DefaultTimeline( if (this::sendingEvents.isInitialized) { sendingEvents.removeAllChangeListeners() } - if (this::nonFilteredEvents.isInitialized) { - nonFilteredEvents.removeAllChangeListeners() - } - if (settings.shouldHandleHiddenReadReceipts()) { - hiddenReadReceipts.dispose() + if (this::timelineEvents.isInitialized) { + timelineEvents.removeAllChangeListeners() } clearAllValues() backgroundRealm.getAndSet(null).also { @@ -256,48 +238,6 @@ internal class DefaultTimeline( } } - override fun getFirstDisplayableEventId(eventId: String): String? { - // If the item is built, the id is obviously displayable - val builtIndex = builtEventsIdMap[eventId] - if (builtIndex != null) { - return eventId - } - // Otherwise, we should check if the event is in the db, but is hidden because of filters - return realmSessionProvider.withRealm { localRealm -> - val nonFilteredEvents = buildEventQuery(localRealm) - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - .findAll() - - val nonFilteredEvent = nonFilteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) - .findFirst() - - val filteredEvents = nonFilteredEvents.where() - .filterEventsWithSettings(settings) - .findAll() - val isEventInDb = nonFilteredEvent != null - - val isHidden = isEventInDb && filteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) - .findFirst() == null - - if (isHidden) { - val displayIndex = nonFilteredEvent?.displayIndex - if (displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) - .findFirst() - firstDisplayedEvent?.eventId - } else { - null - } - } else { - null - } - } - } - override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { return hasMoreInCache(direction) || !hasReachedEnd(direction) } @@ -319,18 +259,6 @@ internal class DefaultTimeline( listeners.clear() } -// TimelineHiddenReadReceipts.Delegate - - override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { - return rebuildEvent(eventId) { te -> - te.copy(readReceipts = readReceipts) - } - } - - override fun onReadReceiptsUpdated() { - postSnapshot() - } - override fun onNewTimelineEvents(roomId: String, eventIds: List) { if (isLive && this.roomId == roomId) { listeners.forEach { @@ -341,18 +269,13 @@ internal class DefaultTimeline( override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { if (roomId != this.roomId || !isLive) return - - val postSnapShot = uiEchoManager.onLocalEchoCreated(timelineEvent) - - if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) { - listeners.forEach { + uiEchoManager.onLocalEchoCreated(timelineEvent) + listeners.forEach { + tryOrNull { it.onNewTimelineEvents(listOf(timelineEvent.eventId)) } } - - if (postSnapShot) { - postSnapshot() - } + postSnapshot() } override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { @@ -439,23 +362,21 @@ internal class DefaultTimeline( val builtSendingEvents = mutableListOf() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { uiEchoManager.getInMemorySendingEvents() - .filterSendingEventsTo(builtSendingEvents) + .updateWithUiEchoInto(builtSendingEvents) sendingEvents .filter { timelineEvent -> builtSendingEvents.none { it.eventId == timelineEvent.eventId } } .map { timelineEventMapper.map(it) } - .filterSendingEventsTo(builtSendingEvents) + .updateWithUiEchoInto(builtSendingEvents) } return builtSendingEvents } - private fun List.filterSendingEventsTo(target: MutableList) { + private fun List.updateWithUiEchoInto(target: MutableList) { target.addAll( - // Filter out sending event that are not displayable! - filterEventsWithSettings(settings) - // Get most up to date send state (in memory) - .map { uiEchoManager.updateSentStateWithUiEcho(it) } + // Get most up to date send state (in memory) + map { uiEchoManager.updateSentStateWithUiEcho(it) } ) } @@ -487,9 +408,9 @@ internal class DefaultTimeline( var shouldFetchInitialEvent = false val currentInitialEventId = initialEventId val initialDisplayIndex = if (currentInitialEventId == null) { - nonFilteredEvents.firstOrNull()?.displayIndex + timelineEvents.firstOrNull()?.displayIndex } else { - val initialEvent = nonFilteredEvents.where() + val initialEvent = timelineEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) .findFirst() @@ -501,7 +422,7 @@ internal class DefaultTimeline( if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(currentInitialEventId) } else { - val count = filteredEvents.size.coerceAtMost(settings.initialSize) + val count = timelineEvents.size.coerceAtMost(settings.initialSize) if (initialEventId == null) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { @@ -541,8 +462,7 @@ internal class DefaultTimeline( val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> postSnapshot = rebuildEvent(eventId) { - val builtEvent = buildTimelineEvent(eventEntity) - listOf(builtEvent).filterEventsWithSettings(settings).firstOrNull() + buildTimelineEvent(eventEntity) } || postSnapshot } } @@ -563,9 +483,9 @@ internal class DefaultTimeline( // We are in the case where event exists, but we do not know the token. // Fetch (again) the last event to get a token val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { - nonFilteredEvents.firstOrNull()?.eventId + timelineEvents.firstOrNull()?.eventId } else { - nonFilteredEvents.lastOrNull()?.eventId + timelineEvents.lastOrNull()?.eventId } if (lastKnownEventId == null) { updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } @@ -636,7 +556,7 @@ internal class DefaultTimeline( * Return the current Chunk */ private fun getLiveChunk(): ChunkEntity? { - return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() + return timelineEvents.firstOrNull()?.chunk?.firstOrNull() } /** @@ -680,17 +600,18 @@ internal class DefaultTimeline( val time = System.currentTimeMillis() - start Timber.v("Built ${offsetResults.size} items from db in $time ms") // For the case where wo reach the lastForward chunk - updateLoadingStates(filteredEvents) + updateLoadingStates(timelineEvents) return offsetResults.size } - private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( - timelineEventEntity = eventEntity, - buildReadReceipts = settings.buildReadReceipts, - correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) - ).let { - // eventually enhance with ui echo? - (uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it) + private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { + return timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = settings.buildReadReceipts + ).let { timelineEvent -> + // eventually enhance with ui echo? + uiEchoManager.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent + } } /** @@ -699,7 +620,7 @@ internal class DefaultTimeline( private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, count: Long): RealmResults { - val offsetQuery = filteredEvents.where() + val offsetQuery = timelineEvents.where() if (direction == Timeline.Direction.BACKWARDS) { offsetQuery .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) @@ -747,7 +668,7 @@ internal class DefaultTimeline( if (isReady.get().not()) { return@post } - updateLoadingStates(filteredEvents) + updateLoadingStates(timelineEvents) val snapshot = createSnapshot() val runnable = Runnable { listeners.forEach { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index c3714a1303..8de36d0427 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider -import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields @@ -52,7 +51,6 @@ internal class DefaultTimelineService @AssistedInject constructor( private val paginationTask: PaginationTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler ) : TimelineService { @@ -72,7 +70,6 @@ internal class DefaultTimelineService @AssistedInject constructor( paginationTask = paginationTask, timelineEventMapper = timelineEventMapper, settings = settings, - hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), timelineInput = timelineInput, eventDecryptor = eventDecryptor, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt deleted file mode 100644 index b2c8021f3b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.session.room.timeline - -import io.realm.RealmQuery -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.query.filterEvents - -internal fun RealmQuery.filterEventsWithSettings(settings: TimelineSettings): RealmQuery { - return filterEvents(settings.filters) -} - -internal fun List.filterEventsWithSettings(settings: TimelineSettings): List { - return filter { event -> - val filterType = !settings.filters.filterTypes - || settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) } - if (!filterType) return@filter false - - val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) { - val messageContent = event.root.getClearContent().toModel() - messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE - } else { - true - } - if (!filterEdits) return@filter false - - val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted() - !filterRedacted - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt index 76c4b3812c..96646b42ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -48,8 +48,8 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor( override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() - val response = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) + val response = executeRequest(globalErrorReceiver) { + roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) } val fromToken = if (params.direction == PaginationDirection.FORWARDS) { response.end diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt index d02a7bafe9..015e55f070 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt @@ -40,9 +40,9 @@ internal class DefaultGetContextOfEventTask @Inject constructor( override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() - val response = executeRequest(globalErrorReceiver) { + val response = executeRequest(globalErrorReceiver) { // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. - apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) + roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) } return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.FORWARDS) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index b8585b1e74..cbbc54e90d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -16,28 +16,49 @@ package org.matrix.android.sdk.internal.session.room.timeline +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.EventDecryptor +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -// TODO Add parent task - -internal class GetEventTask @Inject constructor( - private val roomAPI: RoomAPI, - private val globalErrorReceiver: GlobalErrorReceiver -) : Task { - - internal data class Params( +internal interface GetEventTask : Task { + data class Params( val roomId: String, val eventId: String ) +} - override suspend fun execute(params: Params): Event { - return executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getEvent(params.roomId, params.eventId) +internal class DefaultGetEventTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + private val eventDecryptor: EventDecryptor +) : GetEventTask { + + override suspend fun execute(params: GetEventTask.Params): Event { + val event = executeRequest(globalErrorReceiver) { + roomAPI.getEvent(params.roomId, params.eventId) } + + // Try to decrypt the Event + if (event.isEncrypted()) { + tryOrNull(message = "Unable to decrypt the event") { + eventDecryptor.decryptEvent(event, "") + } + ?.let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } + } + + return event } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt index 1f99893e17..8aeccb66c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt @@ -42,9 +42,11 @@ internal class DefaultPaginationTask @Inject constructor( override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() - val chunk = executeRequest(globalErrorReceiver) { - isRetryable = true - apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) + val chunk = executeRequest( + globalErrorReceiver, + canRetry = true + ) { + roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) } return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt deleted file mode 100644 index 0ade8ad3b8..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.session.room.timeline - -import android.util.SparseArray -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.RealmResults -import org.matrix.android.sdk.api.session.room.model.ReadReceipt -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity -import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.TimelineEventFilter -import org.matrix.android.sdk.internal.database.query.whereInRoom - -/** - * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). - * When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event. - * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. - */ -internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val roomId: String, - private val settings: TimelineSettings) { - - interface Delegate { - fun rebuildEvent(eventId: String, readReceipts: List): Boolean - fun onReadReceiptsUpdated() - } - - private val correctedReadReceiptsEventByIndex = SparseArray() - private val correctedReadReceiptsByEvent = HashMap>() - - private lateinit var hiddenReadReceipts: RealmResults - private lateinit var nonFilteredEvents: RealmResults - private lateinit var filteredEvents: RealmResults - private lateinit var delegate: Delegate - - private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - if (!collection.isLoaded || !collection.isValid) { - return@OrderedRealmCollectionChangeListener - } - var hasChange = false - // Deletion here means we don't have any readReceipts for the given hidden events - changeSet.deletions.forEach { - val eventId = correctedReadReceiptsEventByIndex.get(it, "") - val timelineEvent = filteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) - .findFirst() - - // We are rebuilding the corresponding event with only his own RR - val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) - hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange - } - correctedReadReceiptsEventByIndex.clear() - correctedReadReceiptsByEvent.clear() - for (index in 0 until hiddenReadReceipts.size) { - val summary = hiddenReadReceipts[index] ?: continue - val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue - val isLoaded = nonFilteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null - val displayIndex = timelineEvent.displayIndex - - if (isLoaded) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should - if (firstDisplayedEvent != null) { - correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent - .getOrPut(firstDisplayedEvent.eventId, { - ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) - }) - .addAll(readReceiptsSummaryMapper.map(summary)) - } - } - } - if (correctedReadReceiptsByEvent.isNotEmpty()) { - correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> - val sortedReadReceipts = correctedReadReceipts.sortedByDescending { - it.originServerTs - } - hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange - } - } - if (hasChange) { - delegate.onReadReceiptsUpdated() - } - } - - /** - * Start the realm query subscription. Has to be called on an HandlerThread - */ - fun start(realm: Realm, - filteredEvents: RealmResults, - nonFilteredEvents: RealmResults, - delegate: Delegate) { - this.filteredEvents = filteredEvents - this.nonFilteredEvents = nonFilteredEvents - this.delegate = delegate - // We are looking for read receipts set on hidden events. - // We only accept those with a timelineEvent (so coming from pagination/sync). - this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) - .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.`$`) - .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(hiddenReadReceiptsListener) } - } - - /** - * Dispose the realm query subscription. Has to be called on an HandlerThread - */ - fun dispose() { - if (this::hiddenReadReceipts.isInitialized) { - this.hiddenReadReceipts.removeAllChangeListeners() - } - } - - /** - * Return the current corrected [ReadReceipt] list for an event, or null - */ - fun correctedReadReceipts(eventId: String?): List? { - return correctedReadReceiptsByEvent[eventId] - } - - /** - * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - var needOr = false - if (settings.filters.filterTypes) { - beginGroup() - // Events: A, B, C, D, (E and S1), F, G, (H and S1), I - // Allowed: A, B, C, (E and S1), G, (H and S2) - // Result: D, F, H, I - settings.filters.allowedTypes.forEachIndexed { index, filter -> - if (filter.stateKey == null) { - notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType) - } else { - beginGroup() - notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType) - or() - notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.STATE_KEY}", filter.stateKey) - endGroup() - } - if (index != settings.filters.allowedTypes.size - 1) { - and() - } - } - endGroup() - needOr = true - } - if (settings.filters.filterUseless) { - if (needOr) or() - equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.IS_USELESS}", true) - needOr = true - } - if (settings.filters.filterEdits) { - if (needOr) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.EDIT) - or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.RESPONSE) - needOr = true - } - if (settings.filters.filterRedacted) { - if (needOr) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) - } - endGroup() - return this - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index 67d0d90d77..4804fbd731 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -70,15 +70,12 @@ internal class UIEchoManager( return existingState != sendState } - // return true if should update - fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean { - var postSnapshot = false - + fun onLocalEchoCreated(timelineEvent: TimelineEvent) { // Manage some ui echos (do it before filter because actual event could be filtered out) when (timelineEvent.root.getClearType()) { EventType.REDACTION -> { } - EventType.REACTION -> { + EventType.REACTION -> { val content = timelineEvent.root.content?.toModel() if (RelationType.ANNOTATION == content?.relatesTo?.type) { val reaction = content.relatesTo.key @@ -91,21 +88,14 @@ internal class UIEchoManager( reaction = reaction ) ) - postSnapshot = listener.rebuildEvent(relatedEventID) { + listener.rebuildEvent(relatedEventID) { decorateEventWithReactionUiEcho(it) - } || postSnapshot + } } } } - - // do not add events that would have been filtered - if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) { - Timber.v("On local echo created: ${timelineEvent.eventId}") - inMemorySendingEvents.add(0, timelineEvent) - postSnapshot = true - } - - return postSnapshot + Timber.v("On local echo created: ${timelineEvent.eventId}") + inMemorySendingEvents.add(0, timelineEvent) } fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt index 3b56d04872..0b0df74311 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt @@ -44,8 +44,8 @@ internal class DefaultSendTypingTask @Inject constructor( override suspend fun execute(params: SendTypingTask.Params) { delay(params.delay ?: -1) - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.sendTypingState( + executeRequest(globalErrorReceiver) { + roomAPI.sendTypingState( params.roomId, userId, TypingBody(params.isTyping, params.typingTimeoutMillis?.takeIf { params.isTyping }) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt index b3e4a5aa05..028c3e9193 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt @@ -37,7 +37,6 @@ import org.matrix.android.sdk.internal.session.filter.FilterFactory import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -86,8 +85,8 @@ internal class DefaultGetUploadsTask @Inject constructor( val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available") val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString() - val chunk = executeRequest(globalErrorReceiver) { - apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter) + val chunk = executeRequest(globalErrorReceiver) { + roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter) } result = GetUploadsResult( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt index 4a74b0a023..b5099e7238 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.search import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.search.request.SearchRequestBody import org.matrix.android.sdk.internal.session.search.response.SearchResponse -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Query @@ -31,6 +30,6 @@ internal interface SearchAPI { * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-search */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "search") - fun search(@Query("next_batch") nextBatch: String?, - @Body body: SearchRequestBody): Call + suspend fun search(@Query("next_batch") nextBatch: String?, + @Body body: SearchRequestBody): SearchResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt index 402602e4d5..8de762ee1b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt @@ -51,25 +51,25 @@ internal class DefaultSearchTask @Inject constructor( ) : SearchTask { override suspend fun execute(params: SearchTask.Params): SearchResult { - return executeRequest(globalErrorReceiver) { - val searchRequestBody = SearchRequestBody( - searchCategories = SearchRequestCategories( - roomEvents = SearchRequestRoomEvents( - searchTerm = params.searchTerm, - orderBy = if (params.orderByRecent) SearchRequestOrder.RECENT else SearchRequestOrder.RANK, - filter = SearchRequestFilter( - limit = params.limit, - rooms = listOf(params.roomId) - ), - eventContext = SearchRequestEventContext( - beforeLimit = params.beforeLimit, - afterLimit = params.afterLimit, - includeProfile = params.includeProfile - ) - ) - ) - ) - apiCall = searchAPI.search(params.nextBatch, searchRequestBody) + val searchRequestBody = SearchRequestBody( + searchCategories = SearchRequestCategories( + roomEvents = SearchRequestRoomEvents( + searchTerm = params.searchTerm, + orderBy = if (params.orderByRecent) SearchRequestOrder.RECENT else SearchRequestOrder.RANK, + filter = SearchRequestFilter( + limit = params.limit, + rooms = listOf(params.roomId) + ), + eventContext = SearchRequestEventContext( + beforeLimit = params.beforeLimit, + afterLimit = params.afterLimit, + includeProfile = params.includeProfile + ) + ) + ) + ) + return executeRequest(globalErrorReceiver) { + searchAPI.search(params.nextBatch, searchRequestBody) }.toDomain() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt index 2c3cd5d270..563e85aefc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.signout -import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams @@ -39,8 +38,8 @@ internal class DefaultSignInAgainTask @Inject constructor( ) : SignInAgainTask { override suspend fun execute(params: SignInAgainTask.Params) { - val newCredentials = executeRequest(globalErrorReceiver) { - apiCall = signOutAPI.loginAgain( + val newCredentials = executeRequest(globalErrorReceiver) { + signOutAPI.loginAgain( PasswordLoginParams.userIdentifier( // Reuse the same userId sessionParams.userId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt index 4c92938b77..a56362e587 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.signout import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.Headers import retrofit2.http.POST @@ -35,11 +34,11 @@ internal interface SignOutAPI { */ @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") - fun loginAgain(@Body loginParams: PasswordLoginParams): Call + suspend fun loginAgain(@Body loginParams: PasswordLoginParams): Credentials /** * Invalidate the access token, so that it can no longer be used for authorization. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "logout") - fun signOut(): Call + suspend fun signOut() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt index 0cb8704782..9c25eccb3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt @@ -45,8 +45,8 @@ internal class DefaultSignOutTask @Inject constructor( if (params.signOutFromHomeserver) { Timber.d("SignOut: send request...") try { - executeRequest(globalErrorReceiver) { - apiCall = signOutAPI.signOut() + executeRequest(globalErrorReceiver) { + signOutAPI.signOut() } } catch (throwable: Throwable) { // Maybe due to https://github.com/matrix-org/synapse/issues/5756 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 0f97d0cb39..2bb606e921 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -49,6 +49,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith +import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.reportSubtask @@ -64,9 +65,9 @@ import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.model.RoomSync import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.util.computeBestChunkSize import timber.log.Timber import javax.inject.Inject -import kotlin.math.ceil internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, @@ -140,17 +141,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle syncLocalTimeStampMillis: Long, aggregator: SyncResponsePostTreatmentAggregator, reporter: ProgressReporter?) { - val maxSize = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE - val listSize = handlingStrategy.data.keys.size - val numberOfChunks = ceil(listSize / maxSize.toDouble()).toInt() + val bestChunkSize = computeBestChunkSize( + listSize = handlingStrategy.data.keys.size, + limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE + ) - if (numberOfChunks > 1) { - reportSubtask(reporter, InitSyncStep.ImportingAccountJoinedRooms, numberOfChunks, 0.6f) { - val chunkSize = listSize / numberOfChunks - Timber.d("INIT_SYNC $listSize rooms to insert, split into $numberOfChunks sublists of $chunkSize items") + if (bestChunkSize.shouldChunk()) { + reportSubtask(reporter, InitSyncStep.ImportingAccountJoinedRooms, bestChunkSize.numberOfChunks, 0.6f) { + Timber.d("INIT_SYNC ${handlingStrategy.data.keys.size} rooms to insert, split with $bestChunkSize") // I cannot find a better way to chunk a map, so chunk the keys and then create new maps handlingStrategy.data.keys - .chunked(chunkSize) + .chunked(bestChunkSize.chunkSize) .forEachIndexed { index, roomIds -> val roomEntities = roomIds .also { Timber.d("INIT_SYNC insert ${roomIds.size} rooms") } @@ -407,6 +408,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun decryptIfNeeded(event: Event, roomId: String) { try { + // Event from sync does not have roomId, so add it to the event first val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, @@ -464,18 +466,4 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } } } - - private fun Event.getFixedRoomMemberContent(): RoomMemberContent? { - val content = content.toModel() - // if user is leaving, we should grab his last name and avatar from prevContent - return if (content?.membership?.isLeft() == true) { - val prevContent = resolvedPrevContent().toModel() - content.copy( - displayName = prevContent?.displayName, - avatarUrl = prevContent?.avatarUrl - ) - } else { - content - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt index f9ae41bc94..add5d841d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt @@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.sync import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomTagEntity -import org.matrix.android.sdk.internal.database.query.where import io.realm.Realm +import org.matrix.android.sdk.internal.database.query.getOrCreate import javax.inject.Inject internal class RoomTagHandler @Inject constructor() { @@ -31,12 +31,8 @@ internal class RoomTagHandler @Inject constructor() { } val tags = content.tags.entries.map { (tagName, params) -> RoomTagEntity(tagName, params["order"] as? Double) + Pair(tagName, params["order"] as? Double) } - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: RoomSummaryEntity(roomId) - - roomSummaryEntity.tags.clear() - roomSummaryEntity.tags.addAll(tags) - realm.insertOrUpdate(roomSummaryEntity) + RoomSummaryEntity.getOrCreate(realm, roomId).updateTags(tags) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt index 8e3523bc57..2616803463 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt @@ -31,11 +31,11 @@ internal interface SyncAPI { * Set all the timeouts to 1 minute by default */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync") - fun sync(@QueryMap params: Map, - @Header(TimeOutInterceptor.CONNECT_TIMEOUT) connectTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT, - @Header(TimeOutInterceptor.READ_TIMEOUT) readTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT, - @Header(TimeOutInterceptor.WRITE_TIMEOUT) writeTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT - ): Call + suspend fun sync(@QueryMap params: Map, + @Header(TimeOutInterceptor.CONNECT_TIMEOUT) connectTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT, + @Header(TimeOutInterceptor.READ_TIMEOUT) readTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT, + @Header(TimeOutInterceptor.WRITE_TIMEOUT) writeTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT + ): SyncResponse /** * Set all the timeouts to 1 minute by default diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 8d8d69be1e..83a2ffc446 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -29,7 +29,6 @@ import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilit import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.Task @@ -115,8 +114,8 @@ internal class DefaultSyncTask @Inject constructor( workingDir.deleteRecursively() } else { val syncResponse = logDuration("INIT_SYNC Request") { - executeRequest(globalErrorReceiver) { - apiCall = syncAPI.sync( + executeRequest(globalErrorReceiver) { + syncAPI.sync( params = requestParams, readTimeOut = readTimeOut ) @@ -130,8 +129,8 @@ internal class DefaultSyncTask @Inject constructor( } initialSyncProgressService.endAll() } else { - val syncResponse = executeRequest(globalErrorReceiver) { - apiCall = syncAPI.sync( + val syncResponse = executeRequest(globalErrorReceiver) { + syncAPI.sync( params = requestParams, readTimeOut = readTimeOut ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt index 449d47abe5..b8d987d500 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt @@ -45,6 +45,8 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver +import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent @@ -60,7 +62,10 @@ internal class UserAccountDataSyncHandler @Inject constructor( @SessionDatabase private val monarchy: Monarchy, @UserId private val userId: String, private val directChatsHelper: DirectChatsHelper, - private val updateUserAccountDataTask: UpdateUserAccountDataTask) { + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val roomAvatarResolver: RoomAvatarResolver, + private val roomDisplayNameResolver: RoomDisplayNameResolver +) { fun handle(realm: Realm, accountData: UserAccountDataSync?) { accountData?.list?.forEach { event -> @@ -151,23 +156,29 @@ internal class UserAccountDataSyncHandler @Inject constructor( } private fun handleDirectChatRooms(realm: Realm, event: UserAccountDataEvent) { - val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) - oldDirectRooms.forEach { - it.isDirect = false - it.directUserId = null - } val content = event.content.toModel() ?: return - content.forEach { - val userId = it.key - it.value.forEach { roomId -> + content.forEach { (userId, roomIds) -> + roomIds.forEach { roomId -> val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() if (roomSummaryEntity != null) { roomSummaryEntity.isDirect = true roomSummaryEntity.directUserId = userId - realm.insertOrUpdate(roomSummaryEntity) + // Also update the avatar and displayname, there is a specific treatment for DMs + roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId) } } } + + // Handle previous direct rooms + RoomSummaryEntity.getDirectRooms(realm, excludeRoomIds = content.values.flatten().toSet()) + .forEach { + it.isDirect = false + it.directUserId = null + // Also update the avatar and displayname, there was a specific treatment for DMs + it.avatarUrl = roomAvatarResolver.resolve(realm, it.roomId) + it.displayName = roomDisplayNameResolver.resolve(realm, it.roomId) + } } private fun handleIgnoredUsers(realm: Realm, event: UserAccountDataEvent) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt index c406f3acf1..41173dea96 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt @@ -16,4 +16,7 @@ package org.matrix.android.sdk.internal.session.sync.model.accountdata -typealias DirectMessagesContent = Map> +/** + * Keys are userIds, values are list of roomIds + */ +internal typealias DirectMessagesContent = Map> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index 41914cc799..bac725fad2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -17,7 +17,8 @@ package org.matrix.android.sdk.internal.session.terms import dagger.Lazy -import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.terms.GetTermsResponse import org.matrix.android.sdk.api.session.terms.TermsService @@ -29,12 +30,9 @@ import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI import org.matrix.android.sdk.internal.session.identity.IdentityRegisterTask import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.ensureTrailingSlash -import okhttp3.OkHttpClient import javax.inject.Inject internal class DefaultTermsService @Inject constructor( @@ -45,43 +43,39 @@ internal class DefaultTermsService @Inject constructor( private val retrofitFactory: RetrofitFactory, private val getOpenIdTokenTask: GetOpenIdTokenTask, private val identityRegisterTask: IdentityRegisterTask, - private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers + private val updateUserAccountDataTask: UpdateUserAccountDataTask ) : TermsService { + override suspend fun getTerms(serviceType: TermsService.ServiceType, - baseUrl: String): GetTermsResponse { - return withContext(coroutineDispatchers.main) { - val url = buildUrl(baseUrl, serviceType) - val termsResponse = executeRequest(null) { - apiCall = termsAPI.getTerms("${url}terms") - } - GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + baseUrl: String): GetTermsResponse { + val url = buildUrl(baseUrl, serviceType) + val termsResponse = executeRequest(null) { + termsAPI.getTerms("${url}terms") } + return GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) } override suspend fun agreeToTerms(serviceType: TermsService.ServiceType, baseUrl: String, agreedUrls: List, token: String?) { - withContext(coroutineDispatchers.main) { - val url = buildUrl(baseUrl, serviceType) - val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) + val url = buildUrl(baseUrl, serviceType) + val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) - executeRequest(null) { - apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") - } - - // client SHOULD update this account data section adding any the URLs - // of any additional documents that the user agreed to this list. - // Get current m.accepted_terms append new ones and update account data - val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() - - val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() - - updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( - acceptedTermsContent = AcceptedTermsContent(newList) - )) + executeRequest(null) { + termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") } + + // client SHOULD update this account data section adding any the URLs + // of any additional documents that the user agreed to this list. + // Get current m.accepted_terms append new ones and update account data + val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() + + val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + )) } private suspend fun getToken(url: String): String { @@ -97,7 +91,7 @@ internal class DefaultTermsService @Inject constructor( private fun buildUrl(baseUrl: String, serviceType: TermsService.ServiceType): String { val servicePath = when (serviceType) { TermsService.ServiceType.IntegrationManager -> NetworkConstants.URI_INTEGRATION_MANAGER_PATH - TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 + TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 } return "${baseUrl.ensureTrailingSlash()}$servicePath" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt index 4c97f462eb..91d27030de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.terms import org.matrix.android.sdk.internal.network.HttpHeaders -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header @@ -29,13 +28,13 @@ internal interface TermsAPI { * This request does not require authentication */ @GET - fun getTerms(@Url url: String): Call + suspend fun getTerms(@Url url: String): TermsResponse /** * This request requires authentication */ @POST - fun agreeToTerms(@Url url: String, - @Body params: AcceptTermsBody, - @Header(HttpHeaders.Authorization) token: String): Call + suspend fun agreeToTerms(@Url url: String, + @Body params: AcceptTermsBody, + @Header(HttpHeaders.Authorization) token: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt index fd1ed741e9..026e17b513 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt @@ -31,7 +31,7 @@ internal class DefaultGetThirdPartyProtocolsTask @Inject constructor( override suspend fun execute(params: Unit): Map { return executeRequest(globalErrorReceiver) { - apiCall = thirdPartyAPI.thirdPartyProtocols() + thirdPartyAPI.thirdPartyProtocols() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt index 01a8b57678..f541dcb814 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt @@ -37,7 +37,7 @@ internal class DefaultGetThirdPartyUserTask @Inject constructor( override suspend fun execute(params: GetThirdPartyUserTask.Params): List { return executeRequest(globalErrorReceiver) { - apiCall = thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields) + thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index 0c60a27341..2e03bc7a86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.thirdparty import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.QueryMap @@ -32,7 +31,7 @@ internal interface ThirdPartyAPI { * Ref: https://matrix.org/docs/spec/client_server/r0.6.1.html#get-matrix-client-r0-thirdparty-protocols */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") - fun thirdPartyProtocols(): Call> + suspend fun thirdPartyProtocols(): Map /** * Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters. @@ -40,5 +39,6 @@ internal interface ThirdPartyAPI { * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-thirdparty-user-protocol */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}") - fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map?): Call> + suspend fun getThirdPartyUser(@Path("protocol") protocol: String, + @QueryMap params: Map?): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt index 1740956915..52b8cc3689 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt @@ -18,54 +18,35 @@ package org.matrix.android.sdk.internal.session.user import androidx.lifecycle.LiveData import androidx.paging.PagedList -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.user.model.User -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask import org.matrix.android.sdk.internal.session.user.model.SearchUserTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import javax.inject.Inject internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource, private val searchUserTask: SearchUserTask, private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask, - private val getProfileInfoTask: GetProfileInfoTask, - private val taskExecutor: TaskExecutor) : UserService { + private val getProfileInfoTask: GetProfileInfoTask) : UserService { override fun getUser(userId: String): User? { return userDataSource.getUser(userId) } - override fun resolveUser(userId: String, callback: MatrixCallback) { + override suspend fun resolveUser(userId: String): User { val known = getUser(userId) if (known != null) { - callback.onSuccess(known) + return known } else { val params = GetProfileInfoTask.Params(userId) - getProfileInfoTask - .configureWith(params) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: JsonDict) { - callback.onSuccess( - User( - userId, - data[ProfileService.DISPLAY_NAME_KEY] as? String, - data[ProfileService.AVATAR_URL_KEY] as? String) - ) - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) + val data = getProfileInfoTask.execute(params) + return User( + userId, + data[ProfileService.DISPLAY_NAME_KEY] as? String, + data[ProfileService.AVATAR_URL_KEY] as? String) } } @@ -85,33 +66,20 @@ internal class DefaultUserService @Inject constructor(private val userDataSource return userDataSource.getIgnoredUsersLive() } - override fun searchUsersDirectory(search: String, - limit: Int, - excludedUserIds: Set, - callback: MatrixCallback>): Cancelable { + override suspend fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set): List { val params = SearchUserTask.Params(limit, search, excludedUserIds) - return searchUserTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return searchUserTask.execute(params) } - override fun ignoreUserIds(userIds: List, callback: MatrixCallback): Cancelable { + override suspend fun ignoreUserIds(userIds: List) { val params = UpdateIgnoredUserIdsTask.Params(userIdsToIgnore = userIds.toList()) - return updateIgnoredUserIdsTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + updateIgnoredUserIdsTask.execute(params) } - override fun unIgnoreUserIds(userIds: List, callback: MatrixCallback): Cancelable { + override suspend fun unIgnoreUserIds(userIds: List) { val params = UpdateIgnoredUserIdsTask.Params(userIdsToUnIgnore = userIds.toList()) - return updateIgnoredUserIdsTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + updateIgnoredUserIdsTask.execute(params) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt index c5c546bbed..e03d406639 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.user import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.user.model.SearchUsersParams import org.matrix.android.sdk.internal.session.user.model.SearchUsersResponse -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST @@ -31,5 +30,5 @@ internal interface SearchUserAPI { * @param searchUsersParams the search params. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") - fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call + suspend fun searchUsers(@Body searchUsersParams: SearchUsersParams): SearchUsersResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt index 3de484fab3..cc5625b255 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.user.accountdata import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.PUT import retrofit2.http.Path @@ -32,7 +31,7 @@ interface AccountDataAPI { * @param params the put params */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") - fun setAccountData(@Path("userId") userId: String, - @Path("type") type: String, - @Body params: Any): Call + suspend fun setAccountData(@Path("userId") userId: String, + @Path("type") type: String, + @Body params: Any) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt index 1f1e987ebf..27db30f3b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -18,16 +18,15 @@ package org.matrix.android.sdk.internal.session.user.accountdata import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.awaitCallback import javax.inject.Inject internal class DefaultAccountDataService @Inject constructor( @@ -54,26 +53,18 @@ internal class DefaultAccountDataService @Inject constructor( return accountDataDataSource.getLiveAccountDataEvents(types) } - override fun updateAccountData(type: String, content: Content, callback: MatrixCallback?): Cancelable { - return updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams( - type = type, - any = content - )) { - this.retryCount = 5 - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - // TODO Move that to the task (but it created a circular dependencies...) - monarchy.runTransactionSync { realm -> - userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) - } - callback?.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - callback?.onFailure(failure) - } + override suspend fun updateAccountData(type: String, content: Content) { + val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content) + awaitCallback { callback -> + updateUserAccountDataTask.configureWith(params) { + this.retryCount = 5 // TODO: Need to refactor retrying out into a helper method. + this.callback = callback } + .executeBy(taskExecutor) + } + // TODO Move that to the task (but it created a circular dependencies...) + monarchy.runTransactionSync { realm -> + userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) } - .executeBy(taskExecutor) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt index 26e8d3380a..445b78104c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt @@ -63,8 +63,8 @@ internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor( val list = ignoredUserIds.toList() val body = IgnoredUsersContent.createWithUserIds(list) - executeRequest(globalErrorReceiver) { - apiCall = accountDataApi.setAccountData(userId, UserAccountDataTypes.TYPE_IGNORED_USER_LIST, body) + executeRequest(globalErrorReceiver) { + accountDataApi.setAccountData(userId, UserAccountDataTypes.TYPE_IGNORED_USER_LIST, body) } // Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index dba28253a7..1a588d2245 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -105,7 +105,7 @@ internal class DefaultUpdateUserAccountDataTask @Inject constructor( override suspend fun execute(params: UpdateUserAccountDataTask.Params) { return executeRequest(globalErrorReceiver) { - apiCall = accountDataApi.setAccountData(userId, params.type, params.getData()) + accountDataApi.setAccountData(userId, params.type, params.getData()) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt index 380fa6e209..5a8779f40f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt @@ -38,8 +38,8 @@ internal class DefaultSearchUserTask @Inject constructor( ) : SearchUserTask { override suspend fun execute(params: SearchUserTask.Params): List { - val response = executeRequest(globalErrorReceiver) { - apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) + val response = executeRequest(globalErrorReceiver) { + searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) } return response.users.map { User(it.userId, it.displayName, it.avatarUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt index ae807ce30f..18a043be45 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt @@ -46,8 +46,8 @@ internal class DefaultCreateWidgetTask @Inject constructor(@SessionDatabase priv private val globalErrorReceiver: GlobalErrorReceiver) : CreateWidgetTask { override suspend fun execute(params: CreateWidgetTask.Params) { - executeRequest(globalErrorReceiver) { - apiCall = roomAPI.sendStateEvent( + executeRequest(globalErrorReceiver) { + roomAPI.sendStateEvent( roomId = params.roomId, stateEventType = EventType.STATE_ROOM_WIDGET_LEGACY, stateKey = params.widgetId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt index 9f5a9360ee..5912dc7b53 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -17,14 +17,12 @@ package org.matrix.android.sdk.internal.session.widgets import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator import org.matrix.android.sdk.api.session.widgets.WidgetService import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter import org.matrix.android.sdk.api.session.widgets.model.Widget -import org.matrix.android.sdk.api.util.Cancelable import javax.inject.Inject import javax.inject.Provider @@ -77,21 +75,19 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage return widgetManager.getUserWidgets(widgetTypes, excludedTypes) } - override fun createRoomWidget( + override suspend fun createRoomWidget( roomId: String, widgetId: String, - content: Content, - callback: MatrixCallback - ): Cancelable { - return widgetManager.createRoomWidget(roomId, widgetId, content, callback) + content: Content + ): Widget { + return widgetManager.createRoomWidget(roomId, widgetId, content) } - override fun destroyRoomWidget( + override suspend fun destroyRoomWidget( roomId: String, - widgetId: String, - callback: MatrixCallback - ): Cancelable { - return widgetManager.destroyRoomWidget(roomId, widgetId, callback) + widgetId: String + ) { + return widgetManager.destroyRoomWidget(roomId, widgetId) } override fun hasPermissionsToHandleWidgets(roomId: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index 73a4cc697d..3244212487 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes @@ -34,7 +33,6 @@ import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.api.session.widgets.model.Widget -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope @@ -43,8 +41,6 @@ import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.launchToCallback import java.util.HashMap import javax.inject.Inject @@ -52,7 +48,6 @@ import javax.inject.Inject internal class WidgetManager @Inject constructor(private val integrationManager: IntegrationManager, private val accountDataDataSource: AccountDataDataSource, private val stateEventDataSource: StateEventDataSource, - private val taskExecutor: TaskExecutor, private val createWidgetTask: CreateWidgetTask, private val widgetFactory: WidgetFactory, @UserId private val userId: String) @@ -165,37 +160,33 @@ internal class WidgetManager @Inject constructor(private val integrationManager: .toList() } - fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(callback = callback) { - if (!hasPermissionsToHandleWidgets(roomId)) { - throw WidgetManagementFailure.NotEnoughPower - } - val params = CreateWidgetTask.Params( - roomId = roomId, - widgetId = widgetId, - content = content - ) - createWidgetTask.execute(params) - try { - getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first() - } catch (failure: Throwable) { - throw WidgetManagementFailure.CreationFailed - } + suspend fun createRoomWidget(roomId: String, widgetId: String, content: Content): Widget { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = content + ) + createWidgetTask.execute(params) + try { + return getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first() + } catch (failure: Throwable) { + throw WidgetManagementFailure.CreationFailed } } - fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(callback = callback) { - if (!hasPermissionsToHandleWidgets(roomId)) { - throw WidgetManagementFailure.NotEnoughPower - } - val params = CreateWidgetTask.Params( - roomId = roomId, - widgetId = widgetId, - content = emptyMap() - ) - createWidgetTask.execute(params) + suspend fun destroyRoomWidget(roomId: String, widgetId: String) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = emptyMap() + ) + createWidgetTask.execute(params) } fun hasPermissionsToHandleWidgets(roomId: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt index 1fece8b580..6652628026 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.widgets import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse -import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -30,10 +29,10 @@ internal interface WidgetsAPI { * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) */ @POST("register") - fun register(@Body body: RequestOpenIdTokenResponse, - @Query("v") version: String?): Call + suspend fun register(@Body body: RequestOpenIdTokenResponse, + @Query("v") version: String?): RegisterWidgetResponse @GET("account") - fun validateToken(@Query("scalar_token") scalarToken: String?, - @Query("v") version: String?): Call + suspend fun validateToken(@Query("scalar_token") scalarToken: String?, + @Query("v") version: String?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt index 6db79da35f..78a40d1977 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask -import org.matrix.android.sdk.internal.session.widgets.RegisterWidgetResponse import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.internal.session.widgets.WidgetsAPI import org.matrix.android.sdk.internal.session.widgets.WidgetsAPIProvider @@ -59,8 +58,8 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets private suspend fun getNewScalarToken(widgetsAPI: WidgetsAPI, serverUrl: String): String { val openId = getOpenIdTokenTask.execute(Unit) - val registerWidgetResponse = executeRequest(null) { - apiCall = widgetsAPI.register(openId, WIDGET_API_VERSION) + val registerWidgetResponse = executeRequest(null) { + widgetsAPI.register(openId, WIDGET_API_VERSION) } if (registerWidgetResponse.scalarToken == null) { // Should not happen @@ -72,8 +71,8 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String { return try { - executeRequest(null) { - apiCall = widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION) + executeRequest(null) { + widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION) } scalarToken } catch (failure: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MathUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MathUtils.kt new file mode 100644 index 0000000000..c9c597e93d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MathUtils.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import kotlin.math.ceil + +internal data class BestChunkSize( + val numberOfChunks: Int, + val chunkSize: Int +) { + fun shouldChunk() = numberOfChunks > 1 +} + +internal fun computeBestChunkSize(listSize: Int, limit: Int): BestChunkSize { + return if (listSize <= limit) { + BestChunkSize( + numberOfChunks = 1, + chunkSize = listSize + ) + } else { + val numberOfChunks = ceil(listSize / limit.toDouble()).toInt() + // Round on next Int + val chunkSize = ceil(listSize / numberOfChunks.toDouble()).toInt() + + BestChunkSize( + numberOfChunks = numberOfChunks, + chunkSize = chunkSize + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt index 3f0e27f410..7a9beac8c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -89,8 +89,8 @@ internal class DefaultGetWellknownTask @Inject constructor( .create(WellKnownAPI::class.java) return try { - val wellKnown = executeRequest(null) { - apiCall = wellKnownAPI.getWellKnown(domain) + val wellKnown = executeRequest(null) { + wellKnownAPI.getWellKnown(domain) } // Success @@ -140,8 +140,8 @@ internal class DefaultGetWellknownTask @Inject constructor( .create(CapabilitiesAPI::class.java) try { - executeRequest(null) { - apiCall = capabilitiesAPI.ping() + executeRequest(null) { + capabilitiesAPI.ping() } } catch (throwable: Throwable) { return WellknownResult.FailError @@ -178,8 +178,8 @@ internal class DefaultGetWellknownTask @Inject constructor( .create(IdentityAuthAPI::class.java) return try { - executeRequest(null) { - apiCall = identityPingApi.ping() + executeRequest(null) { + identityPingApi.ping() } true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt index 981d013f49..428f7f65c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt @@ -16,11 +16,10 @@ package org.matrix.android.sdk.internal.wellknown import org.matrix.android.sdk.api.auth.data.WellKnown -import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path internal interface WellKnownAPI { @GET("https://{domain}/.well-known/matrix/client") - fun getWellKnown(@Path("domain") domain: String): Call + suspend fun getWellKnown(@Path("domain") domain: String): WellKnown } diff --git a/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml b/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml index b690fee4ed..12edb39070 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml @@ -35,7 +35,7 @@ Robotti Hattu Silmälasit - Mutteriavain + Kiintoavain Joulupukki Peukalo ylös Sateenvarjo diff --git a/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml index 618302eb4f..12f90e316d 100644 --- a/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml @@ -3,18 +3,66 @@ + ライオン + ユニコーン + ブタ + ゾウ + うさぎ + パンダ + ニワトリ + ペンギン + + たこ + ちょうちょ + サボテン きのこ + 地球 + + + バナナ リンゴ + いちご + とうもろこし + ピザ ケーキ + ハート + スマイル ロボと + 帽子 めがね + スパナ + サンタ + いいね + + 砂時計 + 時計 + ギフト + 電球 + 鉛筆 + クリップ + はさみ + 錠前 + + 金槌 電話機 + 電車 自転車 + 飛行機 + ロケット + トロフィー + ボール + ギター + トランペット + ベル + いかり + ヘッドホン + フォルダ + ピン diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/MathUtilTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/MathUtilTest.kt new file mode 100644 index 0000000000..ade811f9b7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/MathUtilTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldHaveSize +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.MatrixTest + +@FixMethodOrder(MethodSorters.JVM) +class MathUtilTest : MatrixTest { + + @Test + fun testComputeBestChunkSize0() = doTest(0, 100, 1, 0) + + @Test + fun testComputeBestChunkSize1to99() { + for (i in 1..99) { + doTest(i, 100, 1, i) + } + } + + @Test + fun testComputeBestChunkSize100() = doTest(100, 100, 1, 100) + + @Test + fun testComputeBestChunkSize101() = doTest(101, 100, 2, 51) + + @Test + fun testComputeBestChunkSize199() = doTest(199, 100, 2, 100) + + @Test + fun testComputeBestChunkSize200() = doTest(200, 100, 2, 100) + + @Test + fun testComputeBestChunkSize201() = doTest(201, 100, 3, 67) + + @Test + fun testComputeBestChunkSize240() = doTest(240, 100, 3, 80) + + private fun doTest(listSize: Int, limit: Int, expectedNumberOfChunks: Int, expectedChunkSize: Int) { + val result = computeBestChunkSize(listSize, limit) + + result.numberOfChunks shouldBeEqualTo expectedNumberOfChunks + result.chunkSize shouldBeEqualTo expectedChunkSize + + // Test that the result make sense, when we use chunked() + if (result.chunkSize > 0) { + generateSequence { "a" } + .take(listSize) + .chunked(result.chunkSize) + .shouldHaveSize(result.numberOfChunks) + } + } +} diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 89a692a782..26afd5fb77 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -43,7 +43,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment-ktx:1.3.0" + implementation "androidx.fragment:fragment-ktx:1.3.2" implementation 'androidx.exifinterface:exifinterface:1.3.2' // Log diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 468fe717c0..5a53ececec 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===92 +enum class===94 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/import_emojis.py b/tools/import_emojis.py new file mode 100755 index 0000000000..30db3b0b13 --- /dev/null +++ b/tools/import_emojis.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +from collections import OrderedDict + +import requests +import json +import re +import os +from bs4 import BeautifulSoup + +# A list of words to not capitalize in emoji-names +capitalization_exclude = {'with', 'a', 'at', 'of', 'for', 'and', 'over', 'the', 'off', 'on', 'out', 'in', 'but', 'or'} + +# Create skeleton of the final json file as a python dictionary: +emoji_picker_datasource = { + "compressed": True, + "categories": [], + "emojis": {}, + "aliases": {} +} +emoji_picker_datasource_categories = emoji_picker_datasource["categories"] +emoji_picker_datasource_emojis = emoji_picker_datasource["emojis"] + + +# Get official emoji list from unicode.org (Emoji List, v13.1 at time of writing) +print("Fetching emoji list from Unicode.org...") +req = requests.get("https://unicode.org/emoji/charts/emoji-list.html") +soup = BeautifulSoup(req.content, 'html.parser') + +# Navigate to table +table = soup.body.table + +# Go over all rows +print("Extracting emojis...") +for row in table.find_all('tr'): + # Add "bigheads" rows to categories + if 'bighead' in next(row.children)['class']: + relevant_element = row.find('a') + category_id = relevant_element['name'] + category_name = relevant_element.text + emoji_picker_datasource_categories.append({ + "id": category_id, + "name": category_name, + "emojis": [] + }) + + # Add information in "rchars" rows to the last encountered category and emojis + if row.find('td', class_='code'): + # Get columns + cols = row.find_all('td') + no_element = cols[0] + code_element = cols[1] + sample_element = cols[2] + cldr_element = cols[3] + keywords_element = cols[4] + + # Extract information from columns + # Extract name and id + # => Remove spaces, colons and unicode-characters + emoji_name = cldr_element.text + emoji_id = cldr_element.text.lower() + emoji_id = re.sub(r'[^A-Za-z0-9 ]+', '', emoji_id, flags=re.UNICODE) # Only keep alphanumeric, space characters + emoji_id = emoji_id.strip() # Remove leading/trailing whitespaces + emoji_id = emoji_id.replace(' ', '-') + + # Capitalize name according to the same rules as the previous emoji_picker_datasource.json + # - Words are separated by any non-word character (\W), e.g. space, comma, parentheses, dots, etc. + # - Words are capitalized if they are either at the beginning of the name OR not in capitalization_exclude (extracted from the previous datasource, too) + emoji_name_cap = "".join([w.capitalize() if i == 0 or w not in capitalization_exclude else w for i, w in enumerate(re.split('(\W)', emoji_name))]) + + # Extract emoji unicode-codepoint + emoji_code_raw = code_element.text + emoji_code_list = emoji_code_raw.split(" ") + emoji_code_list = [e[2:] for e in emoji_code_list] + emoji_code = "-".join(emoji_code_list) + + # Extract keywords + emoji_keywords = keywords_element.text.split(" | ") + + # Add the emoji-id to the last entry in "categories" + emoji_picker_datasource_categories[-1]["emojis"].append(emoji_id) + + # Add the emoji itself to the "emojis" dict + emoji_picker_datasource_emojis[emoji_id] = { + "a": emoji_name_cap, + "b": emoji_code, + "j": emoji_keywords + } + +# The keywords of unicode.org are usually quite sparse. +# There is no official specification of keywords beyond that, but muan/emojilib maintains a well maintained and +# established repository with additional keywords. We extend our list with the keywords from there. +# At the time of writing it had additional keyword information for all emojis except a few from the newest unicode 13.1. +print("Fetching additional keywords from Emojilib...") +req = requests.get("https://raw.githubusercontent.com/muan/emojilib/main/dist/emoji-en-US.json") +emojilib_data = json.loads(req.content) + +# We just go over all the official emojis from unicode, and add the keywords there +print("Adding keywords to emojis...") +for emoji in emoji_picker_datasource_emojis: + emoji_name = emoji_picker_datasource_emojis[emoji]["a"] + emoji_code = emoji_picker_datasource_emojis[emoji]["b"] + + # Convert back to actual unicode emoji + emoji_unicode = ''.join(map(lambda s: chr(int(s, 16)), emoji_code.split("-"))) + + # Search for emoji in emojilib + if emoji_unicode in emojilib_data: + emoji_additional_keywords = emojilib_data[emoji_unicode] + elif emoji_unicode+chr(0xfe0f) in emojilib_data: + emoji_additional_keywords = emojilib_data[emoji_unicode+chr(0xfe0f)] + else: + print("* No additional keywords for", emoji_unicode, emoji_picker_datasource_emojis[emoji]) + continue + + # If additional keywords exist, add them to emoji_picker_datasource_emojis + # Avoid duplicates and keep order. Put official unicode.com keywords first and extend up with emojilib ones. + new_keywords = OrderedDict.fromkeys(emoji_picker_datasource_emojis[emoji]["j"] + emoji_additional_keywords) + # Remove the ones derived from the unicode name + for keyword in [emoji.replace("-", "_")] + [emoji.replace("-", " ")] + [emoji_name]: + if keyword in new_keywords: + new_keywords.pop(keyword) + # Write new keywords back + emoji_picker_datasource_emojis[emoji]["j"] = list(new_keywords.keys()) + +# Filter out components from unicode 13.1 (as they are not suitable for single-emoji reactions) +emoji_picker_datasource['categories'] = [x for x in emoji_picker_datasource['categories'] if x['id'] != 'component'] + +# Write result to file (overwrite previous), without escaping unicode characters +print("Writing emoji_picker_datasource.json...") +scripts_dir = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(scripts_dir, "../vector/src/main/res/raw/emoji_picker_datasource.json"), "w") as outfile: + json.dump(emoji_picker_datasource, outfile, ensure_ascii=False, separators=(',', ':')) +print("Done.") diff --git a/vector/build.gradle b/vector/build.gradle index 0468c68614..d5a105d893 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -14,7 +14,7 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 1 -ext.versionPatch = 3 +ext.versionPatch = 4 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -290,13 +290,13 @@ android { dependencies { - def epoxy_version = '4.4.2' - def fragment_version = '1.3.0' + def epoxy_version = '4.4.4' + def fragment_version = '1.3.2' def arrow_version = "0.8.2" def markwon_version = '4.1.2' def big_image_viewer_version = '1.7.1' def glide_version = '4.12.0' - def moshi_version = '1.11.0' + def moshi_version = '1.12.0' def daggerVersion = '2.33' def autofill_version = "1.1.0" def work_version = '2.5.0' @@ -320,7 +320,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.recyclerview:recyclerview:1.2.0-beta02" + implementation "androidx.recyclerview:recyclerview:1.2.0-rc01" implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation 'androidx.constraintlayout:constraintlayout:2.0.4' @@ -339,10 +339,10 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' // Debug - implementation 'com.facebook.stetho:stetho:1.5.1' + implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.19' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.21' // rx implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' @@ -442,7 +442,11 @@ dependencies { implementation('com.facebook.react:react-native-webrtc:1.87.3-jitsi-6624067@aar') // Jitsi - implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') + implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') { + exclude group: 'com.google.firebase' + exclude group: 'com.google.android.gms' + exclude group: 'com.android.installreferrer' + } // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 diff --git a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt index 8959416445..79090c42dd 100644 --- a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt @@ -42,17 +42,15 @@ class EmojiDataSourceTest : InstrumentedTest { @Test fun checkNumberOfResult() { val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Wrong number of emojis", 1545, emojiDataSource.rawData.emojis.size) - assertEquals("Wrong number of categories", 8, emojiDataSource.rawData.categories.size) - assertEquals("Wrong number of aliases", 57, emojiDataSource.rawData.aliases.size) + assertTrue("Wrong number of emojis", emojiDataSource.rawData.emojis.size >= 500) + assertTrue("Wrong number of categories", emojiDataSource.rawData.categories.size >= 8) } @Test fun searchTestEmptySearch() { val emojiDataSource = EmojiDataSource(context().resources) - assertEquals("Empty search should return 1545 results", 1545, emojiDataSource.filterWith("").size) + assertTrue("Empty search should return at least 500 results", emojiDataSource.filterWith("").size >= 500) } @Test diff --git a/vector/src/androidTest/java/im/vector/app/features/roomdirectory/ExplicitTermFilterTest.kt b/vector/src/androidTest/java/im/vector/app/features/roomdirectory/ExplicitTermFilterTest.kt new file mode 100644 index 0000000000..b2beec5b66 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/roomdirectory/ExplicitTermFilterTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory + +import im.vector.app.InstrumentedTest +import im.vector.app.core.utils.AssetReader +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class ExplicitTermFilterTest : InstrumentedTest { + + private val explicitTermFilter = ExplicitTermFilter(AssetReader(context())) + + @Test + fun isValidEmptyTrue() { + explicitTermFilter.isValid("") shouldBe true + } + + @Test + fun isValidTrue() { + explicitTermFilter.isValid("Hello") shouldBe true + } + + @Test + fun isValidFalse() { + explicitTermFilter.isValid("nsfw") shouldBe false + } + + @Test + fun isValidUpCaseFalse() { + explicitTermFilter.isValid("Nsfw") shouldBe false + } + + @Test + fun isValidMultilineTrue() { + explicitTermFilter.isValid("Hello\nWorld") shouldBe true + } + + @Test + fun isValidMultilineFalse() { + explicitTermFilter.isValid("Hello\nnsfw") shouldBe false + } + + @Test + fun isValidMultilineFalse2() { + explicitTermFilter.isValid("nsfw\nHello") shouldBe false + } + + @Test + fun isValidAnalFalse() { + explicitTermFilter.isValid("anal") shouldBe false + } + + @Test + fun isValidAnal2False() { + explicitTermFilter.isValid("There is some anal in this room") shouldBe false + } + + @Test + fun isValidAnalysisTrue() { + explicitTermFilter.isValid("analysis") shouldBe true + } + + @Test + fun isValidAnalysis2True() { + explicitTermFilter.isValid("There is some analysis in the room") shouldBe true + } + + @Test + fun isValidSpecialCharFalse() { + explicitTermFilter.isValid("18+") shouldBe false + } + + @Test + fun isValidSpecialChar2False() { + explicitTermFilter.isValid("This is a room with 18+ content") shouldBe false + } + + @Test + fun isValidOtherSpecialCharFalse() { + explicitTermFilter.isValid("strap-on") shouldBe false + } + + @Test + fun isValidOtherSpecialChar2False() { + explicitTermFilter.isValid("This is a room with strap-on content") shouldBe false + } + + @Test + fun isValid18True() { + explicitTermFilter.isValid("18") shouldBe true + } + + @Test + fun isValidLastFalse() { + explicitTermFilter.isValid("zoo") shouldBe false + } + + @Test + fun canSearchForFalse() { + explicitTermFilter.canSearchFor("zoo") shouldBe false + } + + @Test + fun canSearchForTrue() { + explicitTermFilter.canSearchFor("android") shouldBe true + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 6f8056de13..53e1645f09 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -78,6 +78,7 @@ class UiAllScreensSanityTest { // Last passing: // 2020-11-09 // 2020-12-16 After ViewBinding huge change + // 2021-04-08 Testing 429 change @Test fun allScreensTest() { // Create an account diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt index da93d54075..015754145f 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -24,9 +24,10 @@ import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.push.fcm.FcmHelper -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.pushers.PushGatewayFailure -import org.matrix.android.sdk.api.util.Cancelable import javax.inject.Inject /** @@ -38,29 +39,31 @@ class TestPushFromPushGateway @Inject constructor(private val context: AppCompat private val pushersManager: PushersManager) : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { - private var action: Cancelable? = null + private var action: Job? = null override fun perform(activityResultLauncher: ActivityResultLauncher) { val fcmToken = FcmHelper.getFcmToken(context) ?: run { status = TestStatus.FAILED return } - action = pushersManager.testPush(fcmToken, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - description = if (failure is PushGatewayFailure.PusherRejected) { - stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_failed) - } else { - errorFormatter.toHumanReadable(failure) - } - status = TestStatus.FAILED - } - - override fun onSuccess(data: Unit) { - // Wait for the push to be received - description = stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_waiting_for_push) - status = TestStatus.RUNNING - } - }) + action = GlobalScope.launch { + status = runCatching { pushersManager.testPush(fcmToken) } + .fold( + { + // Wait for the push to be received + description = stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_waiting_for_push) + TestStatus.RUNNING + }, + { + description = if (it is PushGatewayFailure.PusherRejected) { + stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_failed) + } else { + errorFormatter.toHumanReadable(it) + } + TestStatus.FAILED + } + ) + } } override fun onPushReceived() { diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index 4d2cbecfe4..4cefeadb62 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -31,6 +31,7 @@ import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver @@ -40,6 +41,10 @@ import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.SimpleNotifiableEvent import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.Action import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event @@ -55,6 +60,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private lateinit var pusherManager: PushersManager private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var vectorPreferences: VectorPreferences + private lateinit var wifiDetector: WifiDetector + + private val coroutineScope = CoroutineScope(SupervisorJob()) // UI handler private val mUIHandler by lazy { @@ -69,6 +77,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { pusherManager = pusherManager() activeSessionHolder = activeSessionHolder() vectorPreferences = vectorPreferences() + wifiDetector = wifiDetector() } } @@ -78,6 +87,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * @param message the message */ override fun onMessageReceived(message: RemoteMessage) { + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("## onMessageReceived() %s", message.data.toString()) + } + Timber.d("## onMessageReceived() from FCM with priority %s", message.priority) + // Diagnostic Push if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { val intent = Intent(NotificationUtils.PUSH_ACTION) @@ -90,14 +104,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { return } - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.i("## onMessageReceived() %s", message.data.toString()) - Timber.i("## onMessageReceived() from FCM with priority %s", message.priority) - } mUIHandler.post { if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { // we are in foreground, let the sync do the things? - Timber.v("PUSH received in a foreground state, ignore") + Timber.d("PUSH received in a foreground state, ignore") } else { onMessageReceivedInternal(message.data) } @@ -140,7 +150,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun onMessageReceivedInternal(data: Map) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.i("## onMessageReceivedInternal() : $data") + Timber.d("## onMessageReceivedInternal() : $data") + } else { + Timber.d("## onMessageReceivedInternal() : $data") } // update the badge counter @@ -156,9 +168,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val roomId = data["room_id"] if (isEventAlreadyKnown(eventId, roomId)) { - Timber.i("Ignoring push, event already known") + Timber.d("Ignoring push, event already known") } else { - Timber.v("Requesting background sync") + // Try to get the Event content faster + Timber.d("Requesting event in fast lane") + getEventFastLane(session, roomId, eventId) + + Timber.d("Requesting background sync") session.requireBackgroundSync() } } @@ -167,6 +183,36 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } } + private fun getEventFastLane(session: Session, roomId: String?, eventId: String?) { + roomId?.takeIf { it.isNotEmpty() } ?: return + eventId?.takeIf { it.isNotEmpty() } ?: return + + // If the room is currently displayed, we will not show a notification, so no need to get the Event faster + if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(roomId)) { + return + } + + if (wifiDetector.isConnectedToWifi().not()) { + Timber.d("No WiFi network, do not get Event") + return + } + + coroutineScope.launch { + Timber.d("Fast lane: start request") + val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch + + val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event) + + resolvedEvent + ?.also { Timber.d("Fast lane: notify drawer") } + ?.let { + it.isPushGatewayEvent = true + notificationDrawerManager.onNotifiableEventReceived(it) + notificationDrawerManager.refreshNotificationDrawer() + } + } + } + // check if the event was not yet received // a previous catchup might have already retrieved the notified event private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean { diff --git a/vector/src/main/assets/forbidden_terms.txt b/vector/src/main/assets/forbidden_terms.txt new file mode 100644 index 0000000000..84e7fe1d28 --- /dev/null +++ b/vector/src/main/assets/forbidden_terms.txt @@ -0,0 +1,68 @@ +anal +bbw +bdsm +beast +bestiality +blowjob +bondage +boobs +clit +cock +cuck +cum +cunt +daddy +dick +dildo +erotic +exhibitionism +faggot +femboy +fisting +flogging +fmf +foursome +futa +gangbang +gore +h3ntai +handjob +hentai +incest +jizz +kink +loli +m4f +masturbate +masturbation +mfm +milf +moresome +naked +neet +nsfw +nude +nudity +orgy +pedo +pegging +penis +petplay +porn +pussy +rape +rimming +sadism +sadomasochism +sexy +shota +spank +squirt +strap-on +threesome +vagina +vibrator +voyeur +watersports +xxx +zoo diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 1e92f7bc67..edec704f18 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -19,78 +19,26 @@ package im.vector.app import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent -import arrow.core.Option -import im.vector.app.features.grouplist.ALL_COMMUNITIES_GROUP_ID -import im.vector.app.features.grouplist.SelectedGroupDataSource -import im.vector.app.features.home.HomeRoomListDataSource -import im.vector.app.features.home.room.list.ChronologicalRoomComparator -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable -import io.reactivex.functions.BiFunction -import io.reactivex.rxkotlin.addTo -import org.matrix.android.sdk.api.session.group.model.GroupSummary -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.rx.rx -import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton /** - * This class handles the global app state. At the moment, it only manages room list. + * This class handles the global app state. * It requires to be added to ProcessLifecycleOwner.get().lifecycle */ +// TODO Keep this class for now, will maybe be used fro Space @Singleton -class AppStateHandler @Inject constructor( - private val sessionDataSource: ActiveSessionDataSource, - private val homeRoomListDataSource: HomeRoomListDataSource, - private val selectedGroupDataSource: SelectedGroupDataSource, - private val chronologicalRoomComparator: ChronologicalRoomComparator) : LifecycleObserver { +class AppStateHandler @Inject constructor() : LifecycleObserver { private val compositeDisposable = CompositeDisposable() @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { - observeRoomsAndGroup() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun entersBackground() { compositeDisposable.clear() } - - private fun observeRoomsAndGroup() { - Observable - .combineLatest, Option, List>( - sessionDataSource.observe() - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { - val query = roomSummaryQueryParams {} - it.orNull()?.rx()?.liveRoomSummaries(query) - ?: Observable.just(emptyList()) - } - .throttleLast(300, TimeUnit.MILLISECONDS), - selectedGroupDataSource.observe(), - BiFunction { rooms, selectedGroupOption -> - val selectedGroup = selectedGroupOption.orNull() - val filteredRooms = rooms.filter { - if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) { - true - } else if (it.isDirect) { - it.otherMemberIds - .intersect(selectedGroup.userIds) - .isNotEmpty() - } else { - selectedGroup.roomIds.contains(it.roomId) - } - } - filteredRooms.sortedWith(chronologicalRoomComparator) - } - ) - .subscribe { - homeRoomListDataSource.post(it) - } - .addTo(compositeDisposable) - } } diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 23d6b618fe..4b88ff6767 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -26,6 +26,7 @@ import im.vector.app.EmojiCompatWrapper import im.vector.app.VectorApplication import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.DimensionConverter @@ -35,7 +36,6 @@ import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.HomeRoomListDataSource import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder @@ -113,8 +113,6 @@ interface VectorComponent { fun errorFormatter(): ErrorFormatter - fun homeRoomListObservableStore(): HomeRoomListDataSource - fun selectedGroupStore(): SelectedGroupDataSource fun roomDetailPendingActionStore(): RoomDetailPendingActionStore @@ -143,6 +141,8 @@ interface VectorComponent { fun vectorPreferences(): VectorPreferences + fun wifiDetector(): WifiDetector + fun vectorFileLogger(): VectorFileLogger fun uiStateRepository(): UiStateRepository diff --git a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt index b77670ba76..c51573bf21 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt @@ -16,6 +16,7 @@ package im.vector.app.core.epoxy +import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -25,6 +26,17 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents abstract class TimelineEmptyItem : VectorEpoxyModel(), ItemWithEvents { @EpoxyAttribute lateinit var eventId: String + @EpoxyAttribute var notBlank: Boolean = false + + override fun isVisible() = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.updateLayoutParams { + // Force height to 1px so scrolling works correctly + this.height = if (notBlank) 1 else 0 + } + } override fun getEventIds(): List { return listOf(eventId) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index a323ce995b..5ff7a07e3c 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -19,6 +19,7 @@ package im.vector.app.core.epoxy.bottomsheet import android.text.method.MovementMethod import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -27,6 +28,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess +import im.vector.app.features.media.ImageContentRenderer import org.matrix.android.sdk.api.util.MatrixItem /** @@ -44,6 +46,12 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.id.bottom_sheet_message_preview_avatar) val sender by bind(R.id.bottom_sheet_message_preview_sender) val body by bind(R.id.bottom_sheet_message_preview_body) val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) + val imagePreview by bind(R.id.bottom_sheet_message_preview_image) } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt b/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt index 588063e2a4..5a6599acff 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/LiveData.kt @@ -39,7 +39,7 @@ inline fun LiveData>.observeEventFirstThrottle(owner: Lifecycle val firstThrottler = FirstThrottler(minimumInterval) this.observe(owner, EventObserver { - if (firstThrottler.canHandle()) { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { it.run(observer) } }) diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 81e81bb78a..cbc5effe44 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -28,10 +28,10 @@ import com.bumptech.glide.signature.ObjectKey import im.vector.app.core.extensions.vectorComponent import im.vector.app.core.files.LocalFilesHelper import im.vector.app.features.media.ImageContentRenderer +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import okhttp3.OkHttpClient -import org.matrix.android.sdk.api.MatrixCallback import timber.log.Timber -import java.io.File import java.io.IOException import java.io.InputStream @@ -113,21 +113,19 @@ class VectorGlideDataFetcher(context: Context, callback.onLoadFailed(IllegalArgumentException("No File service")) } // Use the file vector service, will avoid flickering and redownload after upload - fileService.downloadFile( - fileName = data.filename, - mimeType = data.mimeType, - url = data.url, - elementToDecrypt = data.elementToDecrypt, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - callback.onDataReady(data.inputStream()) - } - - override fun onFailure(failure: Throwable) { - callback.onLoadFailed(failure as? Exception ?: IOException(failure.localizedMessage)) - } - } - ) + GlobalScope.launch { + val result = runCatching { + fileService.downloadFile( + fileName = data.filename, + mimeType = data.mimeType, + url = data.url, + elementToDecrypt = data.elementToDecrypt) + } + result.fold( + { callback.onDataReady(it.inputStream()) }, + { callback.onLoadFailed(it as? Exception ?: IOException(it.localizedMessage)) } + ) + } // val url = contentUrlResolver.resolveFullSize(data.url) // ?: return // diff --git a/vector/src/main/java/im/vector/app/core/network/WifiDetector.kt b/vector/src/main/java/im/vector/app/core/network/WifiDetector.kt new file mode 100644 index 0000000000..34b2a7590d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/network/WifiDetector.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import androidx.core.content.getSystemService +import org.matrix.android.sdk.api.extensions.orFalse +import timber.log.Timber +import javax.inject.Inject + +class WifiDetector @Inject constructor( + context: Context +) { + private val connectivityManager = context.getSystemService()!! + + fun isConnectedToWifi(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connectivityManager.activeNetwork + ?.let { connectivityManager.getNetworkCapabilities(it) } + ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + .orFalse() + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.type == ConnectivityManager.TYPE_WIFI + } + .also { Timber.d("isConnected to WiFi: $it") } + } +} diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index f515060db6..258517aa39 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -127,6 +127,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScre Timber.i("onResume Fragment ${javaClass.simpleName}") } + @CallSuper + override fun onPause() { + super.onPause() + Timber.i("onPause Fragment ${javaClass.simpleName}") + } + @CallSuper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -149,7 +155,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScre super.onDestroyView() } + @CallSuper override fun onDestroy() { + Timber.i("onDestroy Fragment ${javaClass.simpleName}") uiDisposables.dispose() super.onDestroy() } diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 5fe30141d9..5896122393 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -21,8 +21,6 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable import java.util.UUID import javax.inject.Inject import kotlin.math.abs @@ -35,15 +33,14 @@ class PushersManager @Inject constructor( private val stringProvider: StringProvider, private val appNameProvider: AppNameProvider ) { - fun testPush(pushKey: String, callback: MatrixCallback): Cancelable { + suspend fun testPush(pushKey: String) { val currentSession = activeSessionHolder.getActiveSession() - return currentSession.testPush( + currentSession.testPush( stringProvider.getString(R.string.pusher_http_url), stringProvider.getString(R.string.pusher_app_id), pushKey, - TEST_EVENT_ID, - callback + TEST_EVENT_ID ) } @@ -64,9 +61,9 @@ class PushersManager @Inject constructor( ) } - fun unregisterPusher(pushKey: String, callback: MatrixCallback) { + suspend fun unregisterPusher(pushKey: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return - currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id), callback) + currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id)) } companion object { diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index f7d7b3864e..9ab3b9bf45 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -41,7 +41,11 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: vectorPreferences.neverShowLongClickOnRoomHelpAgain() } - fun shouldShowRoomMemberStateEvents(): Boolean { - return vectorPreferences.showRoomMemberStateEvents() + fun shouldShowJoinLeaves(): Boolean { + return vectorPreferences.showJoinLeaveMessages() + } + + fun shouldShowAvatarDisplayNameChanges(): Boolean { + return vectorPreferences.showAvatarDisplayNameChangeMessages() } } diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt index b3b9a39f30..66907ded10 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt @@ -16,62 +16,7 @@ package im.vector.app.core.utils -import java.util.regex.Pattern - -private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" + - "|[\uD83E\uDD00-\uD83E\uDDFF]" + - "|[\uD83D\uDE00-\uD83D\uDE4F]" + - "|[\uD83D\uDE80-\uD83D\uDEFF]" + - "|[\u2600-\u26FF]\uFE0F?" + - "|[\u2700-\u27BF]\uFE0F?" + - "|\u24C2\uFE0F?" + - "|[\uD83C\uDDE6-\uD83C\uDDFF]{1,2}" + - "|[\uD83C\uDD70\uD83C\uDD71\uD83C\uDD7E\uD83C\uDD7F\uD83C\uDD8E\uD83C\uDD91-\uD83C\uDD9A]\uFE0F?" + - "|[\u0023\u002A\u0030-\u0039]\uFE0F?\u20E3" + - "|[\u2194-\u2199\u21A9-\u21AA]\uFE0F?" + - "|[\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55]\uFE0F?" + - "|[\u2934\u2935]\uFE0F?" + - "|[\u3030\u303D]\uFE0F?" + - "|[\u3297\u3299]\uFE0F?" + - "|[\uD83C\uDE01\uD83C\uDE02\uD83C\uDE1A\uD83C\uDE2F\uD83C\uDE32-\uD83C\uDE3A\uD83C\uDE50\uD83C\uDE51]\uFE0F?" + - "|[\u203C\u2049]\uFE0F?" + - "|[\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE]\uFE0F?" + - "|[\u00A9\u00AE]\uFE0F?" + - "|[\u2122\u2139]\uFE0F?" + - "|\uD83C\uDC04\uFE0F?" + - "|\uD83C\uDCCF\uFE0F?" + - "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))") - -/* -// A hashset from all supported emoji -private var knownEmojiSet: HashSet? = null - -fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) { - GlobalScope.launch { - context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> - val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(EmojiData::class.java) - val inputAsString = input.bufferedReader().use { it.readText() } - val source = jsonAdapter.fromJson(inputAsString) - knownEmojiSet = HashSet().also { - source?.emojis?.mapTo(it) { (_, value) -> - value.emojiString() - } - } - done?.invoke() - } - } -} - -fun isSingleEmoji(string: String): Boolean { - if (knownEmojiSet == null) { - Timber.e("Known Emoji Hashset not initialized") - // use fallback regexp - return containsOnlyEmojis(string) - } - return knownEmojiSet?.contains(string) ?: false -} - */ +import com.vanniktech.emoji.EmojiUtils /** * Test if a string contains emojis. @@ -82,36 +27,8 @@ fun isSingleEmoji(string: String): Boolean { * @return true if the body contains only emojis */ fun containsOnlyEmojis(str: String?): Boolean { - var res = false - - if (str != null && str.isNotEmpty()) { - val matcher = emojisPattern.matcher(str) - - var start = -1 - var end = -1 - - while (matcher.find()) { - val nextStart = matcher.start() - - // first emoji position - if (start < 0) { - if (nextStart > 0) { - return false - } - } else { - // must not have a character between - if (nextStart != end) { - return false - } - } - start = nextStart - end = matcher.end() - } - - res = -1 != start && end == str.length - } - - return res + // Now rely on vanniktech library + return EmojiUtils.isOnlyEmojis(str) } /** diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index fa1c50f419..859df7d714 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -514,7 +514,7 @@ fun selectTxtFileToWrite( @Suppress("DEPRECATION") fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?): File? { // defines another name for the external media - val dstFileName: String + var dstFileName: String // build a filename is not provided if (null == outputFilename) { @@ -529,6 +529,9 @@ fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: Strin dstFileName = outputFilename } + // remove dangerous characters from the filename + dstFileName = dstFileName.replace(Regex("""[/\\]"""), "_") + var dstFile = File(dstDirPath, dstFileName) // if the file already exists, append a marker diff --git a/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt b/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt index 915e955fa6..004f500c4e 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FirstThrottler.kt @@ -15,6 +15,8 @@ */ package im.vector.app.core.utils +import android.os.SystemClock + /** * Simple ThrottleFirst * See https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.png @@ -22,14 +24,27 @@ package im.vector.app.core.utils class FirstThrottler(private val minimumInterval: Long = 800) { private var lastDate = 0L - fun canHandle(): Boolean { - val now = System.currentTimeMillis() - if (now > lastDate + minimumInterval) { + sealed class CanHandlerResult { + object Yes : CanHandlerResult() + data class No(val shouldWaitMillis: Long) : CanHandlerResult() + + fun waitMillis(): Long { + return when (this) { + Yes -> 0 + is No -> shouldWaitMillis + } + } + } + + fun canHandle(): CanHandlerResult { + val now = SystemClock.elapsedRealtime() + val delaySinceLast = now - lastDate + if (delaySinceLast > minimumInterval) { lastDate = now - return true + return CanHandlerResult.Yes } // Too soon - return false + return CanHandlerResult.No(minimumInterval - delaySinceLast) } } diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 143506d4df..34e73c8702 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -43,6 +43,7 @@ import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.signout.soft.SoftLogoutActivity +import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.ui.UiStateRepository import kotlinx.parcelize.Parcelize import kotlinx.coroutines.Dispatchers @@ -83,6 +84,8 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity override fun getBinding() = ActivityMainBinding.inflate(layoutInflater) + override fun getOtherThemes() = ActivityOtherThemes.Launcher + private lateinit var args: MainActivityArgs @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @@ -158,25 +161,22 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity lifecycleScope.launch { try { session.signOut(!args.isUserLoggedOut) - Timber.w("SIGN_OUT: success, start app") - sessionHolder.clearActiveSession() - doLocalCleanup(clearPreferences = true) - startNextActivityAndFinish() } catch (failure: Throwable) { displayError(failure) + return@launch } + Timber.w("SIGN_OUT: success, start app") + sessionHolder.clearActiveSession() + doLocalCleanup(clearPreferences = true) + startNextActivityAndFinish() } } args.clearCache -> { lifecycleScope.launch { - try { - session.clearCache() - doLocalCleanup(clearPreferences = false) - session.startSyncing(applicationContext) - startNextActivityAndFinish() - } catch (failure: Throwable) { - displayError(failure) - } + session.clearCache() + doLocalCleanup(clearPreferences = false) + session.startSyncing(applicationContext) + startNextActivityAndFinish() } } } @@ -212,15 +212,16 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity .setTitle(R.string.dialog_title_error) .setMessage(errorFormatter.toHumanReadable(failure)) .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } - .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } + .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish(ignoreClearCredentials = true) } .setCancelable(false) .show() } } - private fun startNextActivityAndFinish() { + private fun startNextActivityAndFinish(ignoreClearCredentials: Boolean = false) { val intent = when { args.clearCredentials + && !ignoreClearCredentials && (!args.isUserLoggedOut || args.isAccountDeactivated) -> // User has explicitly asked to log out or deactivated his account LoginActivity.newIntent(this, null) diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index 05af63d7ba..cfbdef8ffb 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -33,9 +33,7 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.identity.FoundThreePid import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber @@ -101,56 +99,56 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } } - private fun performLookup(data: List) { + private fun performLookup(contacts: List) { if (!session.identityService().getUserConsent()) { return } viewModelScope.launch { - val threePids = data.flatMap { contact -> + val threePids = contacts.flatMap { contact -> contact.emails.map { ThreePid.Email(it.email) } + contact.msisdns.map { ThreePid.Msisdn(it.phoneNumber) } } - session.identityService().lookUp(threePids, object : MatrixCallback> { - override fun onFailure(failure: Throwable) { - Timber.w(failure, "Unable to perform the lookup") - // Should not happen, but just to be sure - if (failure is IdentityServiceError.UserConsentNotProvided) { - setState { - copy(userConsent = false) - } - } - } - - override fun onSuccess(data: List) { - mappedContacts = allContacts.map { contactModel -> - contactModel.copy( - emails = contactModel.emails.map { email -> - email.copy( - matrixId = data - .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email } - ?.matrixId - ) - }, - msisdns = contactModel.msisdns.map { msisdn -> - msisdn.copy( - matrixId = data - .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber } - ?.matrixId - ) - } - ) - } + val data = try { + session.identityService().lookUp(threePids) + } catch (failure: Throwable) { + Timber.w(failure, "Unable to perform the lookup") + // Should not happen, but just to be sure + if (failure is IdentityServiceError.UserConsentNotProvided) { setState { - copy( - isBoundRetrieved = true - ) + copy(userConsent = false) } - - updateFilteredMappedContacts() } - }) + return@launch + } + + mappedContacts = allContacts.map { contactModel -> + contactModel.copy( + emails = contactModel.emails.map { email -> + email.copy( + matrixId = data + .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email } + ?.matrixId + ) + }, + msisdns = contactModel.msisdns.map { msisdn -> + msisdn.copy( + matrixId = data + .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber } + ?.matrixId + ) + } + ) + } + + setState { + copy( + isBoundRetrieved = true + ) + } + + updateFilteredMappedContacts() } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index e95f250dd3..11a30b304e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -43,7 +43,6 @@ import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.io.ByteArrayOutputStream @@ -220,13 +219,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor( withContext(Dispatchers.IO) { args.requestedSecrets.forEach { if (session.getAccountDataEvent(it) != null) { - val res = awaitCallback { callback -> - session.sharedSecretStorageService.getSecret( - name = it, - keyId = keyInfo.id, - secretKey = keySpec, - callback = callback) - } + val res = session.sharedSecretStorageService.getSecret( + name = it, + keyId = keyInfo.id, + secretKey = keySpec) decryptedSecretMap[it] = res } else { Timber.w("## Cannot find secret $it in SSSS, skip") @@ -292,13 +288,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor( withContext(Dispatchers.IO) { args.requestedSecrets.forEach { if (session.getAccountDataEvent(it) != null) { - val res = awaitCallback { callback -> - session.sharedSecretStorageService.getSecret( - name = it, - keyId = keyInfo.id, - secretKey = keySpec, - callback = callback) - } + val res = session.sharedSecretStorageService.getSecret( + name = it, + keyId = keyInfo.id, + secretKey = keySpec) decryptedSecretMap[it] = res } else { Timber.w("## Cannot find secret $it in SSSS, skip") diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 37d8bf9b46..ba2c923d8b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -30,10 +30,8 @@ import im.vector.app.R import im.vector.app.core.extensions.showPassword import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding import io.reactivex.android.schedulers.AndroidSchedulers - import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -59,8 +57,9 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( key ) .toSpannable() - .colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + // TODO Restore coloration when we will have a FAQ to open with those terms + // .colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + // .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) views.ssssPassphraseEnterEdittext.editorActionEvents() .throttleFirst(300, TimeUnit.MILLISECONDS) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt index 8fbef016cf..74bab9b0b6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -97,37 +97,31 @@ class BackupToQuadSMigrationTask @Inject constructor( when { params.passphrase?.isNotEmpty() == true -> { reportProgress(params, R.string.bootstrap_progress_generating_ssss) - awaitCallback { - quadS.generateKeyWithPassphrase( - UUID.randomUUID().toString(), - "ssss_key", - params.passphrase, - EmptyKeySigner(), - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - params.progressListener?.onProgress( - WaitingViewData( - stringProvider.getString( - R.string.bootstrap_progress_generating_ssss_with_info, - "$progress/$total") - )) - } - }, - it - ) - } + quadS.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + params.passphrase, + EmptyKeySigner(), + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString( + R.string.bootstrap_progress_generating_ssss_with_info, + "$progress/$total") + )) + } + } + ) } params.recoveryKey != null -> { reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery) - awaitCallback { - quadS.generateKey( - UUID.randomUUID().toString(), - extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) }, - "ssss_key", - EmptyKeySigner(), - it - ) - } + quadS.generateKey( + UUID.randomUUID().toString(), + extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) }, + "ssss_key", + EmptyKeySigner() + ) } else -> { return Result.IllegalParams @@ -137,14 +131,11 @@ class BackupToQuadSMigrationTask @Inject constructor( // Ok, so now we have migrated the old keybackup secret as the quadS key // Now we need to store the keybackup key in SSSS in a compatible way reportProgress(params, R.string.bootstrap_progress_storing_in_sss) - awaitCallback { - quadS.storeSecret( - KEYBACKUP_SECRET_SSSS_NAME, - curveKey.toBase64NoPadding(), - listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)), - it - ) - } + quadS.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + curveKey.toBase64NoPadding(), + listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)) + ) // save for gossiping keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index d1a1237463..70cda4bf79 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -126,25 +126,21 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}") try { - keyInfo = awaitCallback { - params.passphrase?.let { passphrase -> - ssssService.generateKeyWithPassphrase( - UUID.randomUUID().toString(), - "ssss_key", - passphrase, - EmptyKeySigner(), - null, - it - ) - } ?: run { - ssssService.generateKey( - UUID.randomUUID().toString(), - params.keySpec, - "ssss_key", - EmptyKeySigner(), - it - ) - } + keyInfo = params.passphrase?.let { passphrase -> + ssssService.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + passphrase, + EmptyKeySigner(), + null + ) + } ?: run { + ssssService.generateKey( + UUID.randomUUID().toString(), + params.keySpec, + "ssss_key", + EmptyKeySigner() + ) } } catch (failure: Failure) { Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>") @@ -159,9 +155,7 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key") try { - awaitCallback { - ssssService.setDefaultKey(keyInfo.keyId, it) - } + ssssService.setDefaultKey(keyInfo.keyId) } catch (failure: Failure) { // Maybe we could just ignore this error? Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>") @@ -183,13 +177,11 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...") - awaitCallback { - ssssService.storeSecret( - MASTER_KEY_SSSS_NAME, - mskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it - ) - } + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + mskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), @@ -197,27 +189,22 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...") - awaitCallback { - ssssService.storeSecret( - USER_SIGNING_KEY_SSSS_NAME, - uskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), - it - ) - } + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + uskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true ) ) Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...") - awaitCallback { - ssssService.storeSecret( - SELF_SIGNING_KEY_SSSS_NAME, - sskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it - ) - } + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + sskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) } catch (failure: Failure) { Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>") // Maybe we could just ignore this error? @@ -265,14 +252,12 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping") session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) - awaitCallback { - extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> - ssssService.storeSecret( - KEYBACKUP_SECRET_SSSS_NAME, - secret, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it - ) - } + extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> + ssssService.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + secret, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) } } else { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found") @@ -284,14 +269,12 @@ class BootstrapCrossSigningTask @Inject constructor( } if (isValid) { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known") - awaitCallback { - extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret -> - ssssService.storeSecret( - KEYBACKUP_SECRET_SSSS_NAME, - secret, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it - ) - } + extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret -> + ssssService.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + secret, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) } } else { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key is unknown by this session") diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 48f3f0a460..0b93120b52 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -60,7 +60,7 @@ class IncomingVerificationRequestHandler @Inject constructor( // TODO maybe check also if val uid = "kvr_${tx.transactionId}" when (tx.state) { - is VerificationTxState.OnStarted -> { + is VerificationTxState.OnStarted -> { // Add a notification for every incoming request val user = session?.getUser(tx.otherUserId) val name = user?.getBestName() ?: tx.otherUserId @@ -119,13 +119,25 @@ class IncomingVerificationRequestHandler @Inject constructor( Timber.v("## SAS verificationRequestCreated ${pr.transactionId}") // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { - val user = session?.getUser(pr.otherUserId) + // if it's a self verification for my devices, we can discard the review login alert + // if not this request will be underneath and not visible by the user... + // it will re-appear later + if (pr.otherUserId == session?.myUserId) { + // XXX this is a bit hard coded :/ + popupAlertManager.cancelAlert("review_login") + } + val user = session?.getUser(pr.otherUserId)?.toMatrixItem() val name = user?.getBestName() ?: pr.otherUserId + val description = if (name == pr.otherUserId) { + name + } else { + "$name (${pr.otherUserId})" + } val alert = VerificationVectorAlert( uniqueIdForVerificationRequest(pr), context.getString(R.string.sas_incoming_request_notif_title), - "$name(${pr.otherUserId})", + description, R.drawable.ic_shield_black, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { @@ -136,7 +148,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } ) .apply { - viewBinder = VerificationVectorAlert.ViewBinder(user?.toMatrixItem(), avatarRenderer.get()) + viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get()) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { val roomId = pr.roomId diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 04ac79d4a4..0e230c6727 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -305,8 +305,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( transactionId = action.pendingRequestTransactionId, roomId = roomId, otherUserId = request.otherUserId, - otherDeviceId = otherDevice ?: "", - callback = null + otherDeviceId = otherDevice ?: "" ) } Unit diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index bf2defafa1..11fd796534 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -35,7 +35,6 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx class DiscoverySettingsViewModel @AssistedInject constructor( @@ -123,7 +122,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { session.identityService().disconnect(it) } + session.identityService().disconnect() setState { copy( identityServer = Success(null), @@ -141,9 +140,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - val data = awaitCallback { - session.identityService().setNewIdentityServer(action.url, it) - } + val data = session.identityService().setNewIdentityServer(action.url) setState { copy( identityServer = Success(data), @@ -163,7 +160,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { identityService.startBindThreePid(action.threePid, it) } + identityService.startBindThreePid(action.threePid) changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS)) } catch (failure: Throwable) { _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) @@ -240,7 +237,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { identityService.unbindThreePid(threePid, it) } + identityService.unbindThreePid(threePid) changeThreePidState(threePid, Success(SharedState.NOT_SHARED)) } catch (failure: Throwable) { _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) @@ -256,7 +253,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { identityService.unbindThreePid(threePid, it) } + identityService.unbindThreePid(threePid) changeThreePidState(threePid, Success(SharedState.NOT_SHARED)) } catch (failure: Throwable) { _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) @@ -268,7 +265,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( private fun cancelBinding(action: DiscoverySettingsAction.CancelBinding) { viewModelScope.launch { try { - awaitCallback { identityService.cancelBindThreePid(action.threePid, it) } + identityService.cancelBindThreePid(action.threePid) changeThreePidState(action.threePid, Success(SharedState.NOT_SHARED)) changeThreePidSubmitState(action.threePid, Uninitialized) } catch (failure: Throwable) { @@ -304,9 +301,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - val data = awaitCallback> { - identityService.getShareStatus(threePids, it) - } + val data = identityService.getShareStatus(threePids) setState { copy( emailList = Success(data.filter { it.key is ThreePid.Email }.toPidInfoList()), @@ -346,9 +341,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { - identityService.submitValidationToken(action.threePid, action.code, it) - } + identityService.submitValidationToken(action.threePid, action.code) changeThreePidSubmitState(action.threePid, Uninitialized) finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(action.threePid), true) } catch (failure: Throwable) { @@ -371,7 +364,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { identityService.finalizeBindThreePid(threePid, it) } + identityService.finalizeBindThreePid(threePid) changeThreePidSubmitState(action.threePid, Uninitialized) changeThreePidState(action.threePid, Success(SharedState.SHARED)) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt index 9455b1bff4..08632a2bd1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt @@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.terms.TermsService -import org.matrix.android.sdk.internal.util.awaitCallback import java.net.UnknownHostException class SetIdentityServerViewModel @AssistedInject constructor( @@ -97,9 +96,7 @@ class SetIdentityServerViewModel @AssistedInject constructor( viewModelScope.launch { try { // First ping the identity server v2 API - awaitCallback { - mxSession.identityService().isValidIdentityServer(baseUrl, it) - } + mxSession.identityService().isValidIdentityServer(baseUrl) // Ok, next step checkTerms(baseUrl) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index ad61928509..447a567cf4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -203,9 +203,8 @@ class HomeActivityViewModel @AssistedInject constructor( _viewEvents.post( HomeActivityViewEvents.OnNewSession( session.getUser(session.myUserId)?.toMatrixItem(), - // If it's an old unverified, we should send requests - // instead of waiting for an incoming one - reAuthHelper.data != null + // Always send request instead of waiting for an incoming as per recent EW changes + false ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt index 447820ed7b..c64f9d453d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class HomeDetailAction : VectorViewModelAction { data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() + object MarkAllRoomsRead : HomeDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 4c7b7aa991..5def43b60b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -18,6 +18,8 @@ package im.vector.app.features.home import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat @@ -33,8 +35,8 @@ import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.ui.views.CurrentCallsView -import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.KeysBackupBanner +import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity @@ -49,7 +51,6 @@ import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState - import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -79,6 +80,32 @@ class HomeDetailFragment @Inject constructor( private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel + private var hasUnreadRooms = false + set(value) { + if (value != field) { + field = value + invalidateOptionsMenu() + } + } + + override fun getMenuRes() = R.menu.room_list + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_home_mark_all_as_read -> { + viewModel.handle(HomeDetailAction.MarkAllRoomsRead) + return true + } + } + + return super.onOptionsItemSelected(item) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms + super.onPrepareOptionsMenu(menu) + } + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { return FragmentHomeDetailBinding.inflate(inflater, container, false) } @@ -314,6 +341,8 @@ class HomeDetailFragment @Inject constructor( views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) views.syncStateView.render(it.syncState) + + hasUnreadRooms = it.hasUnreadMessages } private fun BadgeDrawable.render(count: Int, highlight: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index c261081055..c87b19f0e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -16,22 +16,30 @@ package im.vector.app.features.home +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.ui.UiStateRepository -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.internal.util.awaitCallback +import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx +import timber.log.Timber +import java.util.concurrent.TimeUnit /** * View model used to update the home bottom bar notification counts, observe the sync state and @@ -41,7 +49,6 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private val session: Session, private val uiStateRepository: UiStateRepository, private val selectedGroupStore: SelectedGroupDataSource, - private val homeRoomListStore: HomeRoomListDataSource, private val stringProvider: StringProvider) : VectorViewModel(initialState) { @@ -75,6 +82,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho override fun handle(action: HomeDetailAction) { when (action) { is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) + HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() } } @@ -90,6 +98,26 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho // PRIVATE METHODS ***************************************************************************** + private fun handleMarkAllRoomsRead() = withState { _ -> + // questionable to use viewmodelscope + viewModelScope.launch(Dispatchers.Default) { + val roomIds = session.getRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS + } + ) + .map { it.roomId } + try { + awaitCallback { + session.markAllAsRead(roomIds, it) + } + } catch (failure: Throwable) { + Timber.d(failure, "Failed to mark all as read") + } + } + } + private fun observeSyncState() { session.rx() .liveSyncState() @@ -113,43 +141,51 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho } private fun observeRoomSummaries() { - homeRoomListStore - .observe() - .observeOn(Schedulers.computation()) - .map { it.asSequence() } - .subscribe { summaries -> - val invitesDm = summaries - .filter { it.membership == Membership.INVITE && it.isDirect } - .count() + session.getPagedRoomSummariesLive( + roomSummaryQueryParams { + memberships = Membership.activeMemberships() + } + ) + .asObservable() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .subscribe { + val dmInvites = session.getRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.INVITE) + roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } + ).size - val invitesRoom = summaries - .filter { it.membership == Membership.INVITE && it.isDirect.not() } - .count() + val roomsInvite = session.getRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.INVITE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ).size - val peopleNotifications = summaries - .filter { it.isDirect } - .map { it.notificationCount } - .sum() - val peopleHasHighlight = summaries - .filter { it.isDirect } - .any { it.highlightCount > 0 } + val dmRooms = session.getNotificationCountForRooms( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } + ) - val roomsNotifications = summaries - .filter { !it.isDirect } - .map { it.notificationCount } - .sum() - val roomsHasHighlight = summaries - .filter { !it.isDirect } - .any { it.highlightCount > 0 } + val otherRooms = session.getNotificationCountForRooms( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ) setState { copy( - notificationCountCatchup = peopleNotifications + roomsNotifications + invitesDm + invitesRoom, - notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, - notificationCountPeople = peopleNotifications + invitesDm, - notificationHighlightPeople = peopleHasHighlight || invitesDm > 0, - notificationCountRooms = roomsNotifications + invitesRoom, - notificationHighlightRooms = roomsHasHighlight || invitesRoom > 0 + notificationCountCatchup = dmRooms.totalCount + otherRooms.totalCount + roomsInvite + dmInvites, + notificationHighlightCatchup = dmRooms.isHighlight || otherRooms.isHighlight, + notificationCountPeople = dmRooms.totalCount + dmInvites, + notificationHighlightPeople = dmRooms.isHighlight || dmInvites > 0, + notificationCountRooms = otherRooms.totalCount + roomsInvite, + notificationHighlightRooms = otherRooms.isHighlight || roomsInvite > 0, + hasUnreadMessages = dmRooms.totalCount + otherRooms.totalCount > 0 ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index f5e4bc9fa3..533c9166f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -34,5 +34,6 @@ data class HomeDetailViewState( val notificationHighlightPeople: Boolean = false, val notificationCountRooms: Int = 0, val notificationHighlightRooms: Boolean = false, + val hasUnreadMessages: Boolean = false, val syncState: SyncState = SyncState.Idle ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt index 3684a8b3f8..4a2d001e1d 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt @@ -21,36 +21,44 @@ import android.content.pm.ShortcutManager import android.os.Build import androidx.core.content.getSystemService import androidx.core.content.pm.ShortcutManagerCompat -import io.reactivex.Observable +import im.vector.app.core.di.ActiveSessionHolder import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import io.reactivex.disposables.Disposables +import org.matrix.android.sdk.api.query.RoomTagQueryFilter +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.rx.asObservable import javax.inject.Inject class ShortcutsHandler @Inject constructor( private val context: Context, - private val homeRoomListStore: HomeRoomListDataSource, - private val shortcutCreator: ShortcutCreator + private val shortcutCreator: ShortcutCreator, + private val activeSessionHolder: ActiveSessionHolder ) { fun observeRoomsAndBuildShortcuts(): Disposable { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { // No op - return Observable.empty().subscribe() + return Disposables.empty() } - return homeRoomListStore - .observe() - .distinctUntilChanged() - .observeOn(Schedulers.computation()) - .subscribe { rooms -> + return activeSessionHolder.getSafeActiveSession() + ?.getPagedRoomSummariesLive( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomTagQueryFilter = RoomTagQueryFilter(isFavorite = true, null, null) + } + ) + ?.asObservable() + ?.subscribe { rooms -> val shortcuts = rooms - .filter { room -> room.isFavorite } .take(n = 4) // Android only allows us to create 4 shortcuts .map { shortcutCreator.create(it) } ShortcutManagerCompat.removeAllDynamicShortcuts(context) ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) } + ?: Disposables.empty() } fun clearShortcuts() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt index 2810b27aa6..7c0dcbb0d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -66,7 +66,7 @@ class JumpToBottomViewVisibilityManager( } private fun maybeShowJumpToBottomViewVisibility() { - if (layoutManager.findFirstVisibleItemPosition() != 0) { + if (layoutManager.findFirstVisibleItemPosition() > 1) { jumpToBottomView.show() } else { jumpToBottomView.hide() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index a80aeb65b0..b7e2e189d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -98,6 +98,7 @@ import im.vector.app.core.ui.views.FailedMessagesWarningView import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer +import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.KeyboardStateUtils import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.app.core.utils.TextUtils @@ -137,6 +138,7 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem @@ -168,12 +170,12 @@ import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event @@ -199,7 +201,6 @@ import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import timber.log.Timber -import java.io.File import java.net.URL import java.util.UUID import java.util.concurrent.TimeUnit @@ -223,6 +224,7 @@ class RoomDetailFragment @Inject constructor( private val eventHtmlRenderer: EventHtmlRenderer, private val vectorPreferences: VectorPreferences, private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, private val notificationUtils: NotificationUtils, private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, @@ -873,6 +875,15 @@ class RoomDetailFragment @Inject constructor( } views.composerLayout.views.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) + // Image Event + val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66)) + val isImageVisible = if (data != null) { + imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, views.composerLayout.views.composerRelatedMessageImage) + true + } else { + false + } + updateComposerText(defaultContent) views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) @@ -884,6 +895,7 @@ class RoomDetailFragment @Inject constructor( if (isAdded) { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() + views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible } } focusComposerAndShowKeyboard() @@ -1193,7 +1205,6 @@ class RoomDetailFragment @Inject constructor( if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages - scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) views.inviteView.visibility = View.GONE if (state.tombstoneEvent == null) { @@ -1663,16 +1674,14 @@ class RoomDetailFragment @Inject constructor( if (action.messageContent is MessageTextContent) { shareText(requireContext(), action.messageContent.body) } else if (action.messageContent is MessageWithAttachmentContent) { - session.fileService().downloadFile( - messageContent = action.messageContent, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - if (isAdded) { - shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri())) - } - } - } - ) + lifecycleScope.launch { + val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } + if (!isAdded) return@launch + result.fold( + { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) }, + { showErrorInSnackbar(it) } + ) + } } } @@ -1693,22 +1702,24 @@ class RoomDetailFragment @Inject constructor( sharedActionViewModel.pendingAction = action return } - session.fileService().downloadFile( - messageContent = action.messageContent, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - if (isAdded) { - saveMedia( - context = requireContext(), - file = data, - title = action.messageContent.body, - mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), data.toUri()), - notificationUtils = notificationUtils - ) - } + lifecycleScope.launch { + val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } + if (!isAdded) return@launch + result.fold( + { + saveMedia( + context = requireContext(), + file = it, + title = action.messageContent.body, + mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), + notificationUtils = notificationUtils + ) + }, + { + showErrorInSnackbar(it) } - } - ) + ) + } } private fun handleActions(action: EventSharedAction) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index af3d5461ef..6152562850 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -65,7 +65,6 @@ import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService @@ -97,16 +96,13 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent -import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber -import java.io.File import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -181,7 +177,12 @@ class RoomDetailViewModel @AssistedInject constructor( observePowerLevel() updateShowDialerOptionState() room.getRoomSummaryLive() - room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) + viewModelScope.launch { + try { + room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) + } catch (_: Exception) { + } + } // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) callManager.addPstnSupportListener(this) @@ -483,9 +484,7 @@ class RoomDetailViewModel @AssistedInject constructor( ) try { - val widget = awaitCallback { - session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent, it) - } + val widget = session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) @@ -499,7 +498,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) viewModelScope.launch(Dispatchers.IO) { try { - awaitCallback { session.widgetService().destroyRoomWidget(room.roomId, widgetId, it) } + session.widgetService().destroyRoomWidget(room.roomId, widgetId) // local echo setState { copy( @@ -546,7 +545,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun stopTrackingUnreadMessages() { if (trackUnreadMessages.getAndSet(false)) { mostRecentDisplayedEvent?.root?.eventId?.also { - room.setReadMarker(it, callback = NoOpMatrixCallback()) + viewModelScope.launch { + try { + room.setReadMarker(it) + } catch (_: Exception) { + } + } } mostRecentDisplayedEvent = null } @@ -816,7 +820,7 @@ class RoomDetailViewModel @AssistedInject constructor( } }.exhaustive } - is SendMode.EDIT -> { + is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId if (inReplyTo != null) { @@ -839,7 +843,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } - is SendMode.QUOTE -> { + is SendMode.QUOTE -> { val messageContent = state.sendMode.timelineEvent.getLastMessageContent() val textMsg = messageContent?.body @@ -860,7 +864,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } - is SendMode.REPLY -> { + is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _viewEvents.post(RoomDetailViewEvents.MessageSent) @@ -940,14 +944,14 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - launchSlashCommandFlow { - room.invite(invite.userId, invite.reason, it) + launchSlashCommandFlowSuspendable { + room.invite(invite.userId, invite.reason) } } private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { - launchSlashCommandFlow { - room.invite3pid(invite.threePid, it) + launchSlashCommandFlowSuspendable { + room.invite3pid(invite.threePid) } } @@ -965,26 +969,26 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { - launchSlashCommandFlow { - session.setDisplayName(session.myUserId, changeDisplayName.displayName, it) + launchSlashCommandFlowSuspendable { + session.setDisplayName(session.myUserId, changeDisplayName.displayName) } } private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { - launchSlashCommandFlow { - room.kick(kick.userId, kick.reason, it) + launchSlashCommandFlowSuspendable { + room.kick(kick.userId, kick.reason) } } private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { - launchSlashCommandFlow { - room.ban(ban.userId, ban.reason, it) + launchSlashCommandFlowSuspendable { + room.ban(ban.userId, ban.reason) } } private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { - launchSlashCommandFlow { - room.unban(unban.userId, unban.reason, it) + launchSlashCommandFlowSuspendable { + room.unban(unban.userId, unban.reason) } } @@ -1088,11 +1092,21 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleRejectInvite() { - room.leave(null, NoOpMatrixCallback()) + viewModelScope.launch { + try { + room.leave(null) + } catch (_: Exception) { + } + } } private fun handleAcceptInvite() { - room.join(callback = NoOpMatrixCallback()) + viewModelScope.launch { + try { + room.join() + } catch (_: Exception) { + } + } } private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { @@ -1140,41 +1154,32 @@ class RoomDetailViewModel @AssistedInject constructor( )) } } else { - session.fileService().downloadFile( - messageContent = action.messageFileContent, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - _viewEvents.post(RoomDetailViewEvents.DownloadFileState( - action.messageFileContent.mimeType, - data, - null - )) - } + viewModelScope.launch { + val result = runCatching { + session.fileService().downloadFile(messageContent = action.messageFileContent) + } - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.DownloadFileState( - action.messageFileContent.mimeType, - null, - failure - )) - } - }) + _viewEvents.post(RoomDetailViewEvents.DownloadFileState( + action.messageFileContent.mimeType, + result.getOrNull(), + result.exceptionOrNull() + )) + } } } private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { stopTrackingUnreadMessages() val targetEventId: String = action.eventId - val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId - val indexOfEvent = timeline.getIndexOfEvent(correctedEventId) + val indexOfEvent = timeline.getIndexOfEvent(targetEventId) if (indexOfEvent == null) { // Event is not already in RAM timeline.restartWithEventId(targetEventId) } if (action.highlight) { - setState { copy(highlightedEventId = correctedEventId) } + setState { copy(highlightedEventId = targetEventId) } } - _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId)) + _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId)) } private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { @@ -1248,14 +1253,21 @@ class RoomDetailViewModel @AssistedInject constructor( } } bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> - room.setReadReceipt(eventId, callback = NoOpMatrixCallback()) + viewModelScope.launch { + room.setReadReceipt(eventId) + } } }) .disposeOnClear() } private fun handleMarkAllAsRead() { - room.markAsRead(ReadService.MarkAsReadParams.BOTH, NoOpMatrixCallback()) + viewModelScope.launch { + try { + room.markAsRead(ReadService.MarkAsReadParams.BOTH) + } catch (_: Exception) { + } + } } private fun handleReportContent(action: RoomDetailAction.ReportContent) { @@ -1275,15 +1287,15 @@ class RoomDetailViewModel @AssistedInject constructor( return } - session.ignoreUserIds(listOf(action.userId), object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + viewModelScope.launch { + val event = try { + session.ignoreUserIds(listOf(action.userId)) + RoomDetailViewEvents.ActionSuccess(action) + } catch (failure: Throwable) { + RoomDetailViewEvents.ActionFailure(action, failure) } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) - } - }) + _viewEvents.post(event) + } } private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { @@ -1398,15 +1410,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown - val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) - val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) - if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) { - return if (timeline.isLive) { - UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) - } else { - UnreadState.Unknown - } - } + val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot) + ?: return if (timeline.isLive) { + UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) + } else { + UnreadState.Unknown + } for (i in (firstDisplayableEventIndex - 1) downTo 0) { val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 0eb02f5c75..5d3a91f18d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import org.matrix.android.sdk.api.session.room.timeline.Timeline import timber.log.Timber import java.util.concurrent.atomic.AtomicReference @@ -33,8 +32,6 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView, private val scheduledEventId = AtomicReference() - var timeline: Timeline? = null - override fun onInserted(position: Int, count: Int) { scrollIfNeeded() } @@ -45,9 +42,7 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView, private fun scrollIfNeeded() { val eventId = scheduledEventId.get() ?: return - val nonNullTimeline = timeline ?: return - val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId) - val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId) + val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) if (positionToScroll != null) { val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt index fbf9ebe32f..249618e12f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -20,7 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents -import timber.log.Timber +import org.matrix.android.sdk.api.extensions.tryOrNull import java.util.concurrent.CopyOnWriteArrayList class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, @@ -38,24 +38,27 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, } override fun onInserted(position: Int, count: Int) { + if (position != 0) { + return + } if (forceScroll) { forceScroll = false - layoutManager.scrollToPosition(position) + layoutManager.scrollToPosition(0) return } - Timber.v("On inserted $count count at position: $position") - if (layoutManager.findFirstVisibleItemPosition() != position) { + if (layoutManager.findFirstVisibleItemPosition() > 1) { return } - val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return + val firstNewItem = tryOrNull { + timelineEventController.adapter.getModelAtPosition(position) + } as? ItemWithEvents ?: return val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) if (indexOfFirstNewItem != -1) { - Timber.v("Should scroll to position: $position") - repeat(newTimelineEventIds.size - indexOfFirstNewItem) { - newTimelineEventIds.removeAt(indexOfFirstNewItem) + while (newTimelineEventIds.lastOrNull() != firstNewItemIds) { + newTimelineEventIds.removeLastOrNull() } - layoutManager.scrollToPosition(position) + layoutManager.scrollToPosition(0) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt index cb93cf95d2..fb3abf002e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt @@ -24,26 +24,24 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.search.SearchResult -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.util.awaitCallback class SearchViewModel @AssistedInject constructor( @Assisted private val initialState: SearchViewState, session: Session ) : VectorViewModel(initialState) { - private var room: Room? = session.getRoom(initialState.roomId) + private val room = session.getRoom(initialState.roomId) - private var currentTask: Cancelable? = null + private var currentTask: Job? = null private var nextBatch: String? = null @@ -92,6 +90,7 @@ class SearchViewModel @AssistedInject constructor( } private fun startSearching(isNextBatch: Boolean) = withState { state -> + if (room == null) return@withState if (state.searchTerm == null) return@withState // There is no batch to retrieve @@ -108,20 +107,17 @@ class SearchViewModel @AssistedInject constructor( currentTask?.cancel() - viewModelScope.launch { + currentTask = viewModelScope.launch { try { - val result = awaitCallback { - currentTask = room?.search( - searchTerm = state.searchTerm, - nextBatch = nextBatch, - orderByRecent = true, - beforeLimit = 0, - afterLimit = 0, - includeProfile = true, - limit = 20, - callback = it - ) - } + val result = room.search( + searchTerm = state.searchTerm, + nextBatch = nextBatch, + orderByRecent = true, + beforeLimit = 0, + afterLimit = 0, + includeProfile = true, + limit = 20 + ) onSearchResultSuccess(result) } catch (failure: Throwable) { if (failure is Failure.Cancelled) return@launch diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 44f1e9b759..b67527c24c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,17 +31,21 @@ import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory +import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem @@ -49,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer @@ -58,6 +63,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent @@ -65,8 +71,6 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject -private const val DEFAULT_PREFETCH_THRESHOLD = 30 - class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val vectorPreferences: VectorPreferences, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, @@ -77,7 +81,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val session: Session, private val callManager: WebRtcCallManager, @TimelineEventControllerHandler - private val backgroundHandler: Handler + private val backgroundHandler: Handler, + private val userPreferencesProvider: UserPreferencesProvider, + private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, + private val readReceiptsItemFactory: ReadReceiptsItemFactory ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { interface Callback : @@ -147,7 +154,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var unreadState: UnreadState = UnreadState.Unknown private var positionOfReadMarker: Int? = null private var eventIdToHighlight: String? = null - private var previousModelsSize = 0 var callback: Callback? = null var timeline: Timeline? = null @@ -198,7 +204,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val interceptorHelper = TimelineControllerInterceptorHelper( ::positionOfReadMarker, adapterPositionMapping, - vectorPreferences, + userPreferencesProvider, callManager ) @@ -311,7 +317,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { cacheItemData.eventModel } - listOf(eventModel, + listOf( + cacheItemData?.readReceiptsItem?.takeUnless { mergedHeaderItemFactory.isCollapsed(cacheItemData.localId) }, + eventModel, cacheItemData?.mergedHeaderModel, cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null } ) @@ -323,61 +331,128 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildCacheItemsIfNeeded() = synchronized(modelCache) { hasUTD = false hasReachedInvite = false - if (modelCache.isEmpty()) { return } + val receiptsByEvents = getReadReceiptsByShownEvent() + val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents) (0 until modelCache.size).forEach { position -> - // Should be build if not cached or if cached but contains additional models - // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { - modelCache[position] = buildCacheItem(position, currentSnapshot) + val event = currentSnapshot[position] + val nextEvent = currentSnapshot.nextOrNull(position) + val prevEvent = currentSnapshot.prevOrNull(position) + val params = TimelineItemFactoryParams( + event = event, + prevEvent = prevEvent, + nextEvent = nextEvent, + highlightedEventId = eventIdToHighlight, + lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, + callback = callback + ) + // Should be build if not cached or if model should be refreshed + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) { + modelCache[position] = buildCacheItem(params) } + val itemCachedData = modelCache[position] ?: return@forEach + // Then update with additional models if needed + modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents) } } - private fun buildCacheItem(currentPosition: Int, items: List): CacheItemData { - val event = items[currentPosition] - val nextEvent = items.nextOrNull(currentPosition) - val prevEvent = items.prevOrNull(currentPosition) + private fun buildCacheItem(params: TimelineItemFactoryParams): CacheItemData { + val event = params.event if (hasReachedInvite && hasUTD) { - return CacheItemData(event.localId, event.root.eventId, null, null, null) + return CacheItemData(event.localId, event.root.eventId) } - updateUTDStates(event, nextEvent) - val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also { + updateUTDStates(event, params.nextEvent) + val eventModel = timelineItemFactory.create(params).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val addDaySeparator = if (hasReachedInvite && hasUTD) { - true - } else { - val date = event.root.localDateTime() - val nextDate = nextEvent?.root?.localDateTime() - date.toLocalDate() != nextDate?.toLocalDate() - } + val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT + return CacheItemData( + localId = event.localId, + eventId = event.root.eventId, + eventModel = eventModel, + shouldTriggerBuild = shouldTriggerBuild) + } + + private fun CacheItemData.enrichWithModels(event: TimelineEvent, + nextEvent: TimelineEvent?, + position: Int, + receiptsByEvents: Map>): CacheItemData { + val wantsDateSeparator = wantsDateSeparator(event, nextEvent) val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - currentPosition = currentPosition, + items = this@TimelineEventController.currentSnapshot, + addDaySeparator = wantsDateSeparator, + currentPosition = position, eventIdToHighlight = eventIdToHighlight, callback = callback ) { requestModelBuild() } - val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs) - // If we have a SENT decoration, we want to built again as it might have to be changed to NONE if more recent event has also SENT decoration - val forceTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT - return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, forceTriggerBuild) - } - - private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? { - return if (addDaySeparator) { - val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER) - DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) + val formattedDayModel = if (wantsDateSeparator) { + buildDaySeparatorItem(event.root.originServerTs) } else { null } + val readReceipts = receiptsByEvents[event.eventId].orEmpty() + return copy( + readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback), + formattedDayModel = formattedDayModel, + mergedHeaderModel = mergedHeaderModel + ) + } + + private fun searchLastSentEventWithoutReadReceipts(receiptsByEvent: Map>): String? { + if (timeline?.isLive == false) { + // If timeline is not live we don't want to show SentStatus + return null + } + for (event in currentSnapshot) { + // If there is any RR on the event, we stop searching for Sent event + if (receiptsByEvent[event.eventId]?.isNotEmpty() == true) { + return null + } + // If the event is not shown, we go to the next one + if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { + continue + } + // If the event is sent by us, we update the holder with the eventId and stop the search + if (event.root.senderId == session.myUserId && event.root.sendState.isSent()) { + return event.eventId + } + } + return null + } + + private fun getReadReceiptsByShownEvent(): Map> { + val receiptsByEvent = HashMap>() + if (!userPreferencesProvider.shouldShowReadReceipts()) { + return receiptsByEvent + } + var lastShownEventId: String? = null + val itr = currentSnapshot.listIterator(currentSnapshot.size) + while (itr.hasPrevious()) { + val event = itr.previous() + val currentReadReceipts = ArrayList(event.readReceipts).filter { + it.user.userId != session.myUserId + } + if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { + lastShownEventId = event.eventId + } + if (lastShownEventId == null) { + continue + } + val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() } + existingReceipts.addAll(currentReadReceipts) + } + return receiptsByEvent + } + + private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem { + val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER) + return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) } private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { @@ -409,6 +484,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } + private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean { + return if (hasReachedInvite && hasUTD) { + true + } else { + val date = event.root.localDateTime() + val nextDate = nextEvent?.root?.localDateTime() + date.toLocalDate() != nextDate?.toLocalDate() + } + } + /** * Return true if added */ @@ -429,14 +514,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private data class CacheItemData( val localId: Long, val eventId: String?, + val readReceiptsItem: ReadReceiptsItem? = null, val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null, val formattedDayModel: DaySeparatorItem? = null, - val forceTriggerBuild: Boolean = false - ) { - fun shouldTriggerBuild(): Boolean { - // Since those items can change when we paginate, force a re-build - return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null - } - } + val shouldTriggerBuild: Boolean = false + ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 4e1492aaba..30587e6659 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -29,11 +29,14 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify +import im.vector.app.features.media.ImageContentRenderer import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState import javax.inject.Inject @@ -45,6 +48,8 @@ class MessageActionsEpoxyController @Inject constructor( private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer, private val fontProvider: EmojiCompatFontProvider, + private val imageContentRenderer: ImageContentRenderer, + private val dimensionConverter: DimensionConverter, private val dateFormatter: VectorDateFormatter ) : TypedEpoxyController() { @@ -59,6 +64,8 @@ class MessageActionsEpoxyController @Inject constructor( avatarRenderer(avatarRenderer) matrixItem(state.informationData.matrixItem) movementMethod(createLinkMovementMethod(listener)) + imageContentRenderer(imageContentRenderer) + data(state.timelineEvent()?.buildImageContentRendererData(dimensionConverter.dpToPx(66))) userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) } body(state.messageBody.linkify(listener)) time(formattedDate) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index ac1c2258aa..b5d3102f46 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -24,6 +24,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.canReact import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -65,6 +66,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private val htmlCompressor: VectorHtmlCompressor, private val session: Session, private val noticeEventFormatter: NoticeEventFormatter, + private val errorFormatter: ErrorFormatter, private val stringProvider: StringProvider, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val vectorPreferences: VectorPreferences @@ -171,42 +173,46 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence { - if (timelineEvent.root.isRedacted()) { - return noticeEventFormatter.formatRedactedEvent(timelineEvent.root) - } + return try { + if (timelineEvent.root.isRedacted()) { + noticeEventFormatter.formatRedactedEvent(timelineEvent.root) + } else { + when (timelineEvent.root.getClearType()) { + EventType.MESSAGE, + EventType.STICKER -> { + val messageContent: MessageContent? = timelineEvent.getLastMessageContent() + if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { + val html = messageContent.formattedBody + ?.takeIf { it.isNotBlank() } + ?.let { htmlCompressor.compress(it) } + ?: messageContent.body - return when (timelineEvent.root.getClearType()) { - EventType.MESSAGE, - EventType.STICKER -> { - val messageContent: MessageContent? = timelineEvent.getLastMessageContent() - if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { - val html = messageContent.formattedBody - ?.takeIf { it.isNotBlank() } - ?.let { htmlCompressor.compress(it) } - ?: messageContent.body - - eventHtmlRenderer.get().render(html, pillsPostProcessor) - } else if (messageContent is MessageVerificationRequestContent) { - stringProvider.getString(R.string.verification_request) - } else { - messageContent?.body + eventHtmlRenderer.get().render(html, pillsPostProcessor) + } else if (messageContent is MessageVerificationRequestContent) { + stringProvider.getString(R.string.verification_request) + } else { + messageContent?.body + } + } + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_ROOM_CANONICAL_ALIAS, + EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, + EventType.CALL_INVITE, + EventType.CALL_CANDIDATES, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER -> { + noticeEventFormatter.format(timelineEvent) + } + else -> null } } - EventType.STATE_ROOM_NAME, - EventType.STATE_ROOM_TOPIC, - EventType.STATE_ROOM_AVATAR, - EventType.STATE_ROOM_MEMBER, - EventType.STATE_ROOM_ALIASES, - EventType.STATE_ROOM_CANONICAL_ALIAS, - EventType.STATE_ROOM_HISTORY_VISIBILITY, - EventType.STATE_ROOM_SERVER_ACL, - EventType.CALL_INVITE, - EventType.CALL_CANDIDATES, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> { - noticeEventFormatter.format(timelineEvent) - } - else -> null + } catch (failure: Throwable) { + errorFormatter.toHumanReadable(failure) } ?: "" } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 548f7a3b1c..3df9898078 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -46,13 +46,11 @@ class CallItemFactory @Inject constructor( private val callManager: WebRtcCallManager ) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event if (event.root.eventId == null) return null val roomId = event.roomId - val informationData = messageInformationDataFactory.create(event, null, null) + val informationData = messageInformationDataFactory.create(params) val callSignalingContent = event.getCallSignallingContent() ?: return null val callId = callSignalingContent.callId ?: return null val call = callManager.getCallById(callId) @@ -68,8 +66,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.IN_CALL, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = call != null ) @@ -80,8 +78,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.INVITED, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = call != null ) @@ -92,8 +90,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.REJECTED, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = false ) @@ -104,8 +102,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.ENDED, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = false ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 71ac46307b..db7b84ed06 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -25,7 +25,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio import im.vector.app.features.home.room.detail.timeline.item.DefaultItem import im.vector.app.features.home.room.detail.timeline.item.DefaultItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider, @@ -43,8 +42,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava text = text, itemLongClickListener = { view -> callback?.onEventLongClicked(informationData, null, view) ?: false - }, - readReceiptsCallback = callback + } ) return DefaultItem_() .leftGuideline(avatarSizeProvider.leftGuideline) @@ -52,16 +50,14 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava .attributes(attributes) } - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?, - throwable: Throwable? = null): DefaultItem { + fun create(params: TimelineItemFactoryParams, throwable: Throwable? = null): DefaultItem { + val event = params.event val text = if (throwable == null) { stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType()) } else { stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId) } - val informationData = informationDataFactory.create(event, null, null) - return create(text, informationData, highlight, callback) + val informationData = informationDataFactory.create(params) + return create(text, informationData, params.isHighlighted, params.callback) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index b531e08359..82d3dea311 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -21,7 +21,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -33,7 +32,6 @@ import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import javax.inject.Inject @@ -46,11 +44,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat private val attributesFactory: MessageItemAttributesFactory, private val vectorPreferences: VectorPreferences) { - fun create(event: TimelineEvent, - prevEvent: TimelineEvent?, - nextEvent: TimelineEvent?, - highlight: Boolean, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event event.root.eventId ?: return null return when { @@ -109,14 +104,14 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } } - val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) - val attributes = attributesFactory.create(event.root.content.toModel(), informationData, callback) + val informationData = messageInformationDataFactory.create(params) + val attributes = attributesFactory.create(event.root.content.toModel(), informationData, params.callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(highlight) + .highlighted(params.isHighlighted) .attributes(attributes) .message(spannableStr) - .movementMethod(createLinkMovementMethod(callback)) + .movementMethod(createLinkMovementMethod(params.callback)) } else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 68716a3eba..1d30136f27 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -28,7 +27,6 @@ import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineI import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import javax.inject.Inject @@ -41,15 +39,14 @@ class EncryptionItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val session: Session) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?): StatusTileTimelineItem? { + fun create(params: TimelineItemFactoryParams): StatusTileTimelineItem? { + val event = params.event if (!event.root.isStateEvent()) { return null } val algorithm = event.root.getClearContent().toModel()?.algorithm - val informationData = informationDataFactory.create(event, null, null) - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val informationData = informationDataFactory.create(params) + val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM val title: String @@ -86,7 +83,7 @@ class EncryptionItemFactory @Inject constructor( readReceiptsCallback = attributes.readReceiptsCallback ) ) - .highlighted(highlight) + .highlighted(params.isHighlighted) .leftGuideline(avatarSizeProvider.leftGuideline) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 2134645d8d..cb2a067540 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -23,9 +23,9 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration -import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ @@ -47,7 +47,8 @@ import javax.inject.Inject class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val avatarRenderer: AvatarRenderer, private val avatarSizeProvider: AvatarSizeProvider, - private val roomSummariesHolder: RoomSummariesHolder) { + private val roomSummariesHolder: RoomSummariesHolder, +private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() @@ -85,12 +86,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { - val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) - return if (prevSameTypeEvents.isEmpty()) { + val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight) + return if (mergedEvents.isEmpty()) { null } else { var highlighted = false - val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() val mergedData = ArrayList(mergedEvents.size) mergedEvents.forEach { mergedEvent -> if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { @@ -126,8 +126,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde onCollapsedStateChanged = { mergeItemCollapseStates[event.localId] = it requestModelBuild() - }, - readReceiptsCallback = callback + } ) MergedMembershipEventsItem_() .id(mergeId) @@ -205,7 +204,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde }, hasEncryptionEvent = hasEncryption, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, - readReceiptsCallback = callback, callback = callback, currentUserId = currentUserId, roomSummary = roomSummariesHolder.get(event.roomId), diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index e969998613..0f214ffb13 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -85,7 +85,6 @@ import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt @@ -118,15 +117,13 @@ class MessageItemFactory @Inject constructor( pillsPostProcessorFactory.create(roomId) } - fun create(event: TimelineEvent, - prevEvent: TimelineEvent?, - nextEvent: TimelineEvent?, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event + val highlight = params.isHighlighted + val callback = params.callback event.root.eventId ?: return null roomId = event.roomId - val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) + val informationData = messageInformationDataFactory.create(params) if (event.root.isRedacted()) { // message is redacted val attributes = messageItemAttributesFactory.create(null, informationData, callback) @@ -142,7 +139,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should display it when debugging as a notice event - return noticeItemFactory.create(event, highlight, callback) + return noticeItemFactory.create(params) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) @@ -158,7 +155,7 @@ class MessageItemFactory @Inject constructor( is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) + is MessagePollResponseContent -> noticeItemFactory.create(params) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index dfabf96199..e757b6b47b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -17,13 +17,11 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, @@ -31,24 +29,23 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv private val informationDataFactory: MessageInformationDataFactory, private val avatarSizeProvider: AvatarSizeProvider) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?): NoticeItem? { + fun create(params: TimelineItemFactoryParams): NoticeItem? { + val event = params.event val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null, null) + val informationData = informationDataFactory.create(params) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, noticeText = formattedText, itemLongClickListener = { view -> - callback?.onEventLongClicked(informationData, null, view) ?: false + params.callback?.onEventLongClicked(informationData, null, view) ?: false }, - readReceiptsCallback = callback, - avatarClickListener = { callback?.onAvatarClicked(informationData) } + readReceiptsCallback = params.callback, + avatarClickListener = { params.callback?.onAvatarClicked(informationData) } ) return NoticeItem_() .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(highlight) + .highlighted(params.isHighlighted) .attributes(attributes) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt new file mode 100644 index 0000000000..1d015d1bca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.factory + +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem +import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_ +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import javax.inject.Inject + +class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) { + + fun create(eventId: String, readReceipts: List, callback: TimelineEventController.Callback?): ReadReceiptsItem? { + if (readReceipts.isEmpty()) { + return null + } + val readReceiptsData = readReceipts + .map { + ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) + } + .toList() + + return ReadReceiptsItem_() + .id("read_receipts_$eventId") + .eventId(eventId) + .readReceipts(readReceiptsData) + .avatarRenderer(avatarRenderer) + .clickListener(DebouncedClickListener({ _ -> + callback?.onReadReceiptsClicked(readReceiptsData) + })) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 31adbdb8a6..382962f98d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -20,13 +20,11 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_ import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider, @@ -34,25 +32,26 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri private val session: Session, private val noticeItemFactory: NoticeItemFactory) { - fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event val createRoomContent = event.root.getClearContent().toModel() ?: return null - val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(event, callback) + val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params) val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null val text = span { +stringProvider.getString(R.string.room_tombstone_continuation_description) +"\n" span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) { textDecorationLine = "underline" - onClick = { callback?.onRoomCreateLinkClicked(roomLink) } + onClick = { params.callback?.onRoomCreateLinkClicked(roomLink) } } } return RoomCreateItem_() .text(text) } - private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + private fun defaultRendering(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { return if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, false, callback) + noticeItemFactory.create(params) } else { null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index ccc8289e08..47bc60eb75 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -19,8 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.epoxy.TimelineEmptyItem import im.vector.app.core.epoxy.TimelineEmptyItem_ import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import timber.log.Timber @@ -35,23 +34,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val widgetItemFactory: WidgetItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory, private val callItemFactory: CallItemFactory, - private val userPreferencesProvider: UserPreferencesProvider) { + private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { /** * Reminder: nextEvent is older and prevEvent is newer. */ - fun create(event: TimelineEvent, - prevEvent: TimelineEvent?, - nextEvent: TimelineEvent?, - eventIdToHighlight: String?, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { - val highlight = event.root.eventId == eventIdToHighlight - + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> { + val event = params.event val computedModel = try { + if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) { + return buildEmptyItem(event, params.prevEvent, params.highlightedEventId) + } when (event.root.getClearType()) { + // Message itemsX EventType.STICKER, - EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) - // State and call + EventType.MESSAGE -> messageItemFactory.create(params) EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, @@ -63,28 +60,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, - EventType.STATE_ROOM_POWER_LEVELS, - EventType.REACTION, - EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) - EventType.STATE_ROOM_WIDGET_LEGACY, - EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback) - EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) - // State room create - EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) - // Calls - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_REJECT, - EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback) - // Crypto - EventType.ENCRYPTED -> { - if (event.root.isRedacted()) { - // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) - } else { - encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback) - } - } + EventType.REDACTION, EventType.STATE_ROOM_ALIASES, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, @@ -94,37 +70,51 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_CANDIDATES, EventType.CALL_REPLACES, EventType.CALL_SELECT_ANSWER, - EventType.CALL_NEGOTIATE -> { - // TODO These are not filtered out by timeline when encrypted - // For now manually ignore - if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, highlight, callback) + EventType.CALL_NEGOTIATE, + EventType.REACTION, + EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params) + EventType.STATE_ROOM_WIDGET_LEGACY, + EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) + EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) + // State room create + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) + // Calls + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_REJECT, + EventType.CALL_ANSWER -> callItemFactory.create(params) + // Crypto + EventType.ENCRYPTED -> { + if (event.root.isRedacted()) { + // Redacted event, let the MessageItemFactory handle it + messageItemFactory.create(params) } else { - null + encryptedItemFactory.create(params) } } EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_DONE -> { - verificationConclusionItemFactory.create(event, highlight, callback) + verificationConclusionItemFactory.create(params) } - // Unhandled event types else -> { // Should only happen when shouldShowHiddenEvents() settings is ON Timber.v("Type ${event.root.getClearType()} not handled") - defaultItemFactory.create(event, highlight, callback) + defaultItemFactory.create(params) } } } catch (throwable: Throwable) { Timber.e(throwable, "failed to create message item") - defaultItemFactory.create(event, highlight, callback, throwable) + defaultItemFactory.create(params, throwable) } - return computedModel ?: buildEmptyItem(event) + return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId) } - private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem { + private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?): TimelineEmptyItem { + val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId) return TimelineEmptyItem_() .id(timelineEvent.localId) .eventId(timelineEvent.eventId) + .notBlank(isNotBlank) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt new file mode 100644 index 0000000000..f92cd2800a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.factory + +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +data class TimelineItemFactoryParams( + val event: TimelineEvent, + val prevEvent: TimelineEvent? = null, + val nextEvent: TimelineEvent? = null, + val highlightedEventId: String? = null, + val lastSentEventIdWithoutReadReceipts: String? = null, + val callback: TimelineEventController.Callback? = null +) { + val isHighlighted = highlightedEventId == event.eventId +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 960487140d..e972ddcab5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -20,7 +20,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject /** @@ -54,37 +52,35 @@ class VerificationItemFactory @Inject constructor( private val session: Session ) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event if (event.root.eventId == null) return null val relContent: MessageRelationContent = event.root.content.toModel() ?: event.root.getClearContent().toModel() - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) - if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback) + if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(params) val refEventId = relContent.relatesTo?.eventId - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) // If we cannot find the referenced request we do not display the done event val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId) - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) // If it's not a request ignore this event // if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) - val referenceInformationData = messageInformationDataFactory.create(refEvent, null, null) + val referenceInformationData = messageInformationDataFactory.create(TimelineItemFactoryParams(refEvent)) - val informationData = messageInformationDataFactory.create(event, null, null) - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val informationData = messageInformationDataFactory.create(params) + val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) when (event.root.getClearType()) { EventType.KEY_VERIFICATION_CANCEL -> { // Is the request referenced is actually really cancelled? val cancelContent = event.root.getClearContent().toModel() - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) when (safeValueOf(cancelContent.code)) { CancelCode.MismatchedCommitment, @@ -107,22 +103,22 @@ class VerificationItemFactory @Inject constructor( readReceiptsCallback = attributes.readReceiptsCallback ) ) - .highlighted(highlight) + .highlighted(params.isHighlighted) .leftGuideline(avatarSizeProvider.leftGuideline) } - else -> return ignoredConclusion(event, highlight, callback) + else -> return ignoredConclusion(params) } } EventType.KEY_VERIFICATION_DONE -> { // Is the request referenced is actually really completed? if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) { - return ignoredConclusion(event, highlight, callback) + return ignoredConclusion(params) } // We only tale the one sent by me if (informationData.sentByMe) { // We only display the done sent by the other user, the done send by me is ignored - return ignoredConclusion(event, highlight, callback) + return ignoredConclusion(params) } return StatusTileTimelineItem_() .attributes( @@ -140,18 +136,15 @@ class VerificationItemFactory @Inject constructor( readReceiptsCallback = attributes.readReceiptsCallback ) ) - .highlighted(highlight) + .highlighted(params.isHighlighted) .leftGuideline(avatarSizeProvider.leftGuideline) } } return null } - private fun ignoredConclusion(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { - if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback) + private fun ignoredConclusion(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(params) return null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index a6a88a3444..1fc57489a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -20,7 +20,6 @@ import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -29,7 +28,6 @@ import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineI import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType import javax.inject.Inject @@ -47,25 +45,24 @@ class WidgetItemFactory @Inject constructor( private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { - WidgetType.Jitsi -> createJitsiItem(event, callback, widgetContent, previousWidgetContent) + WidgetType.Jitsi -> createJitsiItem(params, widgetContent, previousWidgetContent) // There is lot of other widget types we could improve here - else -> noticeItemFactory.create(event, highlight, callback) + else -> noticeItemFactory.create(params) } } - private fun createJitsiItem(timelineEvent: TimelineEvent, - callback: TimelineEventController.Callback?, + private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent, previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { - val informationData = informationDataFactory.create(timelineEvent, null, null) - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val timelineEvent = params.event + val informationData = informationDataFactory.create(params) + val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val message = if (widgetContent.isActive()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 09f173de14..14dd311265 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -19,11 +19,11 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.extensions.localDateTime +import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData -import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.settings.VectorPreferences @@ -51,9 +51,10 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val vectorPreferences: VectorPreferences) { - fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData { - // Non nullability has been tested before - val eventId = event.root.eventId!! + fun create(params: TimelineItemFactoryParams): MessageInformationData { + val event = params.event + val nextEvent = params.nextEvent + val eventId = event.eventId val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() @@ -76,9 +77,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val isSentByMe = event.root.senderId == session.myUserId val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( - eventSendState = event.root.sendState, - prevEventSendState = prevEvent?.root?.sendState, - anyReadReceipts = event.readReceipts.any { it.user.userId != session.myUserId }, + event = event, + lastSentEventWithoutReadReceipts = params.lastSentEventIdWithoutReadReceipts, isMedia = event.root.isAttachmentMessage() ) } else { @@ -111,15 +111,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses }, hasBeenEdited = event.hasBeenEdited(), hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, - readReceipts = event.readReceipts - .asSequence() - .filter { - it.user.userId != session.myUserId - } - .map { - ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) - } - .toList(), referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary -> val verificationState = referencesAggregatedSummary.content.toModel()?.verificationState ?: VerificationState.REQUEST @@ -131,15 +122,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ) } - private fun getSendStateDecoration(eventSendState: SendState, - prevEventSendState: SendState?, - anyReadReceipts: Boolean, + private fun getSendStateDecoration(event: TimelineEvent, + lastSentEventWithoutReadReceipts: String?, isMedia: Boolean): SendStateDecoration { + val eventSendState = event.root.sendState return if (eventSendState.isSending()) { if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA } else if (eventSendState.hasFailed()) { SendStateDecoration.FAILED - } else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) { + } else if (lastSentEventWithoutReadReceipts == event.eventId) { SendStateDecoration.SENT } else { SendStateDecoration.NONE diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 971a3a35d8..72fdef9f7d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -20,13 +20,14 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.TimelineEmptyItem_ +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem +import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ -import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.room.timeline.Timeline import kotlin.reflect.KMutableProperty0 @@ -34,7 +35,7 @@ private const val DEFAULT_PREFETCH_THRESHOLD = 30 class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0, private val adapterPositionMapping: MutableMap, - private val vectorPreferences: VectorPreferences, + private val userPreferencesProvider: UserPreferencesProvider, private val callManager: WebRtcCallManager ) { @@ -56,23 +57,39 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut models.addForwardPrefetchIfNeeded(timeline, callback) val modelsIterator = models.listIterator() - val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() + val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() var index = 0 val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId + var atLeastOneVisibleItemSinceLastDaySeparator = false + var atLeastOneVisibleItemsBeforeReadMarker = false + // Then iterate on models so we have the exact positions in the adapter modelsIterator.forEach { epoxyModel -> if (epoxyModel is ItemWithEvents) { + if (epoxyModel.isVisible()) { + atLeastOneVisibleItemSinceLastDaySeparator = true + atLeastOneVisibleItemsBeforeReadMarker = true + } epoxyModel.getEventIds().forEach { eventId -> adapterPositionMapping[eventId] = index - if (eventId == firstUnreadEventId) { + if (epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) { modelsIterator.addReadMarkerItem(callback) index++ positionOfReadMarker.set(index) } } } - if (epoxyModel is CallTileTimelineItem) { - modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents) + if (epoxyModel is DaySeparatorItem) { + if (!atLeastOneVisibleItemSinceLastDaySeparator) { + modelsIterator.remove() + return@forEach + } + atLeastOneVisibleItemSinceLastDaySeparator = false + } else if (epoxyModel is CallTileTimelineItem) { + val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents) + if (!hasBeenRemoved) { + atLeastOneVisibleItemSinceLastDaySeparator = true + } } index++ } @@ -94,20 +111,23 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut epoxyModel: CallTileTimelineItem, callIds: MutableSet, showHiddenEvents: Boolean - ) { + ): Boolean { val callId = epoxyModel.attributes.callId // We should remove the call tile if we already have one for this call or // if this is an active call tile without an actual call (which can happen with permalink) val shouldRemoveCallItem = callIds.contains(callId) || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) - if (shouldRemoveCallItem && !showHiddenEvents) { + val removed = shouldRemoveCallItem && !showHiddenEvents + if (removed) { remove() val emptyItem = TimelineEmptyItem_() .id(epoxyModel.id()) .eventId(epoxyModel.attributes.informationData.eventId) + .notBlank(false) add(emptyItem) } callIds.add(callId) + return removed } private fun MutableList>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index eb5b8081f9..053b804a82 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -16,12 +16,14 @@ package im.vector.app.features.home.room.detail.timeline.helper -import im.vector.app.core.extensions.localDateTime import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent object TimelineDisplayableEvents { + /** + * All types we have an item to build with. Every type not defined here will be shown as DefaultItem if forced to be shown, otherwise will be hidden. + */ val DISPLAYABLE_TYPES = listOf( EventType.MESSAGE, EventType.STATE_ROOM_WIDGET_LEGACY, @@ -68,7 +70,7 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_POWER_LEVELS, EventType.STATE_ROOM_ENCRYPTION -> true - EventType.STATE_ROOM_MEMBER -> { + EventType.STATE_ROOM_MEMBER -> { // Keep only room member events regarding the room creator (when he joined the room), // but exclude events where the room creator invite others, or where others join roomCreatorUserId != null && root.stateKey == roomCreatorUserId @@ -76,39 +78,3 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { else -> false } } - -fun List.nextSameTypeEvents(index: Int, minSize: Int): List { - if (index >= size - 1) { - return emptyList() - } - val timelineEvent = this[index] - val nextSubList = subList(index + 1, size) - val indexOfNextDay = nextSubList.indexOfFirst { - val date = it.root.localDateTime() - val nextDate = timelineEvent.root.localDateTime() - date.toLocalDate() != nextDate.toLocalDate() - } - val nextSameDayEvents = if (indexOfNextDay == -1) { - nextSubList - } else { - nextSubList.subList(0, indexOfNextDay) - } - val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() } - val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { - nextSameDayEvents - } else { - nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) - } - if (sameTypeEvents.size < minSize) { - return emptyList() - } - return sameTypeEvents -} - -fun List.prevSameTypeEvents(index: Int, minSize: Int): List { - val prevSub = subList(0, index + 1) - return prevSub - .reversed() - .nextSameTypeEvents(0, minSize) - .reversed() -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt new file mode 100644 index 0000000000..580d7d18cf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.helper + +import im.vector.app.core.extensions.localDateTime +import im.vector.app.core.resources.UserPreferencesProvider +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { + + /** + * @param timelineEvents the events to search in + * @param index the index to start computing (inclusive) + * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list + * @param eventIdToHighlight used to compute visibility + * + * @return a list of timeline events which have sequentially the same type following the next direction. + */ + fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?): List { + if (index >= timelineEvents.size - 1) { + return emptyList() + } + val timelineEvent = timelineEvents[index] + val nextSubList = timelineEvents.subList(index, timelineEvents.size) + val indexOfNextDay = nextSubList.indexOfFirst { + val date = it.root.localDateTime() + val nextDate = timelineEvent.root.localDateTime() + date.toLocalDate() != nextDate.toLocalDate() + } + val nextSameDayEvents = if (indexOfNextDay == -1) { + nextSubList + } else { + nextSubList.subList(0, indexOfNextDay) + } + val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() } + val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { + nextSameDayEvents + } else { + nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) + } + val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight) } + if (filteredSameTypeEvents.size < minSize) { + return emptyList() + } + return filteredSameTypeEvents + } + + /** + * @param timelineEvents the events to search in + * @param index the index to start computing (inclusive) + * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list + * @param eventIdToHighlight used to compute visibility + * + * @return a list of timeline events which have sequentially the same type following the prev direction. + */ + fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?): List { + val prevSub = timelineEvents.subList(0, index + 1) + return prevSub + .reversed() + .let { + nextSameTypeEvents(it, 0, minSize, eventIdToHighlight) + } + } + + /** + * @param timelineEvent the event to check for visibility + * @param highlightedEventId can be checked to force visibility to true + * @return true if the event should be shown in the timeline. + */ + fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?): Boolean { + // If show hidden events is true we should always display something + if (userPreferencesProvider.shouldShowHiddenEvents()) { + return true + } + // We always show highlighted event + if (timelineEvent.eventId == highlightedEventId) { + return true + } + if (!timelineEvent.isDisplayable()) { + return false + } + // Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences. + return !timelineEvent.shouldBeHidden() + } + + private fun TimelineEvent.isDisplayable(): Boolean { + return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType()) + } + + private fun TimelineEvent.shouldBeHidden(): Boolean { + if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) { + return true + } + if (root.getRelationContent()?.type == RelationType.REPLACE) { + return true + } + if (root.getClearType() == EventType.STATE_ROOM_MEMBER) { + val diff = computeMembershipDiff() + if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true + if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true + } + return false + } + + private fun TimelineEvent.computeMembershipDiff(): MembershipDiff { + val content = root.getClearContent().toModel() + val prevContent = root.resolvedPrevContent().toModel() + + val isMembershipChanged = content?.membership != prevContent?.membership + val isJoin = isMembershipChanged && content?.membership == Membership.JOIN + val isPart = isMembershipChanged && content?.membership == Membership.LEAVE && root.stateKey == root.senderId + + val isProfileChanged = !isMembershipChanged && content?.membership == Membership.JOIN + val isDisplaynameChange = isProfileChanged && content?.displayName != prevContent?.displayName + val isAvatarChange = isProfileChanged && content?.avatarUrl !== prevContent?.avatarUrl + + return MembershipDiff( + isJoin = isJoin, + isPart = isPart, + isDisplaynameChange = isDisplaynameChange, + isAvatarChange = isAvatarChange + ) + } + + private data class MembershipDiff( + val isJoin: Boolean, + val isPart: Boolean, + val isDisplaynameChange: Boolean, + val isAvatarChange: Boolean + ) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt index 01c7ad3986..3aee65bf19 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt @@ -17,48 +17,14 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.resources.UserPreferencesProvider -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter -import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import javax.inject.Inject -class TimelineSettingsFactory @Inject constructor( - private val userPreferencesProvider: UserPreferencesProvider, - private val session: Session -) { +class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { fun create(): TimelineSettings { - return if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineSettings( - initialSize = 30, - filters = TimelineEventFilters( - filterEdits = false, - filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), - filterUseless = false, - filterTypes = false), - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) - } else { - val allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES.createAllowedEventTypeFilters() - TimelineSettings( - initialSize = 30, - filters = TimelineEventFilters( - filterEdits = true, - filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), - filterUseless = true, - filterTypes = true, - allowedTypes = allowedTypes), - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) - } - } - - private fun List.createAllowedEventTypeFilters(): List { - return map { - EventTypeFilter( - eventType = it, - stateKey = if (it == EventType.STATE_ROOM_MEMBER && !userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null - ) - } + return TimelineSettings( + initialSize = 30, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt new file mode 100644 index 0000000000..7ff184f664 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.image + +import im.vector.app.features.media.ImageContentRenderer +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt + +fun TimelineEvent.buildImageContentRendererData(maxHeight: Int): ImageContentRenderer.Data? { + return when { + root.isImageMessage() -> root.getClearContent().toModel() + ?.let { messageImageContent -> + ImageContentRenderer.Data( + eventId = eventId, + filename = messageImageContent.body, + mimeType = messageImageContent.mimeType, + url = messageImageContent.getFileUrl(), + elementToDecrypt = messageImageContent.encryptedFileInfo?.toElementToDecrypt(), + height = messageImageContent.info?.height, + maxHeight = maxHeight, + width = messageImageContent.info?.width, + maxWidth = maxHeight * 2, + allowNonMxcUrls = false + ) + } + root.isVideoMessage() -> root.getClearContent().toModel() + ?.let { messageVideoContent -> + ImageContentRenderer.Data( + eventId = eventId, + filename = messageVideoContent.body, + mimeType = messageVideoContent.mimeType, + url = messageVideoContent.getFileUrl(), + elementToDecrypt = messageVideoContent.encryptedFileInfo?.toElementToDecrypt(), + height = messageVideoContent.videoInfo?.height, + maxHeight = maxHeight, + width = messageVideoContent.videoInfo?.width, + maxWidth = maxHeight * 2, + allowNonMxcUrls = false + ) + } + else -> null + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index a65f1e10f2..39c04af089 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -24,7 +24,6 @@ import androidx.annotation.IdRes import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.ui.views.ShieldImageView -import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -41,10 +40,6 @@ abstract class AbsBaseMessageItem : BaseEventItem abstract val baseAttributes: Attributes - private val _readReceiptsClickListener = DebouncedClickListener({ - baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts) - }) - private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true) @@ -69,12 +64,6 @@ abstract class AbsBaseMessageItem : BaseEventItem override fun bind(holder: H) { super.bind(holder) - holder.readReceiptsView.render( - baseAttributes.informationData.readReceipts, - baseAttributes.avatarRenderer, - _readReceiptsClickListener - ) - val reactions = baseAttributes.informationData.orderedReactionList if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { holder.reactionsContainer.isVisible = false @@ -111,7 +100,6 @@ abstract class AbsBaseMessageItem : BaseEventItem override fun unbind(holder: H) { holder.reactionsContainer.setOnLongClickListener(null) - holder.readReceiptsView.unbind(baseAttributes.avatarRenderer) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index 13bb6db6ef..7d539f9df7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -26,7 +26,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.platform.CheckableView -import im.vector.app.core.ui.views.ReadReceiptsView import im.vector.app.core.utils.DimensionConverter /** @@ -56,7 +55,6 @@ abstract class BaseEventItem : VectorEpoxyModel abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) - val readReceiptsView by bind(R.id.readReceiptsView) override fun bindView(itemView: View) { super.bindView(itemView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt index 1f8ad3df1b..1c56a0809e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt @@ -19,10 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.TextView import androidx.annotation.IdRes -import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.util.MatrixItem abstract class BasedMergedItem : BaseEventItem() { @@ -41,8 +39,6 @@ abstract class BasedMergedItem : BaseEventItem() holder.separatorView.visibility = View.VISIBLE holder.expandView.setText(R.string.merged_events_collapse) } - // No read receipt for this item - holder.readReceiptsView.isVisible = false } protected val distinctMergeData by lazy { @@ -72,7 +68,6 @@ abstract class BasedMergedItem : BaseEventItem() val isCollapsed: Boolean val mergeData: List val avatarRenderer: AvatarRenderer - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? val onCollapsedStateChanged: (Boolean) -> Unit } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt index cdc677334e..e6c6e1d372 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt @@ -22,9 +22,7 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class DefaultItem : BaseEventItem() { @@ -32,21 +30,15 @@ abstract class DefaultItem : BaseEventItem() { @EpoxyAttribute lateinit var attributes: Attributes - private val _readReceiptsClickListener = DebouncedClickListener({ - attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) - }) - override fun bind(holder: Holder) { super.bind(holder) holder.messageTextView.text = attributes.text attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) holder.view.setOnLongClickListener(attributes.itemLongClickListener) - holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) } override fun unbind(holder: Holder) { attributes.avatarRenderer.clear(holder.avatarImageView) - holder.readReceiptsView.unbind(attributes.avatarRenderer) super.unbind(holder) } @@ -65,8 +57,7 @@ abstract class DefaultItem : BaseEventItem() { val avatarRenderer: AvatarRenderer, val informationData: MessageInformationData, val text: CharSequence, - val itemLongClickListener: View.OnLongClickListener? = null, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + val itemLongClickListener: View.OnLongClickListener? = null ) companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt index cf4211bb2c..050cba0d56 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt @@ -22,4 +22,8 @@ interface ItemWithEvents { * Will generally get only one, but it handles the merged items. */ fun getEventIds(): List + + fun canAppendReadMarker(): Boolean = true + + fun isVisible(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt index ef4a6662b4..a52ddf8336 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt @@ -21,12 +21,10 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.children -import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedMembershipEventsItem : BasedMergedItem() { @@ -56,8 +54,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem, override val avatarRenderer: AvatarRenderer, - override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val onCollapsedStateChanged: (Boolean) -> Unit ) : BasedMergedItem.Attributes } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 6a665bb44f..9faef589ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -92,8 +92,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem, override val avatarRenderer: AvatarRenderer, - override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val onCollapsedStateChanged: (Boolean) -> Unit, val callback: TimelineEventController.Callback? = null, val currentUserId: String, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 67b79bab9b..08aa301538 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -36,10 +36,8 @@ data class MessageInformationData( /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, val pollResponseAggregatedSummary: PollResponseData? = null, - val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList(), val referencesInfoData: ReferencesInfoData? = null, val sentByMe: Boolean, val e2eDecoration: E2EDecoration = E2EDecoration.NONE, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index bcf170dc4d..4876e8e500 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -25,7 +25,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick import im.vector.app.core.ui.views.ShieldImageView -import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel @@ -36,16 +35,11 @@ abstract class NoticeItem : BaseEventItem() { @EpoxyAttribute lateinit var attributes: Attributes - private val _readReceiptsClickListener = DebouncedClickListener({ - attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) - }) - override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = attributes.noticeText attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) holder.view.setOnLongClickListener(attributes.itemLongClickListener) - holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener) when (attributes.informationData.e2eDecoration) { @@ -62,7 +56,6 @@ abstract class NoticeItem : BaseEventItem() { override fun unbind(holder: Holder) { attributes.avatarRenderer.clear(holder.avatarImageView) - holder.readReceiptsView.unbind(attributes.avatarRenderer) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt new file mode 100644 index 0000000000..b88afb0598 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import android.view.View +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.ui.views.ReadReceiptsView +import im.vector.app.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts) +abstract class ReadReceiptsItem : EpoxyModelWithHolder(), ItemWithEvents { + + @EpoxyAttribute lateinit var eventId: String + @EpoxyAttribute lateinit var readReceipts: List + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: View.OnClickListener + + override fun canAppendReadMarker(): Boolean = false + + override fun getEventIds(): List = listOf(eventId) + + override fun bind(holder: Holder) { + super.bind(holder) + holder.readReceiptsView.render(readReceipts, avatarRenderer, clickListener) + } + + override fun unbind(holder: Holder) { + holder.readReceiptsView.unbind(avatarRenderer) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val readReceiptsView by bind(R.id.readReceiptsView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt index df75c0094b..4c018c8a99 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt @@ -52,7 +52,7 @@ class PreviewUrlRetriever(session: Session, // The event is not known or it has been edited // Keep only the first URL for the moment val url = mediaService.extractUrls(event) - .firstOrNull() + .firstOrNull { canShowUrlPreview(it) } ?.takeIf { it !in blockedUrl } if (url == null) { updateState(eventId, latestEventId, PreviewUrlUiState.NoUrl) @@ -98,6 +98,10 @@ class PreviewUrlRetriever(session: Session, } } + private fun canShowUrlPreview(url: String): Boolean { + return blockedDomains.all { !url.startsWith(it) } + } + fun doNotShowPreviewUrlFor(eventId: String, url: String) { blockedUrl.add(url) @@ -143,5 +147,12 @@ class PreviewUrlRetriever(session: Session, companion object { // One week in millis private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000 + + private val blockedDomains = listOf( + "https://matrix.to", + "https://app.element.io", + "https://staging.element.io", + "https://develop.element.io" + ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index 4a6c1c16fc..883efb2e60 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -22,12 +22,11 @@ import org.matrix.android.sdk.api.session.room.notification.RoomNotificationStat sealed class RoomListAction : VectorViewModelAction { data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction() - data class ToggleCategory(val category: RoomCategory) : RoomListAction() + data class ToggleSection(val section: RoomsSection) : RoomListAction() data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction() data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction() data class FilterWith(val filter: String) : RoomListAction() data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction() - object MarkAllRoomsRead : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt new file mode 100644 index 0000000000..d4e062d1e4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.epoxy.helpFooterItem +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.resources.UserPreferencesProvider +import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.home.room.filtered.filteredRoomFooterItem +import javax.inject.Inject + +class RoomListFooterController @Inject constructor( + private val stringProvider: StringProvider, + private val userPreferencesProvider: UserPreferencesProvider +) : TypedEpoxyController() { + + var listener: RoomListListener? = null + + override fun buildModels(data: RoomListViewState?) { + when (data?.displayMode) { + RoomListDisplayMode.FILTERED -> { + filteredRoomFooterItem { + id("filter_footer") + listener(listener) + currentFilter(data.roomFilter) + } + } + else -> { + if (userPreferencesProvider.shouldShowLongClickOnRoomHelp()) { + helpFooterItem { + id("long_click_help") + text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options)) + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 30cb360a9d..aaa5bbcde5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -20,19 +20,15 @@ import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.OnModelBuildFinishedListener -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -44,6 +40,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.databinding.FragmentRoomListBinding import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.actions.RoomListActionsArgs @@ -53,8 +50,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.notifications.NotificationDrawerManager import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState @@ -66,12 +62,13 @@ data class RoomListParams( ) : Parcelable class RoomListFragment @Inject constructor( - private val roomController: RoomSummaryController, + private val pagedControllerFactory: RoomSummaryPagedControllerFactory, val roomListViewModelFactory: RoomListViewModel.Factory, private val notificationDrawerManager: NotificationDrawerManager, - private val sharedViewPool: RecyclerView.RecycledViewPool + private val footerController: RoomListFooterController, + private val userPreferencesProvider: UserPreferencesProvider ) : VectorBaseFragment(), - RoomSummaryController.Listener, + RoomListListener, OnBackPressed, NotifsFabMenuView.Listener { @@ -85,28 +82,25 @@ class RoomListFragment @Inject constructor( return FragmentRoomListBinding.inflate(inflater, container, false) } - private var hasUnreadRooms = false + data class SectionKey( + val name: String, + val isExpanded: Boolean, + val notifyOfLocalEcho: Boolean + ) - override fun getMenuRes() = R.menu.room_list + data class SectionAdapterInfo( + var section: SectionKey, + val headerHeaderAdapter: SectionHeaderAdapter, + val contentAdapter: RoomSummaryPagedController + ) - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_home_mark_all_as_read -> { - roomListViewModel.handle(RoomListAction.MarkAllRoomsRead) - return true - } - } - - return super.onOptionsItemSelected(item) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms - super.onPrepareOptionsMenu(menu) - } + private val adapterInfosList = mutableListOf() + private var concatAdapter : ConcatAdapter? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + views.stateView.contentView = views.roomListView + views.stateView.state = StateView.State.Loading setupCreateRoomButton() setupRecyclerView() sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) @@ -125,6 +119,40 @@ class RoomListFragment @Inject constructor( .observe() .subscribe { handleQuickActions(it) } .disposeOnDestroyView() + + roomListViewModel.selectSubscribe(viewLifecycleOwner, RoomListViewState::roomMembershipChanges) { ms -> + // it's for invites local echo + adapterInfosList.filter { it.section.notifyOfLocalEcho } + .onEach { + it.contentAdapter.roomChangeMembershipStates = ms + } + } + } + + private fun refreshCollapseStates() { + var contentInsertIndex = 1 + roomListViewModel.sections.forEachIndexed { index, roomsSection -> + val actualBlock = adapterInfosList[index] + val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue() + if (actualBlock.section.isExpanded && !isRoomSectionExpanded) { + // we have to remove the content adapter + concatAdapter?.removeAdapter(actualBlock.contentAdapter.adapter) + } else if (!actualBlock.section.isExpanded && isRoomSectionExpanded) { + // we must add it back! + concatAdapter?.addAdapter(contentInsertIndex, actualBlock.contentAdapter.adapter) + } + contentInsertIndex = if (isRoomSectionExpanded) { + contentInsertIndex + 2 + } else { + contentInsertIndex + 1 + } + actualBlock.section = actualBlock.section.copy( + isExpanded = isRoomSectionExpanded + ) + actualBlock.headerHeaderAdapter.updateSection( + actualBlock.headerHeaderAdapter.roomsSectionData.copy(isExpanded = isRoomSectionExpanded) + ) + } } override fun showFailure(throwable: Throwable) { @@ -132,12 +160,15 @@ class RoomListFragment @Inject constructor( } override fun onDestroyView() { - roomController.removeModelBuildListener(modelBuildListener) + adapterInfosList.onEach { it.contentAdapter.removeModelBuildListener(modelBuildListener) } + adapterInfosList.clear() modelBuildListener = null views.roomListView.cleanup() - roomController.listener = null + footerController.listener = null + // TODO Cleanup listener on the ConcatAdapter's adapters? stateRestorer.clear() views.createChatFabMenu.listener = null + concatAdapter = null super.onDestroyView() } @@ -204,13 +235,58 @@ class RoomListFragment @Inject constructor( stateRestorer = LayoutManagerStateRestorer(layoutManager).register() views.roomListView.layoutManager = layoutManager views.roomListView.itemAnimator = RoomListAnimator() - views.roomListView.setRecycledViewPool(sharedViewPool) layoutManager.recycleChildrenOnDetach = true - roomController.listener = this + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } - roomController.addModelBuildListener(modelBuildListener) - views.roomListView.adapter = roomController.adapter - views.stateView.contentView = views.roomListView + + val concatAdapter = ConcatAdapter() + + roomListViewModel.sections.forEach { section -> + val sectionAdapter = SectionHeaderAdapter { + roomListViewModel.handle(RoomListAction.ToggleSection(section)) + }.also { + it.updateSection(SectionHeaderAdapter.RoomsSectionData(section.sectionName)) + } + + val contentAdapter = pagedControllerFactory.createRoomSummaryPagedController() + .also { controller -> + section.livePages.observe(viewLifecycleOwner) { pl -> + controller.submitList(pl) + sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(isHidden = pl.isEmpty())) + checkEmptyState() + } + section.notificationCount.observe(viewLifecycleOwner) { counts -> + sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( + notificationCount = counts.totalCount, + isHighlighted = counts.isHighlight + )) + } + section.isExpanded.observe(viewLifecycleOwner) { _ -> + refreshCollapseStates() + } + controller.listener = this + } + adapterInfosList.add( + SectionAdapterInfo( + SectionKey( + name = section.sectionName, + isExpanded = section.isExpanded.value.orTrue(), + notifyOfLocalEcho = section.notifyOfLocalEcho + ), + sectionAdapter, + contentAdapter + ) + ) + concatAdapter.addAdapter(sectionAdapter) + concatAdapter.addAdapter(contentAdapter.adapter) + } + + // Add the footer controller + footerController.listener = this + concatAdapter.addAdapter(footerController.adapter) + + this.concatAdapter = concatAdapter + views.roomListView.adapter = concatAdapter } private val showFabRunnable = Runnable { @@ -278,89 +354,41 @@ class RoomListFragment @Inject constructor( } override fun invalidate() = withState(roomListViewModel) { state -> - when (state.asyncFilteredRooms) { - is Incomplete -> renderLoading() - is Success -> renderSuccess(state) - is Fail -> renderFailure(state.asyncFilteredRooms.error) - } - roomController.update(state) - // Mark all as read menu - when (roomListParams.displayMode) { - RoomListDisplayMode.NOTIFICATIONS, - RoomListDisplayMode.PEOPLE, - RoomListDisplayMode.ROOMS -> { - val newValue = state.hasUnread - if (hasUnreadRooms != newValue) { - hasUnreadRooms = newValue - invalidateOptionsMenu() - } - } - else -> Unit - } + footerController.setData(state) } - private fun renderSuccess(state: RoomListViewState) { - val allRooms = state.asyncRooms() - val filteredRooms = state.asyncFilteredRooms() - if (filteredRooms.isNullOrEmpty()) { - renderEmptyState(allRooms) - } else { - views.stateView.state = StateView.State.Content - } - } - - private fun renderEmptyState(allRooms: List?) { - val hasNoRoom = allRooms - ?.filter { - it.membership == Membership.JOIN || it.membership == Membership.INVITE - } - .isNullOrEmpty() - val emptyState = when (roomListParams.displayMode) { - RoomListDisplayMode.NOTIFICATIONS -> { - if (hasNoRoom) { - StateView.State.Empty( - title = getString(R.string.room_list_catchup_welcome_title), - image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_catchup), - message = getString(R.string.room_list_catchup_welcome_body) - ) - } else { + private fun checkEmptyState() { + val hasNoRoom = adapterInfosList.all { it.headerHeaderAdapter.roomsSectionData.isHidden } + if (hasNoRoom) { + val emptyState = when (roomListParams.displayMode) { + RoomListDisplayMode.NOTIFICATIONS -> { StateView.State.Empty( title = getString(R.string.room_list_catchup_empty_title), image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper), message = getString(R.string.room_list_catchup_empty_body)) } + RoomListDisplayMode.PEOPLE -> + StateView.State.Empty( + title = getString(R.string.room_list_people_empty_title), + image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_dm), + isBigImage = true, + message = getString(R.string.room_list_people_empty_body) + ) + RoomListDisplayMode.ROOMS -> + StateView.State.Empty( + title = getString(R.string.room_list_rooms_empty_title), + image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_room), + isBigImage = true, + message = getString(R.string.room_list_rooms_empty_body) + ) + else -> + // Always display the content in this mode, because if the footer + StateView.State.Content } - RoomListDisplayMode.PEOPLE -> - StateView.State.Empty( - title = getString(R.string.room_list_people_empty_title), - image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_dm), - isBigImage = true, - message = getString(R.string.room_list_people_empty_body) - ) - RoomListDisplayMode.ROOMS -> - StateView.State.Empty( - title = getString(R.string.room_list_rooms_empty_title), - image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_room), - isBigImage = true, - message = getString(R.string.room_list_rooms_empty_body) - ) - else -> - // Always display the content in this mode, because if the footer - StateView.State.Content + views.stateView.state = emptyState + } else { + views.stateView.state = StateView.State.Content } - views.stateView.state = emptyState - } - - private fun renderLoading() { - views.stateView.state = StateView.State.Loading - } - - private fun renderFailure(error: Throwable) { - val message = when (error) { - is Failure.NetworkConnection -> getString(R.string.network_error_please_check_and_retry) - else -> getString(R.string.unknown_error) - } - views.stateView.state = StateView.State.Error(message) } override fun onBackPressed(toolbarButton: Boolean): Boolean { @@ -377,7 +405,11 @@ class RoomListFragment @Inject constructor( } override fun onRoomLongClicked(room: RoomSummary): Boolean { - roomController.onRoomLongClicked() + userPreferencesProvider.neverShowLongClickOnRoomHelpAgain() + withState(roomListViewModel) { + // refresh footer + footerController.setData(it) + } RoomListQuickActionsBottomSheet .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") @@ -394,10 +426,6 @@ class RoomListFragment @Inject constructor( roomListViewModel.handle(RoomListAction.RejectInvitation(room)) } - override fun onToggleRoomCategory(roomCategory: RoomCategory) { - roomListViewModel.handle(RoomListAction.ToggleCategory(roomCategory)) - } - override fun createRoom(initialName: String) { navigator.openCreateRoom(requireActivity(), initialName) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt new file mode 100644 index 0000000000..e9833d1560 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +interface RoomListListener : FilteredRoomFooterItem.FilteredRoomFooterItemListener { + fun onRoomClicked(room: RoomSummary) + fun onRoomLongClicked(room: RoomSummary): Boolean + fun onRejectRoomInvitation(room: RoomSummary) + fun onAcceptRoomInvitation(room: RoomSummary) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 6e5081a31c..423a950591 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -16,38 +16,61 @@ package im.vector.app.features.home.room.list +import androidx.annotation.StringRes import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext +import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.utils.DataSource +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.RoomListDisplayMode import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.state.isPublic +import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx import timber.log.Timber -import java.lang.Exception import javax.inject.Inject -class RoomListViewModel @Inject constructor(initialState: RoomListViewState, - private val session: Session, - private val roomSummariesSource: DataSource>) - : VectorViewModel(initialState) { +class RoomListViewModel @Inject constructor( + initialState: RoomListViewState, + private val session: Session, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { interface Factory { fun create(initialState: RoomListViewState): RoomListViewModel } + private var updatableQuery: UpdatableFilterLivePageResult? = null + + init { + observeMembershipChanges() + } + + private fun observeMembershipChanges() { + session.rx() + .liveRoomChangeMembershipState() + .subscribe { + setState { copy(roomMembershipChanges = it) } + } + .disposeOnClear() + } + companion object : MvRxViewModelFactory { @JvmStatic @@ -57,28 +80,136 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } } - private val displayMode = initialState.displayMode - private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode) + val sections: List by lazy { + val sections = mutableListOf() + if (initialState.displayMode == RoomListDisplayMode.PEOPLE) { + addSection(sections, R.string.invitations_header, true) { + it.memberships = listOf(Membership.INVITE) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } - init { - observeRoomSummaries() - observeMembershipChanges() + addSection(sections, R.string.bottom_action_favourites) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM + it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null) + } + + addSection(sections, R.string.bottom_action_people_x) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } + } else if (initialState.displayMode == RoomListDisplayMode.ROOMS) { + addSection(sections, R.string.invitations_header, true) { + it.memberships = listOf(Membership.INVITE) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + + addSection(sections, R.string.bottom_action_favourites) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null) + } + + addSection(sections, R.string.bottom_action_rooms) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + it.roomTagQueryFilter = RoomTagQueryFilter(false, false, false) + } + + addSection(sections, R.string.low_priority_header) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + it.roomTagQueryFilter = RoomTagQueryFilter(null, true, null) + } + + addSection(sections, R.string.system_alerts_header) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + it.roomTagQueryFilter = RoomTagQueryFilter(null, null, true) + } + } else if (initialState.displayMode == RoomListDisplayMode.FILTERED) { + withQueryParams( + { + it.memberships = Membership.activeMemberships() + }, + { qpm -> + val name = stringProvider.getString(R.string.bottom_action_rooms) + session.getFilteredPagedRoomSummariesLive(qpm) + .let { updatableFilterLivePageResult -> + updatableQuery = updatableFilterLivePageResult + sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList)) + } + } + ) + } else if (initialState.displayMode == RoomListDisplayMode.NOTIFICATIONS) { + addSection(sections, R.string.invitations_header, true) { + it.memberships = listOf(Membership.INVITE) + it.roomCategoryFilter = RoomCategoryFilter.ALL + } + + addSection(sections, R.string.bottom_action_rooms, true) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS + } + } + + sections } override fun handle(action: RoomListAction) { when (action) { is RoomListAction.SelectRoom -> handleSelectRoom(action) - is RoomListAction.ToggleCategory -> handleToggleCategory(action) is RoomListAction.AcceptInvitation -> handleAcceptInvitation(action) is RoomListAction.RejectInvitation -> handleRejectInvitation(action) is RoomListAction.FilterWith -> handleFilter(action) - is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomListAction.ToggleTag -> handleToggleTag(action) + is RoomListAction.ToggleSection -> handleToggleSection(action.section) }.exhaustive } + private fun addSection(sections: MutableList, + @StringRes nameRes: Int, + notifyOfLocalEcho: Boolean = false, + query: (RoomSummaryQueryParams.Builder) -> Unit) { + withQueryParams( + { query.invoke(it) }, + { roomQueryParams -> + + val name = stringProvider.getString(nameRes) + session.getPagedRoomSummariesLive(roomQueryParams) + .let { livePagedList -> + + // use it also as a source to update count + livePagedList.asObservable() + .observeOn(Schedulers.computation()) + .subscribe { + sections.find { it.sectionName == name } + ?.notificationCount + ?.postValue(session.getNotificationCountForRooms(roomQueryParams)) + } + .disposeOnClear() + + sections.add( + RoomsSection( + sectionName = name, + livePages = livePagedList, + notifyOfLocalEcho = notifyOfLocalEcho + ) + ) + } + } + ) + } + + private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { + RoomSummaryQueryParams.Builder() + .apply { builder.invoke(this) } + .build() + .let { block(it) } + } + fun isPublicRoom(roomId: String): Boolean { return session.getRoom(roomId)?.isPublic().orFalse() } @@ -89,8 +220,14 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary)) } - private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState { - this.toggle(action.category) + private fun handleToggleSection(roomSection: RoomsSection) { + roomSection.isExpanded.postValue(!roomSection.isExpanded.value.orFalse()) + /* TODO Cleanup if it is working + sections.find { it.sectionName == roomSection.sectionName } + ?.let { section -> + section.isExpanded.postValue(!section.isExpanded.value.orFalse()) + } + */ } private fun handleFilter(action: RoomListAction.FilterWith) { @@ -99,23 +236,12 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, roomFilter = action.filter ) } - } - - private fun observeRoomSummaries() { - roomSummariesSource - .observe() - .observeOn(Schedulers.computation()) - .execute { asyncRooms -> - copy(asyncRooms = asyncRooms) - } - - roomSummariesSource - .observe() - .observeOn(Schedulers.computation()) - .map { buildRoomSummaries(it) } - .execute { async -> - copy(asyncFilteredRooms = async) + updatableQuery?.updateQuery( + roomSummaryQueryParams { + memberships = Membership.activeMemberships() + displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) } + ) } private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state -> @@ -127,17 +253,30 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, return@withState } - session.getRoom(roomId)?.join(callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { + // quick echo + setState { + copy( + roomMembershipChanges = roomMembershipChanges.mapValues { + if (it.key == roomId) { + ChangeMembershipState.Joining + } else { + it.value + } + } + ) + } + + val room = session.getRoom(roomId) ?: return@withState + viewModelScope.launch { + try { + room.join() // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined - } - - override fun onFailure(failure: Throwable) { + } catch (failure: Throwable) { // Notify the user _viewEvents.post(RoomListViewEvents.Failure(failure)) } - }) + } } private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state -> @@ -149,28 +288,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, return@withState } - session.getRoom(roomId)?.leave(null, object : MatrixCallback { - override fun onSuccess(data: Unit) { + val room = session.getRoom(roomId) ?: return@withState + viewModelScope.launch { + try { + room.leave(null) // We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data. // Instead, we wait for the room to be rejected // Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons. // If we update the state, the button will be displayed again, so it's not ideal... - } - - override fun onFailure(failure: Throwable) { + } catch (failure: Throwable) { // Notify the user _viewEvents.post(RoomListViewEvents.Failure(failure)) } - }) - } - - private fun handleMarkAllRoomsRead() = withState { state -> - state.asyncFilteredRooms.invoke() - ?.flatMap { it.value } - ?.filter { it.membership == Membership.JOIN } - ?.map { it.roomId } - ?.toList() - ?.let { session.markAllAsRead(it, NoOpMatrixCallback()) } + } } private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) { @@ -220,56 +350,11 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) { _viewEvents.post(RoomListViewEvents.Loading(null)) - session.getRoom(action.roomId)?.leave(null, object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomListViewEvents.Done) - } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomListViewEvents.Failure(failure)) - } - }) - } - - private fun observeMembershipChanges() { - session.rx() - .liveRoomChangeMembershipState() - .subscribe { - Timber.v("ChangeMembership states: $it") - setState { copy(roomMembershipChanges = it) } - } - .disposeOnClear() - } - - private fun buildRoomSummaries(rooms: List): RoomSummaries { - // Set up init size on directChats and groupRooms as they are the biggest ones - val invites = ArrayList() - val favourites = ArrayList() - val directChats = ArrayList(rooms.size) - val groupRooms = ArrayList(rooms.size) - val lowPriorities = ArrayList() - val serverNotices = ArrayList() - - rooms - .filter { roomListDisplayModeFilter.test(it) } - .forEach { room -> - val tags = room.tags.map { it.name } - when { - room.membership == Membership.INVITE -> invites.add(room) - tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room) - tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room) - tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room) - room.isDirect -> directChats.add(room) - else -> groupRooms.add(room) - } - } - return RoomSummaries().apply { - put(RoomCategory.INVITE, invites) - put(RoomCategory.FAVOURITE, favourites) - put(RoomCategory.DIRECT, directChats) - put(RoomCategory.GROUP, groupRooms) - put(RoomCategory.LOW_PRIORITY, lowPriorities) - put(RoomCategory.SERVER_NOTICE, serverNotices) + val room = session.getRoom(action.roomId) ?: return + viewModelScope.launch { + val value = runCatching { room.leave(null) } + .fold({ RoomListViewEvents.Done }, { RoomListViewEvents.Failure(it) }) + _viewEvents.post(value) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt index 44ca8cefda..d36bc45ab6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt @@ -16,20 +16,20 @@ package im.vector.app.features.home.room.list -import im.vector.app.features.home.HomeRoomListDataSource +import im.vector.app.core.resources.StringProvider import org.matrix.android.sdk.api.session.Session import javax.inject.Inject import javax.inject.Provider class RoomListViewModelFactory @Inject constructor(private val session: Provider, - private val homeRoomListDataSource: Provider) + private val stringProvider: StringProvider) : RoomListViewModel.Factory { override fun create(initialState: RoomListViewState): RoomListViewModel { return RoomListViewModel( initialState, session.get(), - homeRoomListDataSource.get() + stringProvider ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt index 095262d74b..104a3710f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt @@ -16,73 +16,15 @@ package im.vector.app.features.home.room.list -import androidx.annotation.StringRes -import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized -import im.vector.app.R import im.vector.app.features.home.RoomListDisplayMode import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomSummary data class RoomListViewState( val displayMode: RoomListDisplayMode, - val asyncRooms: Async> = Uninitialized, val roomFilter: String = "", - val asyncFilteredRooms: Async = Uninitialized, - val roomMembershipChanges: Map = emptyMap(), - val isInviteExpanded: Boolean = true, - val isFavouriteRoomsExpanded: Boolean = true, - val isDirectRoomsExpanded: Boolean = true, - val isGroupRoomsExpanded: Boolean = true, - val isLowPriorityRoomsExpanded: Boolean = true, - val isServerNoticeRoomsExpanded: Boolean = true + val roomMembershipChanges: Map = emptyMap() ) : MvRxState { constructor(args: RoomListParams) : this(displayMode = args.displayMode) - - fun isCategoryExpanded(roomCategory: RoomCategory): Boolean { - return when (roomCategory) { - RoomCategory.INVITE -> isInviteExpanded - RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded - RoomCategory.DIRECT -> isDirectRoomsExpanded - RoomCategory.GROUP -> isGroupRoomsExpanded - RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded - RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded - } - } - - fun toggle(roomCategory: RoomCategory): RoomListViewState { - return when (roomCategory) { - RoomCategory.INVITE -> copy(isInviteExpanded = !isInviteExpanded) - RoomCategory.FAVOURITE -> copy(isFavouriteRoomsExpanded = !isFavouriteRoomsExpanded) - RoomCategory.DIRECT -> copy(isDirectRoomsExpanded = !isDirectRoomsExpanded) - RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded) - RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded) - RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded) - } - } - - val hasUnread: Boolean - get() = asyncFilteredRooms.invoke() - ?.flatMap { it.value } - ?.filter { it.membership == Membership.JOIN } - ?.any { it.hasUnreadMessages } - ?: false -} - -typealias RoomSummaries = LinkedHashMap> - -enum class RoomCategory(@StringRes val titleRes: Int) { - INVITE(R.string.invitations_header), - FAVOURITE(R.string.bottom_action_favourites), - DIRECT(R.string.bottom_action_people_x), - GROUP(R.string.bottom_action_rooms), - LOW_PRIORITY(R.string.low_priority_header), - SERVER_NOTICE(R.string.system_alerts_header) -} - -fun RoomSummaries?.isNullOrEmpty(): Boolean { - return this == null || this.values.flatten().isEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt deleted file mode 100644 index d7cace9edb..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.list - -import androidx.annotation.StringRes -import com.airbnb.epoxy.EpoxyController -import im.vector.app.R -import im.vector.app.core.epoxy.helpFooterItem -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.features.home.RoomListDisplayMode -import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem -import im.vector.app.features.home.room.filtered.filteredRoomFooterItem -import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import javax.inject.Inject - -class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider, - private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val roomListNameFilter: RoomListNameFilter, - private val userPreferencesProvider: UserPreferencesProvider -) : EpoxyController() { - - var listener: Listener? = null - - private var viewState: RoomListViewState? = null - - init { - // We are requesting a model build directly as the first build of epoxy is on the main thread. - // It avoids to build the whole list of rooms on the main thread. - requestModelBuild() - } - - fun update(viewState: RoomListViewState) { - this.viewState = viewState - requestModelBuild() - } - - fun onRoomLongClicked() { - userPreferencesProvider.neverShowLongClickOnRoomHelpAgain() - requestModelBuild() - } - - override fun buildModels() { - val nonNullViewState = viewState ?: return - when (nonNullViewState.displayMode) { - RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState) - else -> buildRooms(nonNullViewState) - } - } - - private fun buildFilteredRooms(viewState: RoomListViewState) { - val summaries = viewState.asyncRooms() ?: return - - roomListNameFilter.filter = viewState.roomFilter - - val filteredSummaries = summaries - .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) } - - buildRoomModels(filteredSummaries, - viewState.roomMembershipChanges, - emptySet()) - - addFilterFooter(viewState) - } - - private fun buildRooms(viewState: RoomListViewState) { - var showHelp = false - val roomSummaries = viewState.asyncFilteredRooms() - roomSummaries?.forEach { (category, summaries) -> - if (summaries.isEmpty()) { - return@forEach - } else { - val isExpanded = viewState.isCategoryExpanded(category) - buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) { - listener?.onToggleRoomCategory(category) - } - if (isExpanded) { - buildRoomModels(summaries, - viewState.roomMembershipChanges, - emptySet()) - // Never set showHelp to true for invitation - if (category != RoomCategory.INVITE) { - showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp() - } - } - } - } - - if (showHelp) { - buildLongClickHelp() - } - } - - private fun buildLongClickHelp() { - helpFooterItem { - id("long_click_help") - text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options)) - } - } - - private fun addFilterFooter(viewState: RoomListViewState) { - filteredRoomFooterItem { - id("filter_footer") - listener(listener) - currentFilter(viewState.roomFilter) - } - } - - private fun buildRoomCategory(viewState: RoomListViewState, - summaries: List, - @StringRes titleRes: Int, - isExpanded: Boolean, - mutateExpandedState: () -> Unit) { - // TODO should add some business logic later - val unreadCount = if (summaries.isEmpty()) { - 0 - } else { - summaries.map { it.notificationCount }.sumBy { i -> i } - } - val showHighlighted = summaries.any { it.highlightCount > 0 } - roomCategoryItem { - id(titleRes) - title(stringProvider.getString(titleRes)) - expanded(isExpanded) - unreadNotificationCount(unreadCount) - showHighlighted(showHighlighted) - listener { - mutateExpandedState() - update(viewState) - } - } - } - - private fun buildRoomModels(summaries: List, - roomChangedMembershipStates: Map, - selectedRoomIds: Set) { - summaries.forEach { roomSummary -> - roomSummaryItemFactory - .create(roomSummary, - roomChangedMembershipStates, - selectedRoomIds, - listener) - .addTo(this) - } - } - - interface Listener : FilteredRoomFooterItem.FilteredRoomFooterItemListener { - fun onToggleRoomCategory(roomCategory: RoomCategory) - fun onRoomClicked(room: RoomSummary) - fun onRoomLongClicked(room: RoomSummary): Boolean - fun onRejectRoomInvitation(room: RoomSummary) - fun onAcceptRoomInvitation(room: RoomSummary) - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 7d7ed1637f..fa6c970d8a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -40,7 +40,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor fun create(roomSummary: RoomSummary, roomChangeMembershipStates: Map, selectedRoomIds: Set, - listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { + listener: RoomListListener?): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> { val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown @@ -52,7 +52,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor private fun createInvitationItem(roomSummary: RoomSummary, changeMembershipState: ChangeMembershipState, - listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { + listener: RoomListListener?): VectorEpoxyModel<*> { val secondLine = if (roomSummary.isDirect) { roomSummary.inviterId } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt new file mode 100644 index 0000000000..20386d739a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.utils.createUIHandler +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import javax.inject.Inject + +class RoomSummaryPagedControllerFactory @Inject constructor( + private val roomSummaryItemFactory: RoomSummaryItemFactory +) { + + fun createRoomSummaryPagedController(): RoomSummaryPagedController { + return RoomSummaryPagedController(roomSummaryItemFactory) + } +} + +class RoomSummaryPagedController( + private val roomSummaryItemFactory: RoomSummaryItemFactory +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + var listener: RoomListListener? = null + + var roomChangeMembershipStates: Map? = null + set(value) { + field = value + // ideally we could search for visible models and update only those + requestForcedModelBuild() + } + + override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { + // for place holder if enabled + item ?: return roomSummaryItemFactory.createRoomItem( + roomSummary = RoomSummary( + roomId = "null_item_pos_$currentPosition", + name = "", + encryptionEventTs = null, + isEncrypted = false, + typingUsers = emptyList() + ), + selectedRoomIds = emptySet(), + onClick = null, + onLongClick = null + ) + + return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), listener) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt new file mode 100644 index 0000000000..71b7169814 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount + +data class RoomsSection( + val sectionName: String, + val livePages: LiveData>, + val isExpanded: MutableLiveData = MutableLiveData(true), + val notificationCount: MutableLiveData = MutableLiveData(RoomAggregateNotificationCount(0, 0)), + val notifyOfLocalEcho: Boolean = false +) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt new file mode 100644 index 0000000000..f9c5766821 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.recyclerview.widget.RecyclerView +import im.vector.app.R +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.databinding.ItemRoomCategoryBinding +import im.vector.app.features.themes.ThemeUtils + +class SectionHeaderAdapter constructor( + private val onClickAction: (() -> Unit) +) : RecyclerView.Adapter() { + + data class RoomsSectionData( + val name: String, + val isExpanded: Boolean = true, + val notificationCount: Int = 0, + val isHighlighted: Boolean = false, + val isHidden: Boolean = true + ) + + lateinit var roomsSectionData: RoomsSectionData + private set + + fun updateSection(newRoomsSectionData: RoomsSectionData) { + if (!::roomsSectionData.isInitialized || newRoomsSectionData != roomsSectionData) { + roomsSectionData = newRoomsSectionData + notifyDataSetChanged() + } + } + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int) = roomsSectionData.hashCode().toLong() + + override fun getItemViewType(position: Int) = R.layout.item_room_category + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + return VH.create(parent, this.onClickAction) + } + + override fun onBindViewHolder(holder: VH, position: Int) { + holder.bind(roomsSectionData) + } + + override fun getItemCount(): Int = if (roomsSectionData.isHidden) 0 else 1 + + class VH constructor( + private val binding: ItemRoomCategoryBinding, + onClickAction: (() -> Unit) + ) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.root.setOnClickListener(DebouncedClickListener({ + onClickAction.invoke() + })) + } + + fun bind(roomsSectionData: RoomsSectionData) { + binding.roomCategoryTitleView.text = roomsSectionData.name + val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.riotx_text_secondary) + val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white + val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { + DrawableCompat.setTint(it, tintColor) + } + binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted)) + binding.roomCategoryTitleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) + } + + companion object { + fun create(parent: ViewGroup, onClickAction: () -> Unit): VH { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_room_category, parent, false) + val binding = ItemRoomCategoryBinding.bind(view) + return VH(binding, onClickAction) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index 03c87c5eec..956cf1615d 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -16,18 +16,24 @@ package im.vector.app.features.login +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible +import im.vector.app.BuildConfig import im.vector.app.databinding.FragmentLoginSplashBinding +import im.vector.app.features.settings.VectorPreferences import javax.inject.Inject /** * In this screen, the user is viewing an introduction to what he can do with this application */ -class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { +class LoginSplashFragment @Inject constructor( + private val vectorPreferences: VectorPreferences +) : AbstractLoginFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplashBinding { return FragmentLoginSplashBinding.inflate(inflater, container, false) @@ -41,6 +47,14 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment { - session.resolveUser(userId, it) - } - } + return tryOrNull { session.resolveUser(userId) } // Create raw user in case the user is not searchable ?: User(userId, null, null) } diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index 11b8832c94..103f42e903 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -31,7 +31,8 @@ import im.vector.lib.attachmentviewer.AttachmentInfo import im.vector.lib.attachmentviewer.AttachmentSourceProvider import im.vector.lib.attachmentviewer.ImageLoaderTarget import im.vector.lib.attachmentviewer.VideoLoaderTarget -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -152,21 +153,20 @@ abstract class BaseAttachmentProvider( target.onVideoURLReady(info.uid, data.url) } else { target.onVideoFileLoading(info.uid) - fileService.downloadFile( - fileName = data.filename, - mimeType = data.mimeType, - url = data.url, - elementToDecrypt = data.elementToDecrypt, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - target.onVideoFileReady(info.uid, data) - } - - override fun onFailure(failure: Throwable) { - target.onVideoFileLoadFailed(info.uid) - } - } - ) + GlobalScope.launch { + val result = runCatching { + fileService.downloadFile( + fileName = data.filename, + mimeType = data.mimeType, + url = data.url, + elementToDecrypt = data.elementToDecrypt + ) + } + result.fold( + { target.onVideoFileReady(info.uid, it) }, + { target.onVideoFileLoadFailed(info.uid) } + ) + } } } diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt index 328d8f943e..d326b8e50a 100644 --- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt @@ -19,7 +19,8 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -77,20 +78,16 @@ class DataAttachmentRoomProvider( override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { val item = getItem(position) - fileService.downloadFile( - fileName = item.filename, - mimeType = item.mimeType, - url = item.url, - elementToDecrypt = item.elementToDecrypt, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - callback(data) - } - - override fun onFailure(failure: Throwable) { - callback(null) - } - } - ) + GlobalScope.launch { + val result = runCatching { + fileService.downloadFile( + fileName = item.filename, + mimeType = item.mimeType, + url = item.url, + elementToDecrypt = item.elementToDecrypt + ) + } + callback(result.getOrNull()) + } } } diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index 53c5dac9ad..fd3386826a 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -19,7 +19,8 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -125,21 +126,16 @@ class RoomEventsAttachmentProvider( val messageContent = timelineEvent.root.getClearContent().toModel() as? MessageWithAttachmentContent ?: return@let - fileService.downloadFile( - fileName = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - callback(data) - } - - override fun onFailure(failure: Throwable) { - callback(null) - } - } - ) + GlobalScope.launch { + val result = runCatching { + fileService.downloadFile( + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + } + callback(result.getOrNull()) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt index 7e4ea15ff0..59b612afb1 100644 --- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt @@ -25,11 +25,11 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.files.LocalFilesHelper +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import timber.log.Timber -import java.io.File import java.net.URLEncoder import javax.inject.Inject @@ -74,28 +74,31 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc thumbnailView.isVisible = true loadingView.isVisible = true - activeSessionHolder.getActiveSession().fileService() - .downloadFile( - fileName = data.filename, - mimeType = data.mimeType, - url = data.url, - elementToDecrypt = data.elementToDecrypt, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - thumbnailView.isVisible = false - loadingView.isVisible = false - videoView.isVisible = true + GlobalScope.launch { + val result = runCatching { + activeSessionHolder.getActiveSession().fileService() + .downloadFile( + fileName = data.filename, + mimeType = data.mimeType, + url = data.url, + elementToDecrypt = data.elementToDecrypt) + } + result.fold( + { data -> + thumbnailView.isVisible = false + loadingView.isVisible = false + videoView.isVisible = true - videoView.setVideoPath(data.path) - videoView.start() - } - - override fun onFailure(failure: Throwable) { - loadingView.isVisible = false - errorView.isVisible = true - errorView.text = errorFormatter.toHumanReadable(failure) - } - }) + videoView.setVideoPath(data.path) + videoView.start() + }, + { + loadingView.isVisible = false + errorView.isVisible = true + errorView.text = errorFormatter.toHumanReadable(it) + } + ) + } } } else { val resolvedUrl = contentUrlResolver.resolveFullSize(data.url) @@ -112,28 +115,31 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc thumbnailView.isVisible = true loadingView.isVisible = true - activeSessionHolder.getActiveSession().fileService() - .downloadFile( - fileName = data.filename, - mimeType = data.mimeType, - url = data.url, - elementToDecrypt = null, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - thumbnailView.isVisible = false - loadingView.isVisible = false - videoView.isVisible = true + GlobalScope.launch { + val result = runCatching { + activeSessionHolder.getActiveSession().fileService() + .downloadFile( + fileName = data.filename, + mimeType = data.mimeType, + url = data.url, + elementToDecrypt = null) + } + result.fold( + { data -> + thumbnailView.isVisible = false + loadingView.isVisible = false + videoView.isVisible = true - videoView.setVideoPath(data.path) - videoView.start() - } - - override fun onFailure(failure: Throwable) { - loadingView.isVisible = false - errorView.isVisible = true - errorView.text = errorFormatter.toHumanReadable(failure) - } - }) + videoView.setVideoPath(data.path) + videoView.start() + }, + { + loadingView.isVisible = false + errorView.isVisible = true + errorView.text = errorFormatter.toHumanReadable(it) + } + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index a4f617bf5b..494c30aab9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -26,9 +26,11 @@ import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isEdition import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult @@ -42,9 +44,10 @@ import javax.inject.Inject * The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that, * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. */ -class NotifiableEventResolver @Inject constructor(private val stringProvider: StringProvider, - private val noticeEventFormatter: NoticeEventFormatter, - private val displayableEventFormatter: DisplayableEventFormatter) { +class NotifiableEventResolver @Inject constructor( + private val stringProvider: StringProvider, + private val noticeEventFormatter: NoticeEventFormatter, + private val displayableEventFormatter: DisplayableEventFormatter) { // private val eventDisplay = RiotEventDisplay(context) @@ -84,6 +87,47 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } + fun resolveInMemoryEvent(session: Session, event: Event): NotifiableEvent? { + if (event.getClearType() != EventType.MESSAGE) return null + + // Ignore message edition + if (event.isEdition()) return null + + val actions = session.getActions(event) + val notificationAction = actions.toNotificationAction() + + return if (notificationAction.shouldNotify) { + val user = session.getUser(event.senderId!!) ?: return null + + val timelineEvent = TimelineEvent( + root = event, + localId = -1, + eventId = event.eventId!!, + displayIndex = 0, + senderInfo = SenderInfo( + userId = user.userId, + displayName = user.getBestName(), + isUniqueDisplayName = true, + avatarUrl = user.avatarUrl + ) + ) + + val notifiableEvent = resolveMessageEvent(timelineEvent, session) + + if (notifiableEvent == null) { + Timber.d("## Failed to resolve event") + // TODO + null + } else { + notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() + notifiableEvent + } + } else { + Timber.d("Matched push rule is set to not notify") + null + } + } + private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index d79d16a052..7125c22342 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -23,7 +23,8 @@ import androidx.core.app.RemoteInput import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.vectorComponent -import org.matrix.android.sdk.api.NoOpMatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.read.ReadService @@ -74,22 +75,37 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleJoinRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> - session.getRoom(roomId) - ?.join(callback = NoOpMatrixCallback()) + val room = session.getRoom(roomId) + if (room != null) { + GlobalScope.launch { + room.join() + } + } } } private fun handleRejectRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> - session.getRoom(roomId) - ?.leave(callback = NoOpMatrixCallback()) + val room = session.getRoom(roomId) + if (room != null) { + GlobalScope.launch { + room.leave() + } + } } } private fun handleMarkAsRead(roomId: String) { activeSessionHolder.getActiveSession().let { session -> - session.getRoom(roomId) - ?.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) + val room = session.getRoom(roomId) + if (room != null) { + GlobalScope.launch { + try { + room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) + } catch (_: Exception) { + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 7f3c0a5beb..7ac9b28b9a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -26,6 +26,7 @@ import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.FirstThrottler import im.vector.app.features.settings.VectorPreferences import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session @@ -88,7 +89,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // If we support multi session, event list should be per userId // Currently only manage single session if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.v("%%%%%%%% onNotifiableEventReceived $notifiableEvent") + Timber.d("onNotifiableEventReceived(): $notifiableEvent") + } else { + Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.isPushGatewayEvent}") } synchronized(eventList) { val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId } @@ -194,10 +197,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID) } + private var firstThrottler = FirstThrottler(200) + fun refreshNotificationDrawer() { // Implement last throttler - Timber.v("refreshNotificationDrawer()") + val canHandle = firstThrottler.canHandle() + Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") backgroundHandler.removeCallbacksAndMessages(null) + backgroundHandler.postDelayed( { try { @@ -206,7 +213,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer Timber.w(throwable, "refreshNotificationDrawerBg failure") } - }, 200) + }, + canHandle.waitMillis()) } @WorkerThread @@ -544,7 +552,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context return bitmapLoader.getRoomBitmap(roomAvatarPath) } - private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean { + fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean { return currentRoomId != null && roomId == currentRoomId } diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 67ef0514f2..3228ffa6e1 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -26,6 +26,7 @@ import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.isAnimationDisabled import im.vector.app.features.pin.PinActivity +import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.themes.ThemeUtils import timber.log.Timber import java.lang.ref.WeakReference @@ -294,6 +295,7 @@ class PopupAlertManager @Inject constructor() { private fun shouldBeDisplayedIn(alert: VectorAlert?, activity: Activity): Boolean { return alert != null && activity !is PinActivity + && activity !is SignedOutActivity && activity is VectorBaseActivity<*> && alert.shouldBeDisplayedIn.invoke(activity) } diff --git a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt index 891f083644..96eda22eb9 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt @@ -16,6 +16,8 @@ package im.vector.app.features.reactions.data import android.content.res.Resources +import android.graphics.Paint +import androidx.core.graphics.PaintCompat import com.squareup.moshi.Moshi import im.vector.app.R import javax.inject.Inject @@ -25,6 +27,7 @@ import javax.inject.Singleton class EmojiDataSource @Inject constructor( resources: Resources ) { + private val paint = Paint() val rawData = resources.openRawResource(R.raw.emoji_picker_datasource) .use { input -> Moshi.Builder() @@ -34,18 +37,32 @@ class EmojiDataSource @Inject constructor( } ?.let { parsedRawData -> // Add key as a keyword, it will solve the issue that ":tada" is not available in completion + // Only add emojis to emojis/categories that can be rendered by the system parsedRawData.copy( emojis = mutableMapOf().apply { parsedRawData.emojis.keys.forEach { key -> val origin = parsedRawData.emojis[key] ?: return@forEach // Do not add keys containing '_' - if (origin.keywords.contains(key) || key.contains("_")) { - put(key, origin) - } else { - put(key, origin.copy(keywords = origin.keywords + key)) + if (isEmojiRenderable(origin.emoji)) { + if (origin.keywords.contains(key) || key.contains("_")) { + put(key, origin) + } else { + put(key, origin.copy(keywords = origin.keywords + key)) + } } } + }, + categories = mutableListOf().apply { + parsedRawData.categories.forEach { entry -> + add(EmojiCategory(entry.id, entry.name, mutableListOf().apply { + entry.emojis.forEach { e -> + if (isEmojiRenderable(parsedRawData.emojis[e]!!.emoji)) { + add(e) + } + } + })) + } } ) } @@ -53,6 +70,10 @@ class EmojiDataSource @Inject constructor( private val quickReactions = mutableListOf() + private fun isEmojiRenderable(emoji: String): Boolean { + return PaintCompat.hasGlyph(paint, emoji) + } + fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) @@ -79,12 +100,12 @@ class EmojiDataSource @Inject constructor( fun getQuickReactions(): List { if (quickReactions.isEmpty()) { listOf( - "+1", // 👍 - "-1", // 👎 - "grinning", // 😄 - "tada", // 🎉 - "confused", // 😕 - "heart", // ❤️ + "thumbs-up", // 👍 + "thumbs-down", // 👎 + "grinning-face-with-smiling-eyes", // 😄 + "party-popper", // 🎉 + "confused-face", // 😕 + "red-heart", // ❤️ "rocket", // 🚀 "eyes" // 👀 ) diff --git a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt index 92408d59f4..33e63434ce 100644 --- a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt +++ b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt @@ -28,23 +28,36 @@ class VectorRoomDisplayNameFallbackProvider( return context.getString(R.string.room_displayname_room_invite) } - override fun getNameForEmptyRoom(): String { - return context.getString(R.string.room_displayname_empty_room) + override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String { + return if (leftMemberNames.isEmpty()) { + context.getString(R.string.room_displayname_empty_room) + } else { + val was = when (val size = leftMemberNames.size) { + 1 -> getNameFor1member(leftMemberNames[0]) + 2 -> getNameFor2members(leftMemberNames[0], leftMemberNames[1]) + 3 -> getNameFor3members(leftMemberNames[0], leftMemberNames[1], leftMemberNames[2]) + 4 -> getNameFor4members(leftMemberNames[0], leftMemberNames[1], leftMemberNames[2], leftMemberNames[3]) + else -> getNameFor4membersAndMore(leftMemberNames[0], leftMemberNames[1], leftMemberNames[2], size - 3) + } + context.getString(R.string.room_displayname_empty_room_was, was) + } } - override fun getNameFor2members(name1: String?, name2: String?): String { + override fun getNameFor1member(name: String) = name + + override fun getNameFor2members(name1: String, name2: String): String { return context.getString(R.string.room_displayname_two_members, name1, name2) } - override fun getNameFor3members(name1: String?, name2: String?, name3: String?): String { + override fun getNameFor3members(name1: String, name2: String, name3: String): String { return context.getString(R.string.room_displayname_3_members, name1, name2, name3) } - override fun getNameFor4members(name1: String?, name2: String?, name3: String?, name4: String?): String { + override fun getNameFor4members(name1: String, name2: String, name3: String, name4: String): String { return context.getString(R.string.room_displayname_4_members, name1, name2, name3, name4) } - override fun getNameFor4membersAndMore(name1: String?, name2: String?, name3: String?, remainingCount: Int): String { + override fun getNameFor4membersAndMore(name1: String, name2: String, name3: String, remainingCount: Int): String { return context.resources.getQuantityString( R.plurals.room_displayname_four_and_more_members, remainingCount, diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt new file mode 100644 index 0000000000..0d1f55485c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory + +import im.vector.app.core.utils.AssetReader +import javax.inject.Inject + +class ExplicitTermFilter @Inject constructor( + assetReader: AssetReader +) { + // List of forbidden terms is in file asset forbidden_terms.txt, in lower case + private val explicitTerms = assetReader.readAssetFile("forbidden_terms.txt") + .orEmpty() + .split("\n") + .map { it.trim() } + .distinct() + .filter { it.isNotEmpty() } + + private val explicitContentRegex = explicitTerms + .joinToString(prefix = ".*\\b(", separator = "|", postfix = ")\\b.*") + .toRegex(RegexOption.IGNORE_CASE) + + fun canSearchFor(term: String): Boolean { + return term !in explicitTerms && term != "18+" + } + + fun isValid(str: String): Boolean { + return explicitContentRegex.matches(str.replace("\n", " ")).not() + // Special treatment for "18+" since word boundaries does not work here + && str.contains("18+").not() + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index 4ef38758c7..a6c4646f8c 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -42,12 +42,12 @@ import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryDat import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.rx.rx import timber.log.Timber -import java.util.Locale class RoomDirectoryViewModel @AssistedInject constructor( @Assisted initialState: PublicRoomsViewState, vectorPreferences: VectorPreferences, - private val session: Session + private val session: Session, + private val explicitTermFilter: ExplicitTermFilter ) : VectorViewModel(initialState) { @AssistedFactory @@ -58,11 +58,6 @@ class RoomDirectoryViewModel @AssistedInject constructor( companion object : MvRxViewModelFactory { private const val PUBLIC_ROOMS_LIMIT = 20 - // List of forbidden terms, in lower case - private val explicitContentTerms = listOf( - "nsfw" - ) - @JvmStatic override fun create(viewModelContext: ViewModelContext, state: PublicRoomsViewState): RoomDirectoryViewModel? { val activity: RoomDirectoryActivity = (viewModelContext as ActivityViewModelContext).activity() @@ -166,6 +161,17 @@ class RoomDirectoryViewModel @AssistedInject constructor( } private fun load(filter: String, roomDirectoryData: RoomDirectoryData) { + if (!showAllRooms && !explicitTermFilter.canSearchFor(filter)) { + setState { + copy( + asyncPublicRoomsRequest = Success(Unit), + publicRooms = emptyList(), + hasMore = false + ) + } + return + } + currentJob = viewModelScope.launch { val data = try { session.getPublicRooms(roomDirectoryData.homeServer, @@ -202,11 +208,7 @@ class RoomDirectoryViewModel @AssistedInject constructor( // Filter val newPublicRooms = data.chunk.orEmpty() .filter { - showAllRooms - || "${it.name.orEmpty()} ${it.topic.orEmpty()} ${it.canonicalAlias.orEmpty()}".toLowerCase(Locale.ROOT) - .let { str -> - explicitContentTerms.all { term -> term !in str } - } + showAllRooms || explicitTermFilter.isValid("${it.name.orEmpty()} ${it.topic.orEmpty()}") } setState { diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 0556b9d2d6..64081a1683 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -37,7 +37,6 @@ import io.reactivex.functions.BiFunction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -53,7 +52,6 @@ import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -198,9 +196,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v viewModelScope.launch { try { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) - awaitCallback { - room.invite(initialState.userId, callback = it) - } + room.invite(initialState.userId) _viewEvents.post(RoomMemberProfileViewEvents.OnInviteActionSuccess) } catch (failure: Throwable) { _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) @@ -215,9 +211,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v viewModelScope.launch { try { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) - awaitCallback { - room.kick(initialState.userId, action.reason, it) - } + room.kick(initialState.userId, action.reason) _viewEvents.post(RoomMemberProfileViewEvents.OnKickActionSuccess) } catch (failure: Throwable) { _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) @@ -233,12 +227,10 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v viewModelScope.launch { try { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) - awaitCallback { - if (membership == Membership.BAN) { - room.unban(initialState.userId, action.reason, it) - } else { - room.ban(initialState.userId, action.reason, it) - } + if (membership == Membership.BAN) { + room.unban(initialState.userId, action.reason) + } else { + room.ban(initialState.userId, action.reason) } _viewEvents.post(RoomMemberProfileViewEvents.OnBanActionSuccess) } catch (failure: Throwable) { @@ -321,19 +313,18 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v private fun handleIgnoreAction() = withState { state -> val isIgnored = state.isIgnored() ?: return@withState _viewEvents.post(RoomMemberProfileViewEvents.Loading()) - val ignoreActionCallback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomMemberProfileViewEvents.OnIgnoreActionSuccess) + viewModelScope.launch { + val event = try { + if (isIgnored) { + session.unIgnoreUserIds(listOf(state.userId)) + } else { + session.ignoreUserIds(listOf(state.userId)) + } + RoomMemberProfileViewEvents.OnIgnoreActionSuccess + } catch (failure: Throwable) { + RoomMemberProfileViewEvents.Failure(failure) } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) - } - } - if (isIgnored) { - session.unIgnoreUserIds(listOf(state.userId), ignoreActionCallback) - } else { - session.ignoreUserIds(listOf(state.userId), ignoreActionCallback) + _viewEvents.post(event) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index bb7d041199..10e6dceebe 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -222,7 +222,6 @@ class RoomProfileController @Inject constructor( buildProfileAction( id = "devTools", title = stringProvider.getString(R.string.dev_tools_menu_name), - subtitle = roomSummary.roomId, dividerColor = dividerColor, divider = false, editable = true, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index c8bb6b5b5c..209ebcc35b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -32,7 +32,6 @@ import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -169,15 +168,14 @@ class RoomProfileViewModel @AssistedInject constructor( private fun handleLeaveRoom() { _viewEvents.post(RoomProfileViewEvents.Loading(stringProvider.getString(R.string.room_profile_leaving_room))) - room.leave(null, object : MatrixCallback { - override fun onSuccess(data: Unit) { + viewModelScope.launch { + try { + room.leave(null) // Do nothing, we will be closing the room automatically when it will get back from sync - } - - override fun onFailure(failure: Throwable) { + } catch (failure: Throwable) { _viewEvents.post(RoomProfileViewEvents.Failure(failure)) } - }) + } } private fun handleShareRoomProfile() { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt index 2fc1575341..1c5f8fe3af 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -128,6 +128,7 @@ class RoomAliasFragment @Inject constructor( state.roomSummary()?.let { views.roomSettingsToolbarTitleView.text = it.displayName avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index 556d950230..620d65781c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -117,6 +117,7 @@ class RoomBannedMemberListFragment @Inject constructor( state.roomSummary()?.let { views.roomSettingsToolbarTitleView.text = it.displayName avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt index 5663392c6c..9e12e30399 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt @@ -39,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -124,9 +123,7 @@ class RoomBannedMemberListViewModel @AssistedInject constructor(@Assisted initia } viewModelScope.launch(Dispatchers.IO) { try { - awaitCallback { - room.unban(roomMemberSummary.userId, null, it) - } + room.unban(roomMemberSummary.userId, null) } catch (failure: Throwable) { _viewEvents.post(RoomBannedMemberListViewEvents.ToastError(stringProvider.getString(R.string.failed_to_unban))) } finally { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 1db9200451..2ff89d6e54 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -140,6 +140,7 @@ class RoomMemberListFragment @Inject constructor( state.roomSummary()?.let { views.roomSettingGeneric.roomSettingsToolbarTitleView.text = it.displayName avatarRenderer.render(it.toMatrixItem(), views.roomSettingGeneric.roomSettingsToolbarAvatarImageView) + views.roomSettingGeneric.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt index 61635c9b31..a538c9269b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt @@ -91,6 +91,7 @@ class RoomPermissionsFragment @Inject constructor( state.roomSummary()?.let { views.roomSettingsToolbarTitleView.text = it.displayName avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index e431dbfcd6..129888ee04 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -154,6 +154,7 @@ class RoomSettingsFragment @Inject constructor( state.roomSummary()?.let { views.roomSettingsToolbarTitleView.text = it.displayName avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } invalidateOptionsMenu() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt index cdf139c7f6..1d6b056816 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -32,10 +32,8 @@ import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap -import java.io.File class RoomUploadsViewModel @AssistedInject constructor( @Assisted initialState: RoomUploadsViewState, @@ -129,32 +127,27 @@ class RoomUploadsViewModel @AssistedInject constructor( private fun handleShare(action: RoomUploadsAction.Share) { viewModelScope.launch { - try { - val file = awaitCallback { - session.fileService().downloadFile( - messageContent = action.uploadEvent.contentWithAttachmentContent, - callback = it - ) - } - _viewEvents.post(RoomUploadsViewEvents.FileReadyForSharing(file)) + val event = try { + val file = session.fileService().downloadFile( + messageContent = action.uploadEvent.contentWithAttachmentContent) + RoomUploadsViewEvents.FileReadyForSharing(file) } catch (failure: Throwable) { - _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) + RoomUploadsViewEvents.Failure(failure) } + _viewEvents.post(event) } } private fun handleDownload(action: RoomUploadsAction.Download) { viewModelScope.launch { - try { - val file = awaitCallback { - session.fileService().downloadFile( - messageContent = action.uploadEvent.contentWithAttachmentContent, - callback = it) - } - _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) + val event = try { + val file = session.fileService().downloadFile( + messageContent = action.uploadEvent.contentWithAttachmentContent) + RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body) } catch (failure: Throwable) { - _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) + RoomUploadsViewEvents.Failure(failure) } + _viewEvents.post(event) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index a1151162c8..9b043cfc7c 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -357,15 +357,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false) } - /** - * Tells if all room member state events should be shown in the messages list. - * - * @return true all room member state events should be shown in the messages list. - */ - fun showRoomMemberStateEvents(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_SHOW_ROOM_MEMBER_STATE_EVENTS_KEY, true) - } - /** * Tells if the join and leave membership events should be shown in the messages list. * diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index b66a37b75f..334464e304 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -55,7 +55,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService @@ -305,17 +304,13 @@ class VectorSettingsGeneralFragment @Inject constructor( private fun uploadAvatar(uri: Uri) { displayLoadingView() - session.updateAvatar(session.myUserId, uri, getFilenameFromUri(context, uri) ?: UUID.randomUUID().toString(), object : MatrixCallback { - override fun onSuccess(data: Unit) { - if (!isAdded) return - onCommonDone(null) + lifecycleScope.launch { + val result = runCatching { + session.updateAvatar(session.myUserId, uri, getFilenameFromUri(context, uri) ?: UUID.randomUUID().toString()) } - - override fun onFailure(failure: Throwable) { - if (!isAdded) return - onCommonDone(failure.localizedMessage) - } - }) + if (!isAdded) return@launch + onCommonDone(result.fold({ null }, { it.localizedMessage })) + } } // ============================================================================================================== @@ -477,20 +472,21 @@ class VectorSettingsGeneralFragment @Inject constructor( if (currentDisplayName != value) { displayLoadingView() - session.setDisplayName(session.myUserId, value, object : MatrixCallback { - override fun onSuccess(data: Unit) { - if (!isAdded) return - // refresh the settings value - mDisplayNamePreference.summary = value - mDisplayNamePreference.text = value - onCommonDone(null) - } - - override fun onFailure(failure: Throwable) { - if (!isAdded) return - onCommonDone(failure.localizedMessage) - } - }) + lifecycleScope.launch { + val result = runCatching { session.setDisplayName(session.myUserId, value) } + if (!isAdded) return@launch + result.fold( + { + // refresh the settings value + mDisplayNamePreference.summary = value + mDisplayNamePreference.text = value + onCommonDone(null) + }, + { + onCommonDone(it.localizedMessage) + } + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index c9160b8ebc..03b7c16274 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -20,6 +20,7 @@ import androidx.preference.Preference import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.preference.VectorPreference +import im.vector.app.core.utils.FirstThrottler import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.displayInWebView import im.vector.app.core.utils.openAppSettingsPage @@ -36,6 +37,8 @@ class VectorSettingsHelpAboutFragment @Inject constructor( override var titleRes = R.string.preference_root_help_about override val preferenceXmlRes = R.xml.vector_settings_help_about + private val firstThrottler = FirstThrottler(1000) + override fun bindPref() { // preference to start the App info screen, to facilitate App permissions access findPreference(APP_INFO_LINK_PREFERENCE_KEY)!! @@ -98,7 +101,9 @@ class VectorSettingsHelpAboutFragment @Inject constructor( // third party notice findPreference(VectorPreferences.SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!! .onPreferenceClickListener = Preference.OnPreferenceClickListener { - activity?.displayInWebView(VectorSettingsUrls.THIRD_PARTY_LICENSES) + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { + activity?.displayInWebView(VectorSettingsUrls.THIRD_PARTY_LICENSES) + } false } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt index 47868eed51..fd1f406bcb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt @@ -39,7 +39,6 @@ import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.features.notifications.NotificationUtils import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleKind @@ -295,20 +294,20 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } else { FcmHelper.getFcmToken(requireContext())?.let { - pushManager.unregisterPusher(it, object : MatrixCallback { - override fun onSuccess(data: Unit) { - session.refreshPushers() - } - - override fun onFailure(failure: Throwable) { - if (!isAdded) { - return - } - // revert the check box - switchPref.isChecked = !switchPref.isChecked - Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() - } - }) + lifecycleScope.launch { + runCatching { pushManager.unregisterPusher(it) } + .fold( + { session.refreshPushers() }, + { + if (!isAdded) { + return@fold + } + // revert the check box + switchPref.isChecked = !switchPref.isChecked + Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() + } + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt index b2200e6a6d..7880e734a5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt @@ -32,7 +32,6 @@ import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx data class AccountDataViewState( @@ -58,9 +57,7 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A private fun handleDeleteAccountData(action: AccountDataAction.DeleteAccountData) { viewModelScope.launch { - awaitCallback { - session.updateAccountData(action.type, emptyMap(), it) - } + session.updateAccountData(action.type, emptyMap()) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt index fdc6585829..aa00f71542 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.ignored +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext @@ -30,7 +31,7 @@ import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.rx.rx @@ -89,24 +90,14 @@ class IgnoredUsersViewModel @AssistedInject constructor(@Assisted initialState: ) } - session.unIgnoreUserIds(listOf(action.userId), object : MatrixCallback { - override fun onFailure(failure: Throwable) { - setState { - copy( - unIgnoreRequest = Fail(failure) - ) - } - - _viewEvents.post(IgnoredUsersViewEvents.Failure(failure)) + viewModelScope.launch { + val result = runCatching { session.unIgnoreUserIds(listOf(action.userId)) } + setState { + copy( + unIgnoreRequest = result.fold(::Success, ::Fail) + ) } - - override fun onSuccess(data: Unit) { - setState { - copy( - unIgnoreRequest = Success(data) - ) - } - } - }) + result.onFailure { _viewEvents.post(IgnoredUsersViewEvents.Failure(it)) } + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index 89d632b813..ac565e72a1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -33,7 +33,6 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ReadOnceTrue import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.Session @@ -58,16 +57,18 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( private var pendingThreePid: ThreePid? = null // private var pendingSession: String? = null - private val loadingCallback: MatrixCallback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - isLoading(false) - _viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure)) - } - - override fun onSuccess(data: Unit) { - pendingThreePid = null - isLoading(false) - } + private suspend fun loadingSuspendable(block: suspend () -> Unit) { + runCatching { block() } + .fold( + { + pendingThreePid = null + isLoading(false) + }, + { + isLoading(false) + _viewEvents.post(ThreePidsSettingsViewEvents.Failure(it)) + } + ) } private fun isLoading(isLoading: Boolean) { @@ -186,24 +187,23 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( viewModelScope.launch { // First submit the code - session.submitSmsCode(action.threePid, action.code, object : MatrixCallback { - override fun onSuccess(data: Unit) { - // then finalize - pendingThreePid = action.threePid - session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback) + try { + session.submitSmsCode(action.threePid, action.code) + } catch (failure: Throwable) { + isLoading(false) + setState { + copy( + msisdnValidationRequests = msisdnValidationRequests.toMutableMap().apply { + put(action.threePid.value, Fail(failure)) + } + ) } + return@launch + } - override fun onFailure(failure: Throwable) { - isLoading(false) - setState { - copy( - msisdnValidationRequests = msisdnValidationRequests.toMutableMap().apply { - put(action.threePid.value, Fail(failure)) - } - ) - } - } - }) + // then finalize + pendingThreePid = action.threePid + loadingSuspendable { session.finalizeAddingThreePid(action.threePid, uiaInterceptor) } } } @@ -230,21 +230,15 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( )))) } else { viewModelScope.launch { - session.addThreePid(action.threePid, object : MatrixCallback { - override fun onSuccess(data: Unit) { - // Also reset the state - setState { - copy( - uiState = ThreePidsSettingsUiState.Idle - ) - } - loadingCallback.onSuccess(data) + loadingSuspendable { + session.addThreePid(action.threePid) + // Also reset the state + setState { + copy( + uiState = ThreePidsSettingsUiState.Idle + ) } - - override fun onFailure(failure: Throwable) { - loadingCallback.onFailure(failure) - } - }) + } } } } @@ -254,14 +248,14 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( isLoading(true) pendingThreePid = action.threePid viewModelScope.launch { - session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback) + loadingSuspendable { session.finalizeAddingThreePid(action.threePid, uiaInterceptor) } } } private fun handleCancelThreePid(action: ThreePidsSettingsAction.CancelThreePid) { isLoading(true) viewModelScope.launch { - session.cancelAddingThreePid(action.threePid, loadingCallback) + loadingSuspendable { session.cancelAddingThreePid(action.threePid) } } } @@ -277,7 +271,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) { isLoading(true) viewModelScope.launch { - session.deleteThreePid(action.threePid, loadingCallback) + loadingSuspendable { session.deleteThreePid(action.threePid) } } } } diff --git a/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt index 847caeab4c..a1065ed10b 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt @@ -31,6 +31,11 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int, R.style.AppTheme_Black ) + object Launcher : ActivityOtherThemes( + R.style.AppTheme_Launcher, + R.style.AppTheme_Launcher + ) + object AttachmentsPreview : ActivityOtherThemes( R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 671d018b63..9637b72581 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.internal.util.awaitCallback class UserCodeSharedViewModel @AssistedInject constructor( @Assisted val initialState: UserCodeState, @@ -126,11 +125,7 @@ class UserCodeSharedViewModel @AssistedInject constructor( _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) } is PermalinkData.UserLink -> { - val user = tryOrNull { - awaitCallback { - session.resolveUser(linkedId.userId, it) - } - } + val user = tryOrNull { session.resolveUser(linkedId.userId) } // Create raw Uxid in case the user is not searchable ?: User(linkedId.userId, null, null) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAPICallback.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAPICallback.kt deleted file mode 100644 index ad17a5ae87..0000000000 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAPICallback.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.widgets - -import im.vector.app.R -import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator -import org.matrix.android.sdk.api.util.JsonDict - -class WidgetAPICallback(private val postAPIMediator: WidgetPostAPIMediator, - private val eventData: JsonDict, - private val stringProvider: StringProvider) : MatrixCallback { - - override fun onFailure(failure: Throwable) { - postAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) - } - - override fun onSuccess(data: Any) { - postAPIMediator.sendSuccess(eventData) - } -} diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 13d49eb20b..9fa04aabbb 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -283,18 +283,20 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo "type" to "m.widget" ) ) - session.updateAccountData( - type = UserAccountDataTypes.TYPE_WIDGETS, - content = addUserWidgetBody, - callback = createWidgetAPICallback(widgetPostAPIMediator, eventData) - ) + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + session.updateAccountData( + type = UserAccountDataTypes.TYPE_WIDGETS, + content = addUserWidgetBody + ) + } } else { - session.widgetService().createRoomWidget( - roomId = roomId, - widgetId = widgetId, - content = widgetEventContent, - callback = createWidgetAPICallback(widgetPostAPIMediator, eventData) - ) + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + session.widgetService().createRoomWidget( + roomId = roomId, + widgetId = widgetId, + content = widgetEventContent + ) + } } } @@ -386,7 +388,9 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo if (member != null && member.membership == Membership.JOIN) { widgetPostAPIMediator.sendSuccess(eventData) } else { - room.invite(userId = userId, callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)) + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + room.invite(userId = userId) + } } } @@ -460,10 +464,6 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo return false } - private fun createWidgetAPICallback(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict): WidgetAPICallback { - return WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider) - } - private fun launchWidgetAPIAction(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict, block: suspend () -> Unit): Job { return GlobalScope.launch { kotlin.runCatching { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index d4e63b1338..b6548a6542 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.mapOptional import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -173,10 +172,8 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi viewModelScope.launch { val widgetId = initialState.widgetId ?: return@launch try { - awaitCallback { - widgetService.destroyRoomWidget(initialState.roomId, widgetId, it) - _viewEvents.post(WidgetViewEvents.Close()) - } + widgetService.destroyRoomWidget(initialState.roomId, widgetId) + _viewEvents.post(WidgetViewEvents.Close()) } catch (failure: Throwable) { _viewEvents.post(WidgetViewEvents.Failure(failure)) } diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 0db905d015..e837d2d80b 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -68,6 +68,13 @@ app:tint="?riotx_text_primary" tools:ignore="MissingConstraints,MissingPrefix" /> + + + + + - diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index 7edaa99016..92655c87b6 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -193,4 +193,15 @@ app:layout_constraintTop_toBottomOf="@+id/loginSplashSubmit" app:layout_constraintVertical_weight="4" /> + + diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index c11ce07062..9c3827f919 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -34,6 +34,15 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@tools:sample/avatars" /> + + + android:layout_height="wrap_content" + android:paddingTop="32dp" + android:paddingBottom="32dp"> - - - - - - - - - + + + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_or" /> - + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index 5fbed68955..afd96d5690 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -50,6 +50,20 @@ app:layout_constraintEnd_toEndOf="parent" tools:text="Friday 8pm" /> + + diff --git a/vector/src/main/res/layout/item_timeline_empty.xml b/vector/src/main/res/layout/item_timeline_empty.xml index c8dee60cc7..562cbd39ba 100644 --- a/vector/src/main/res/layout/item_timeline_empty.xml +++ b/vector/src/main/res/layout/item_timeline_empty.xml @@ -1,4 +1,4 @@ \ No newline at end of file + android:layout_height="0dp" /> diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index ce3460a21c..f9562f65b0 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -188,15 +188,6 @@ android:layout_height="wrap_content" /--> - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 6442f230d5..35e1b097d7 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -10,7 +10,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignBottom="@+id/readReceiptsView" + android:layout_alignParentBottom="true" android:layout_alignParentTop="true" android:background="@drawable/highlighted_message_background" /> @@ -80,14 +80,4 @@ android:visibility="gone" tools:visibility="visible" /> - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index db5ed052f3..98cea901da 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -120,14 +120,6 @@ - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_read_receipts.xml b/vector/src/main/res/layout/item_timeline_event_read_receipts.xml new file mode 100644 index 0000000000..f741e434c7 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_read_receipts.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/raw/emoji_picker_datasource.json b/vector/src/main/res/raw/emoji_picker_datasource.json index 431b7d420c..6aa3799cf0 100644 --- a/vector/src/main/res/raw/emoji_picker_datasource.json +++ b/vector/src/main/res/raw/emoji_picker_datasource.json @@ -1 +1 @@ -{"compressed":true,"categories":[{"id":"people","name":"Smileys & People","emojis":["grinning","grin","joy","rolling_on_the_floor_laughing","smiley","smile","sweat_smile","laughing","wink","blush","yum","sunglasses","heart_eyes","kissing_heart","kissing","kissing_smiling_eyes","kissing_closed_eyes","relaxed","slightly_smiling_face","hugging_face","star-struck","thinking_face","face_with_raised_eyebrow","neutral_face","expressionless","no_mouth","face_with_rolling_eyes","smirk","persevere","disappointed_relieved","open_mouth","zipper_mouth_face","hushed","sleepy","tired_face","sleeping","relieved","stuck_out_tongue","stuck_out_tongue_winking_eye","stuck_out_tongue_closed_eyes","drooling_face","unamused","sweat","pensive","confused","upside_down_face","money_mouth_face","astonished","white_frowning_face","slightly_frowning_face","confounded","disappointed","worried","triumph","cry","sob","frowning","anguished","fearful","weary","exploding_head","grimacing","cold_sweat","scream","flushed","zany_face","dizzy_face","rage","angry","face_with_symbols_on_mouth","mask","face_with_thermometer","face_with_head_bandage","nauseated_face","face_vomiting","sneezing_face","innocent","face_with_cowboy_hat","clown_face","lying_face","shushing_face","face_with_hand_over_mouth","face_with_monocle","nerd_face","smiling_imp","imp","japanese_ogre","japanese_goblin","skull","skull_and_crossbones","ghost","alien","space_invader","robot_face","hankey","smiley_cat","smile_cat","joy_cat","heart_eyes_cat","smirk_cat","kissing_cat","scream_cat","crying_cat_face","pouting_cat","see_no_evil","hear_no_evil","speak_no_evil","baby","child","boy","girl","adult","man","woman","older_adult","older_man","older_woman","male-doctor","female-doctor","male-student","female-student","male-teacher","female-teacher","male-judge","female-judge","male-farmer","female-farmer","male-cook","female-cook","male-mechanic","female-mechanic","male-factory-worker","female-factory-worker","male-office-worker","female-office-worker","male-scientist","female-scientist","male-technologist","female-technologist","male-singer","female-singer","male-artist","female-artist","male-pilot","female-pilot","male-astronaut","female-astronaut","male-firefighter","female-firefighter","cop","male-police-officer","female-police-officer","sleuth_or_spy","male-detective","female-detective","guardsman","male-guard","female-guard","construction_worker","male-construction-worker","female-construction-worker","prince","princess","man_with_turban","man-wearing-turban","woman-wearing-turban","man_with_gua_pi_mao","person_with_headscarf","bearded_person","person_with_blond_hair","blond-haired-man","blond-haired-woman","man_in_tuxedo","bride_with_veil","pregnant_woman","breast-feeding","angel","santa","mrs_claus","mage","female_mage","male_mage","fairy","female_fairy","male_fairy","vampire","female_vampire","male_vampire","merperson","mermaid","merman","elf","female_elf","male_elf","genie","female_genie","male_genie","zombie","female_zombie","male_zombie","person_frowning","man-frowning","woman-frowning","person_with_pouting_face","man-pouting","woman-pouting","no_good","man-gesturing-no","woman-gesturing-no","ok_woman","man-gesturing-ok","woman-gesturing-ok","information_desk_person","man-tipping-hand","woman-tipping-hand","raising_hand","man-raising-hand","woman-raising-hand","bow","man-bowing","woman-bowing","face_palm","man-facepalming","woman-facepalming","shrug","man-shrugging","woman-shrugging","massage","man-getting-massage","woman-getting-massage","haircut","man-getting-haircut","woman-getting-haircut","walking","man-walking","woman-walking","runner","man-running","woman-running","dancer","man_dancing","dancers","man-with-bunny-ears-partying","woman-with-bunny-ears-partying","person_in_steamy_room","woman_in_steamy_room","man_in_steamy_room","person_climbing","woman_climbing","man_climbing","person_in_lotus_position","woman_in_lotus_position","man_in_lotus_position","bath","sleeping_accommodation","man_in_business_suit_levitating","speaking_head_in_silhouette","bust_in_silhouette","busts_in_silhouette","fencer","horse_racing","skier","snowboarder","golfer","man-golfing","woman-golfing","surfer","man-surfing","woman-surfing","rowboat","man-rowing-boat","woman-rowing-boat","swimmer","man-swimming","woman-swimming","person_with_ball","man-bouncing-ball","woman-bouncing-ball","weight_lifter","man-lifting-weights","woman-lifting-weights","bicyclist","man-biking","woman-biking","mountain_bicyclist","man-mountain-biking","woman-mountain-biking","racing_car","racing_motorcycle","person_doing_cartwheel","man-cartwheeling","woman-cartwheeling","wrestlers","man-wrestling","woman-wrestling","water_polo","man-playing-water-polo","woman-playing-water-polo","handball","man-playing-handball","woman-playing-handball","juggling","man-juggling","woman-juggling","couple","two_men_holding_hands","two_women_holding_hands","couplekiss","woman-kiss-man","man-kiss-man","woman-kiss-woman","couple_with_heart","woman-heart-man","man-heart-man","woman-heart-woman","family","man-woman-boy","man-woman-girl","man-woman-girl-boy","man-woman-boy-boy","man-woman-girl-girl","man-man-boy","man-man-girl","man-man-girl-boy","man-man-boy-boy","man-man-girl-girl","woman-woman-boy","woman-woman-girl","woman-woman-girl-boy","woman-woman-boy-boy","woman-woman-girl-girl","man-boy","man-boy-boy","man-girl","man-girl-boy","man-girl-girl","woman-boy","woman-boy-boy","woman-girl","woman-girl-boy","woman-girl-girl","selfie","muscle","point_left","point_right","point_up","point_up_2","middle_finger","point_down","v","crossed_fingers","spock-hand","the_horns","call_me_hand","raised_hand_with_fingers_splayed","hand","ok_hand","+1","-1","fist","facepunch","left-facing_fist","right-facing_fist","raised_back_of_hand","wave","i_love_you_hand_sign","writing_hand","clap","open_hands","raised_hands","palms_up_together","pray","handshake","nail_care","ear","nose","footprints","eyes","eye","eye-in-speech-bubble","brain","tongue","lips","kiss","cupid","heart","heartbeat","broken_heart","two_hearts","sparkling_heart","heartpulse","blue_heart","green_heart","yellow_heart","orange_heart","purple_heart","black_heart","gift_heart","revolving_hearts","heart_decoration","heavy_heart_exclamation_mark_ornament","love_letter","zzz","anger","bomb","boom","sweat_drops","dash","dizzy","speech_balloon","left_speech_bubble","right_anger_bubble","thought_balloon","hole","eyeglasses","dark_sunglasses","necktie","shirt","jeans","scarf","gloves","coat","socks","dress","kimono","bikini","womans_clothes","purse","handbag","pouch","shopping_bags","school_satchel","mans_shoe","athletic_shoe","high_heel","sandal","boot","crown","womans_hat","tophat","mortar_board","billed_cap","helmet_with_white_cross","prayer_beads","lipstick","ring","gem"]},{"id":"nature","name":"Animals & Nature","emojis":["monkey_face","monkey","gorilla","dog","dog2","poodle","wolf","fox_face","cat","cat2","lion_face","tiger","tiger2","leopard","horse","racehorse","unicorn_face","zebra_face","deer","cow","ox","water_buffalo","cow2","pig","pig2","boar","pig_nose","ram","sheep","goat","dromedary_camel","camel","giraffe_face","elephant","rhinoceros","mouse","mouse2","rat","hamster","rabbit","rabbit2","chipmunk","hedgehog","bat","bear","koala","panda_face","feet","turkey","chicken","rooster","hatching_chick","baby_chick","hatched_chick","bird","penguin","dove_of_peace","eagle","duck","owl","frog","crocodile","turtle","lizard","snake","dragon_face","dragon","sauropod","t-rex","whale","whale2","dolphin","fish","tropical_fish","blowfish","shark","octopus","shell","crab","shrimp","squid","snail","butterfly","bug","ant","bee","beetle","cricket","spider","spider_web","scorpion","bouquet","cherry_blossom","white_flower","rosette","rose","wilted_flower","hibiscus","sunflower","blossom","tulip","seedling","evergreen_tree","deciduous_tree","palm_tree","cactus","ear_of_rice","herb","shamrock","four_leaf_clover","maple_leaf","fallen_leaf","leaves"]},{"id":"foods","name":"Food & Drink","emojis":["grapes","melon","watermelon","tangerine","lemon","banana","pineapple","apple","green_apple","pear","peach","cherries","strawberry","kiwifruit","tomato","coconut","avocado","eggplant","potato","carrot","corn","hot_pepper","cucumber","broccoli","mushroom","peanuts","chestnut","bread","croissant","baguette_bread","pretzel","pancakes","cheese_wedge","meat_on_bone","poultry_leg","cut_of_meat","bacon","hamburger","fries","pizza","hotdog","sandwich","taco","burrito","stuffed_flatbread","egg","fried_egg","shallow_pan_of_food","stew","bowl_with_spoon","green_salad","popcorn","canned_food","bento","rice_cracker","rice_ball","rice","curry","ramen","spaghetti","sweet_potato","oden","sushi","fried_shrimp","fish_cake","dango","dumpling","fortune_cookie","takeout_box","icecream","shaved_ice","ice_cream","doughnut","cookie","birthday","cake","pie","chocolate_bar","candy","lollipop","custard","honey_pot","baby_bottle","glass_of_milk","coffee","tea","sake","champagne","wine_glass","cocktail","tropical_drink","beer","beers","clinking_glasses","tumbler_glass","cup_with_straw","chopsticks","knife_fork_plate","fork_and_knife","spoon","hocho","amphora"]},{"id":"activity","name":"Activities","emojis":["jack_o_lantern","christmas_tree","fireworks","sparkler","sparkles","balloon","tada","confetti_ball","tanabata_tree","bamboo","dolls","flags","wind_chime","rice_scene","ribbon","gift","reminder_ribbon","admission_tickets","ticket","medal","trophy","sports_medal","first_place_medal","second_place_medal","third_place_medal","soccer","baseball","basketball","volleyball","football","rugby_football","tennis","8ball","bowling","cricket_bat_and_ball","field_hockey_stick_and_ball","ice_hockey_stick_and_puck","table_tennis_paddle_and_ball","badminton_racquet_and_shuttlecock","boxing_glove","martial_arts_uniform","goal_net","dart","golf","ice_skate","fishing_pole_and_fish","running_shirt_with_sash","ski","sled","curling_stone","video_game","joystick","game_die","spades","hearts","diamonds","clubs","black_joker","mahjong","flower_playing_cards"]},{"id":"places","name":"Travel & Places","emojis":["earth_africa","earth_americas","earth_asia","globe_with_meridians","world_map","japan","snow_capped_mountain","mountain","volcano","mount_fuji","camping","beach_with_umbrella","desert","desert_island","national_park","stadium","classical_building","building_construction","house_buildings","cityscape","derelict_house_building","house","house_with_garden","office","post_office","european_post_office","hospital","bank","hotel","love_hotel","convenience_store","school","department_store","factory","japanese_castle","european_castle","wedding","tokyo_tower","statue_of_liberty","church","mosque","synagogue","shinto_shrine","kaaba","fountain","tent","foggy","night_with_stars","sunrise_over_mountains","sunrise","city_sunset","city_sunrise","bridge_at_night","hotsprings","milky_way","carousel_horse","ferris_wheel","roller_coaster","barber","circus_tent","performing_arts","frame_with_picture","art","slot_machine","steam_locomotive","railway_car","bullettrain_side","bullettrain_front","train2","metro","light_rail","station","tram","monorail","mountain_railway","train","bus","oncoming_bus","trolleybus","minibus","ambulance","fire_engine","police_car","oncoming_police_car","taxi","oncoming_taxi","car","oncoming_automobile","blue_car","truck","articulated_lorry","tractor","bike","scooter","motor_scooter","busstop","motorway","railway_track","fuelpump","rotating_light","traffic_light","vertical_traffic_light","construction","octagonal_sign","anchor","boat","canoe","speedboat","passenger_ship","ferry","motor_boat","ship","airplane","small_airplane","airplane_departure","airplane_arriving","seat","helicopter","suspension_railway","mountain_cableway","aerial_tramway","satellite","rocket","flying_saucer","bellhop_bell","door","bed","couch_and_lamp","toilet","shower","bathtub","hourglass","hourglass_flowing_sand","watch","alarm_clock","stopwatch","timer_clock","mantelpiece_clock","clock12","clock1230","clock1","clock130","clock2","clock230","clock3","clock330","clock4","clock430","clock5","clock530","clock6","clock630","clock7","clock730","clock8","clock830","clock9","clock930","clock10","clock1030","clock11","clock1130","new_moon","waxing_crescent_moon","first_quarter_moon","moon","full_moon","waning_gibbous_moon","last_quarter_moon","waning_crescent_moon","crescent_moon","new_moon_with_face","first_quarter_moon_with_face","last_quarter_moon_with_face","thermometer","sunny","full_moon_with_face","sun_with_face","star","star2","stars","cloud","partly_sunny","thunder_cloud_and_rain","mostly_sunny","barely_sunny","partly_sunny_rain","rain_cloud","snow_cloud","lightning","tornado","fog","wind_blowing_face","cyclone","rainbow","closed_umbrella","umbrella","umbrella_with_rain_drops","umbrella_on_ground","zap","snowflake","snowman","snowman_without_snow","comet","fire","droplet","ocean"]},{"id":"objects","name":"Objects","emojis":["mute","speaker","sound","loud_sound","loudspeaker","mega","postal_horn","bell","no_bell","musical_score","musical_note","notes","studio_microphone","level_slider","control_knobs","microphone","headphones","radio","saxophone","guitar","musical_keyboard","trumpet","violin","drum_with_drumsticks","iphone","calling","phone","telephone_receiver","pager","fax","battery","electric_plug","computer","desktop_computer","printer","keyboard","three_button_mouse","trackball","minidisc","floppy_disk","cd","dvd","movie_camera","film_frames","film_projector","clapper","tv","camera","camera_with_flash","video_camera","vhs","mag","mag_right","microscope","telescope","satellite_antenna","candle","bulb","flashlight","izakaya_lantern","notebook_with_decorative_cover","closed_book","book","green_book","blue_book","orange_book","books","notebook","ledger","page_with_curl","scroll","page_facing_up","newspaper","rolled_up_newspaper","bookmark_tabs","bookmark","label","moneybag","yen","dollar","euro","pound","money_with_wings","credit_card","chart","currency_exchange","heavy_dollar_sign","email","e-mail","incoming_envelope","envelope_with_arrow","outbox_tray","inbox_tray","package","mailbox","mailbox_closed","mailbox_with_mail","mailbox_with_no_mail","postbox","ballot_box_with_ballot","pencil2","black_nib","lower_left_fountain_pen","lower_left_ballpoint_pen","lower_left_paintbrush","lower_left_crayon","memo","briefcase","file_folder","open_file_folder","card_index_dividers","date","calendar","spiral_note_pad","spiral_calendar_pad","card_index","chart_with_upwards_trend","chart_with_downwards_trend","bar_chart","clipboard","pushpin","round_pushpin","paperclip","linked_paperclips","straight_ruler","triangular_ruler","scissors","card_file_box","file_cabinet","wastebasket","lock","unlock","lock_with_ink_pen","closed_lock_with_key","key","old_key","hammer","pick","hammer_and_pick","hammer_and_wrench","dagger_knife","crossed_swords","gun","bow_and_arrow","shield","wrench","nut_and_bolt","gear","compression","alembic","scales","link","chains","syringe","pill","smoking","coffin","funeral_urn","moyai","oil_drum","crystal_ball","shopping_trolley"]},{"id":"symbols","name":"Symbols","emojis":["atm","put_litter_in_its_place","potable_water","wheelchair","mens","womens","restroom","baby_symbol","wc","passport_control","customs","baggage_claim","left_luggage","warning","children_crossing","no_entry","no_entry_sign","no_bicycles","no_smoking","do_not_litter","non-potable_water","no_pedestrians","no_mobile_phones","underage","radioactive_sign","biohazard_sign","arrow_up","arrow_upper_right","arrow_right","arrow_lower_right","arrow_down","arrow_lower_left","arrow_left","arrow_upper_left","arrow_up_down","left_right_arrow","leftwards_arrow_with_hook","arrow_right_hook","arrow_heading_up","arrow_heading_down","arrows_clockwise","arrows_counterclockwise","back","end","on","soon","top","place_of_worship","atom_symbol","om_symbol","star_of_david","wheel_of_dharma","yin_yang","latin_cross","orthodox_cross","star_and_crescent","peace_symbol","menorah_with_nine_branches","six_pointed_star","aries","taurus","gemini","cancer","leo","virgo","libra","scorpius","sagittarius","capricorn","aquarius","pisces","ophiuchus","twisted_rightwards_arrows","repeat","repeat_one","arrow_forward","fast_forward","black_right_pointing_double_triangle_with_vertical_bar","black_right_pointing_triangle_with_double_vertical_bar","arrow_backward","rewind","black_left_pointing_double_triangle_with_vertical_bar","arrow_up_small","arrow_double_up","arrow_down_small","arrow_double_down","double_vertical_bar","black_square_for_stop","black_circle_for_record","eject","cinema","low_brightness","high_brightness","signal_strength","vibration_mode","mobile_phone_off","female_sign","male_sign","medical_symbol","recycle","fleur_de_lis","trident","name_badge","beginner","o","white_check_mark","ballot_box_with_check","heavy_check_mark","heavy_multiplication_x","x","negative_squared_cross_mark","heavy_plus_sign","heavy_minus_sign","heavy_division_sign","curly_loop","loop","part_alternation_mark","eight_spoked_asterisk","eight_pointed_black_star","sparkle","bangbang","interrobang","question","grey_question","grey_exclamation","exclamation","wavy_dash","copyright","registered","tm","hash","keycap_star","zero","one","two","three","four","five","six","seven","eight","nine","keycap_ten","100","capital_abcd","abcd","1234","symbols","abc","a","ab","b","cl","cool","free","information_source","id","m","new","ng","o2","ok","parking","sos","up","vs","koko","sa","u6708","u6709","u6307","ideograph_advantage","u5272","u7121","u7981","accept","u7533","u5408","u7a7a","congratulations","secret","u55b6","u6e80","black_small_square","white_small_square","white_medium_square","black_medium_square","white_medium_small_square","black_medium_small_square","black_large_square","white_large_square","large_orange_diamond","large_blue_diamond","small_orange_diamond","small_blue_diamond","small_red_triangle","small_red_triangle_down","diamond_shape_with_a_dot_inside","radio_button","black_square_button","white_square_button","white_circle","black_circle","red_circle","large_blue_circle"]},{"id":"flags","name":"Flags","emojis":["checkered_flag","cn","crossed_flags","de","es","flag-ac","flag-ad","flag-ae","flag-af","flag-ag","flag-ai","flag-al","flag-am","flag-ao","flag-aq","flag-ar","flag-as","flag-at","flag-au","flag-aw","flag-ax","flag-az","flag-ba","flag-bb","flag-bd","flag-be","flag-bf","flag-bg","flag-bh","flag-bi","flag-bj","flag-bm","flag-bn","flag-bo","flag-br","flag-bs","flag-bt","flag-bv","flag-bw","flag-by","flag-bz","flag-ca","flag-cc","flag-cd","flag-cf","flag-cg","flag-ch","flag-ci","flag-ck","flag-cl","flag-cm","flag-co","flag-cp","flag-cr","flag-cu","flag-cv","flag-cw","flag-cx","flag-cy","flag-cz","flag-dj","flag-dk","flag-dm","flag-do","flag-dz","flag-ec","flag-ee","flag-eg","flag-england","flag-er","flag-et","flag-eu","flag-fi","flag-fj","flag-fm","flag-fo","flag-ga","flag-gd","flag-ge","flag-gg","flag-gh","flag-gi","flag-gl","flag-gm","flag-gn","flag-gq","flag-gr","flag-gt","flag-gu","flag-gw","flag-gy","flag-hk","flag-hm","flag-hn","flag-hr","flag-ht","flag-hu","flag-ic","flag-id","flag-ie","flag-il","flag-im","flag-in","flag-io","flag-iq","flag-ir","flag-is","flag-je","flag-jm","flag-jo","flag-ke","flag-kg","flag-kh","flag-ki","flag-km","flag-kn","flag-kp","flag-kw","flag-ky","flag-kz","flag-la","flag-lb","flag-lc","flag-li","flag-lk","flag-lr","flag-ls","flag-lt","flag-lu","flag-lv","flag-ly","flag-ma","flag-mc","flag-md","flag-me","flag-mg","flag-mh","flag-mk","flag-ml","flag-mm","flag-mn","flag-mo","flag-mp","flag-mr","flag-ms","flag-mt","flag-mu","flag-mv","flag-mw","flag-mx","flag-my","flag-mz","flag-na","flag-ne","flag-nf","flag-ng","flag-ni","flag-nl","flag-no","flag-np","flag-nr","flag-nu","flag-nz","flag-om","flag-pa","flag-pe","flag-pf","flag-pg","flag-ph","flag-pk","flag-pl","flag-pn","flag-pr","flag-ps","flag-pt","flag-pw","flag-py","flag-qa","flag-ro","flag-rs","flag-rw","flag-sa","flag-sb","flag-sc","flag-scotland","flag-sd","flag-se","flag-sg","flag-sh","flag-si","flag-sj","flag-sk","flag-sl","flag-sm","flag-sn","flag-so","flag-sr","flag-ss","flag-st","flag-sv","flag-sx","flag-sy","flag-sz","flag-ta","flag-tc","flag-td","flag-tg","flag-th","flag-tj","flag-tk","flag-tl","flag-tm","flag-tn","flag-to","flag-tr","flag-tt","flag-tv","flag-tw","flag-tz","flag-ua","flag-ug","flag-um","flag-uy","flag-uz","flag-va","flag-vc","flag-ve","flag-vg","flag-vi","flag-vn","flag-vu","flag-wales","flag-ws","flag-ye","flag-za","flag-zm","flag-zw","fr","gb","it","jp","kr","rainbow-flag","ru","triangular_flag_on_post","us","waving_black_flag","waving_white_flag"]}],"emojis":{"100":{"a":"Hundred Points Symbol","b":"1F4AF","j":["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],"k":[25,26]},"1234":{"a":"Input Symbol for Numbers","b":"1F522","j":["numbers","blue-square"],"k":[27,36]},"monkey_face":{"a":"Monkey Face","b":"1F435","j":["animal","nature","circus"],"k":[13,31],"l":[":o)"]},"grinning":{"a":"Grinning Face","b":"1F600","j":["face","smile","happy","joy",":D","grin"],"k":[30,24],"m":":D"},"earth_africa":{"a":"Earth Globe Europe-Africa","b":"1F30D","j":["globe","world","international"],"k":[6,5]},"checkered_flag":{"a":"Chequered Flag","b":"1F3C1","j":["contest","finishline","race","gokart"],"k":[9,27]},"mute":{"a":"Speaker with Cancellation Stroke","b":"1F507","j":["sound","volume","silence","quiet"],"k":[27,9]},"jack_o_lantern":{"a":"Jack-O-Lantern","b":"1F383","j":["halloween","light","pumpkin","creepy","fall"],"k":[8,17]},"atm":{"a":"Automated Teller Machine","b":"1F3E7","j":["money","sales","cash","blue-square","payment","bank"],"k":[12,4]},"grapes":{"a":"Grapes","b":"1F347","j":["fruit","food","wine"],"k":[7,9]},"earth_americas":{"a":"Earth Globe Americas","b":"1F30E","j":["globe","world","USA","international"],"k":[6,6]},"grin":{"a":"Grinning Face with Smiling Eyes","b":"1F601","j":["face","happy","smile","joy","kawaii"],"k":[30,25]},"melon":{"a":"Melon","b":"1F348","j":["fruit","nature","food"],"k":[7,10]},"triangular_flag_on_post":{"a":"Triangular Flag on Post","b":"1F6A9","j":["mark","milestone","place"],"k":[35,14]},"monkey":{"a":"Monkey","b":"1F412","j":["animal","nature","banana","circus"],"k":[12,48]},"christmas_tree":{"a":"Christmas Tree","b":"1F384","j":["festival","vacation","december","xmas","celebration"],"k":[8,18]},"put_litter_in_its_place":{"a":"Put Litter in Its Place Symbol","b":"1F6AE","j":["blue-square","sign","human","info"],"k":[35,19]},"speaker":{"a":"Speaker","b":"1F508","j":["sound","volume","silence","broadcast"],"k":[27,10]},"earth_asia":{"a":"Earth Globe Asia-Australia","b":"1F30F","j":["globe","world","east","international"],"k":[6,7]},"crossed_flags":{"a":"Crossed Flags","b":"1F38C","j":["japanese","nation","country","border"],"k":[8,31]},"joy":{"a":"Face with Tears of Joy","b":"1F602","j":["face","cry","tears","weep","happy","happytears","haha"],"k":[30,26]},"sound":{"a":"Speaker with One Sound Wave","b":"1F509","j":["volume","speaker","broadcast"],"k":[27,11]},"watermelon":{"a":"Watermelon","b":"1F349","j":["fruit","food","picnic","summer"],"k":[7,11]},"gorilla":{"a":"Gorilla","b":"1F98D","j":["animal","nature","circus"],"k":[42,37],"o":9},"fireworks":{"a":"Fireworks","b":"1F386","j":["photo","festival","carnival","congratulations"],"k":[8,25]},"potable_water":{"a":"Potable Water Symbol","b":"1F6B0","j":["blue-square","liquid","restroom","cleaning","faucet"],"k":[35,21]},"wheelchair":{"a":"Wheelchair Symbol","b":"267F","j":["blue-square","disabled","a11y","accessibility"],"k":[48,10],"o":4},"rolling_on_the_floor_laughing":{"a":"Rolling on the Floor Laughing","b":"1F923","k":[38,26],"o":9},"loud_sound":{"a":"Speaker with Three Sound Waves","b":"1F50A","j":["volume","noise","noisy","speaker","broadcast"],"k":[27,12]},"waving_black_flag":{"a":"Waving Black Flag","b":"1F3F4","k":[12,19],"o":7},"tangerine":{"a":"Tangerine","b":"1F34A","j":["food","fruit","nature","orange"],"k":[7,12]},"dog":{"a":"Dog Face","b":"1F436","j":["animal","friend","nature","woof","puppy","pet","faithful"],"k":[13,32]},"sparkler":{"a":"Firework Sparkler","b":"1F387","j":["stars","night","shine"],"k":[8,26]},"globe_with_meridians":{"a":"Globe with Meridians","b":"1F310","j":["earth","international","world","internet","interweb","i18n"],"k":[6,8]},"smiley":{"a":"Smiling Face with Open Mouth","b":"1F603","j":["face","happy","joy","haha",":D",":)","smile","funny"],"k":[30,27],"l":["=)","=-)"],"m":":)"},"loudspeaker":{"a":"Public Address Loudspeaker","b":"1F4E2","j":["volume","sound"],"k":[26,25]},"sparkles":{"a":"Sparkles","b":"2728","j":["stars","shine","shiny","cool","awesome","good","magic"],"k":[49,48]},"dog2":{"a":"Dog","b":"1F415","j":["animal","nature","friend","doge","pet","faithful"],"k":[12,51]},"waving_white_flag":{"a":"Waving White Flag","b":"1F3F3-FE0F","c":"1F3F3","k":[12,15],"o":7},"world_map":{"a":"World Map","b":"1F5FA-FE0F","c":"1F5FA","j":["location","direction"],"k":[30,18],"o":7},"lemon":{"a":"Lemon","b":"1F34B","j":["fruit","nature"],"k":[7,13]},"mens":{"a":"Mens Symbol","b":"1F6B9","j":["toilet","restroom","wc","blue-square","gender","male"],"k":[36,29]},"womens":{"a":"Womens Symbol","b":"1F6BA","j":["purple-square","woman","female","toilet","loo","restroom","gender"],"k":[36,30]},"rainbow-flag":{"a":"Rainbow Flag","b":"1F3F3-FE0F-200D-1F308","c":"1F3F3-200D-1F308","k":[12,14],"o":7},"smile":{"a":"Smiling Face with Open Mouth and Smiling Eyes","b":"1F604","j":["face","happy","joy","funny","haha","laugh","like",":D",":)"],"k":[30,28],"l":["C:","c:",":D",":-D"],"m":":)"},"banana":{"a":"Banana","b":"1F34C","j":["fruit","food","monkey"],"k":[7,14]},"mega":{"a":"Cheering Megaphone","b":"1F4E3","j":["sound","speaker","volume"],"k":[26,26]},"japan":{"a":"Silhouette of Japan","b":"1F5FE","j":["nation","country","japanese","asia"],"k":[30,22]},"poodle":{"a":"Poodle","b":"1F429","j":["dog","animal","101","nature","pet"],"k":[13,19]},"balloon":{"a":"Balloon","b":"1F388","j":["party","celebration","birthday","circus"],"k":[8,27]},"flag-ac":{"a":"Ascension Island Flag","b":"1F1E6-1F1E8","k":[0,31]},"sweat_smile":{"a":"Smiling Face with Open Mouth and Cold Sweat","b":"1F605","j":["face","hot","happy","laugh","sweat","smile","relief"],"k":[30,29]},"pineapple":{"a":"Pineapple","b":"1F34D","j":["fruit","nature","food"],"k":[7,15]},"restroom":{"a":"Restroom","b":"1F6BB","j":["blue-square","toilet","refresh","wc","gender"],"k":[36,31]},"postal_horn":{"a":"Postal Horn","b":"1F4EF","j":["instrument","music"],"k":[26,38]},"wolf":{"a":"Wolf Face","b":"1F43A","j":["animal","nature","wild"],"k":[13,36]},"tada":{"a":"Party Popper","b":"1F389","j":["party","congratulations","birthday","magic","circus","celebration"],"k":[8,28]},"snow_capped_mountain":{"a":"Snow Capped Mountain","b":"1F3D4-FE0F","c":"1F3D4","k":[11,37],"o":7},"laughing":{"a":"Smiling Face with Open Mouth and Tightly-Closed Eyes","b":"1F606","j":["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],"k":[30,30],"l":[":>",":->"],"n":["satisfied"]},"apple":{"a":"Red Apple","b":"1F34E","j":["fruit","mac","school"],"k":[7,16]},"flag-ad":{"a":"Andorra Flag","b":"1F1E6-1F1E9","k":[0,32]},"fox_face":{"a":"Fox Face","b":"1F98A","j":["animal","nature","face"],"k":[42,34],"o":9},"confetti_ball":{"a":"Confetti Ball","b":"1F38A","j":["festival","party","birthday","circus"],"k":[8,29]},"bell":{"a":"Bell","b":"1F514","j":["sound","notification","christmas","xmas","chime"],"k":[27,22]},"mountain":{"a":"Mountain","b":"26F0-FE0F","c":"26F0","j":["photo","nature","environment"],"k":[48,38],"o":5},"baby_symbol":{"a":"Baby Symbol","b":"1F6BC","j":["orange-square","child"],"k":[36,32]},"wc":{"a":"Water Closet","b":"1F6BE","j":["toilet","restroom","blue-square"],"k":[36,34]},"wink":{"a":"Winking Face","b":"1F609","j":["face","happy","mischievous","secret",";)","smile","eye"],"k":[30,33],"l":[";)",";-)"],"m":";)"},"no_bell":{"a":"Bell with Cancellation Stroke","b":"1F515","j":["sound","volume","mute","quiet","silent"],"k":[27,23]},"green_apple":{"a":"Green Apple","b":"1F34F","j":["fruit","nature"],"k":[7,17]},"tanabata_tree":{"a":"Tanabata Tree","b":"1F38B","j":["plant","nature","branch","summer"],"k":[8,30]},"flag-ae":{"a":"United Arab Emirates Flag","b":"1F1E6-1F1EA","k":[0,33]},"volcano":{"a":"Volcano","b":"1F30B","j":["photo","nature","disaster"],"k":[6,3]},"cat":{"a":"Cat Face","b":"1F431","j":["animal","meow","nature","pet","kitten"],"k":[13,27]},"flag-af":{"a":"Afghanistan Flag","b":"1F1E6-1F1EB","k":[0,34]},"musical_score":{"a":"Musical Score","b":"1F3BC","j":["treble","clef","compose"],"k":[9,22]},"blush":{"a":"Smiling Face with Smiling Eyes","b":"1F60A","j":["face","smile","happy","flushed","crush","embarrassed","shy","joy"],"k":[30,34],"m":":)"},"pear":{"a":"Pear","b":"1F350","j":["fruit","nature","food"],"k":[7,18]},"bamboo":{"a":"Pine Decoration","b":"1F38D","j":["plant","nature","vegetable","panda","pine_decoration"],"k":[8,32]},"passport_control":{"a":"Passport Control","b":"1F6C2","j":["custom","blue-square"],"k":[36,43]},"mount_fuji":{"a":"Mount Fuji","b":"1F5FB","j":["photo","mountain","nature","japanese"],"k":[30,19]},"cat2":{"a":"Cat","b":"1F408","j":["animal","meow","pet","cats"],"k":[12,38]},"musical_note":{"a":"Musical Note","b":"1F3B5","j":["score","tone","sound"],"k":[9,15]},"dolls":{"a":"Japanese Dolls","b":"1F38E","j":["japanese","toy","kimono"],"k":[8,33]},"lion_face":{"a":"Lion Face","b":"1F981","k":[42,25],"o":8},"camping":{"a":"Camping","b":"1F3D5-FE0F","c":"1F3D5","j":["photo","outdoors","tent"],"k":[11,38],"o":7},"flag-ag":{"a":"Antigua & Barbuda Flag","b":"1F1E6-1F1EC","k":[0,35]},"customs":{"a":"Customs","b":"1F6C3","j":["passport","border","blue-square"],"k":[36,44]},"yum":{"a":"Face Savouring Delicious Food","b":"1F60B","j":["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],"k":[30,35]},"peach":{"a":"Peach","b":"1F351","j":["fruit","nature","food"],"k":[7,19]},"tiger":{"a":"Tiger Face","b":"1F42F","j":["animal","cat","danger","wild","nature","roar"],"k":[13,25]},"notes":{"a":"Multiple Musical Notes","b":"1F3B6","j":["music","score"],"k":[9,16]},"flags":{"a":"Carp Streamer","b":"1F38F","j":["fish","japanese","koinobori","carp","banner"],"k":[8,34]},"beach_with_umbrella":{"a":"Beach with Umbrella","b":"1F3D6-FE0F","c":"1F3D6","k":[11,39],"o":7},"cherries":{"a":"Cherries","b":"1F352","j":["food","fruit"],"k":[7,20]},"flag-ai":{"a":"Anguilla Flag","b":"1F1E6-1F1EE","k":[0,36]},"baggage_claim":{"a":"Baggage Claim","b":"1F6C4","j":["blue-square","airport","transport"],"k":[36,45]},"sunglasses":{"a":"Smiling Face with Sunglasses","b":"1F60E","j":["face","cool","smile","summer","beach","sunglass"],"k":[30,38],"l":["8)"]},"left_luggage":{"a":"Left Luggage","b":"1F6C5","j":["blue-square","travel"],"k":[36,46]},"wind_chime":{"a":"Wind Chime","b":"1F390","j":["nature","ding","spring","bell"],"k":[8,35]},"strawberry":{"a":"Strawberry","b":"1F353","j":["fruit","food","nature"],"k":[7,21]},"desert":{"a":"Desert","b":"1F3DC-FE0F","c":"1F3DC","j":["photo","warm","saharah"],"k":[11,45],"o":7},"studio_microphone":{"a":"Studio Microphone","b":"1F399-FE0F","c":"1F399","j":["sing","recording","artist","talkshow"],"k":[8,41],"o":7},"flag-al":{"a":"Albania Flag","b":"1F1E6-1F1F1","k":[0,37]},"tiger2":{"a":"Tiger","b":"1F405","j":["animal","nature","roar"],"k":[12,35]},"heart_eyes":{"a":"Smiling Face with Heart-Shaped Eyes","b":"1F60D","j":["face","love","like","affection","valentines","infatuation","crush","heart"],"k":[30,37]},"desert_island":{"a":"Desert Island","b":"1F3DD-FE0F","c":"1F3DD","j":["photo","tropical","mojito"],"k":[11,46],"o":7},"kiwifruit":{"a":"Kiwifruit","b":"1F95D","k":[42,9],"o":9},"rice_scene":{"a":"Moon Viewing Ceremony","b":"1F391","j":["photo","japan","asia","tsukimi"],"k":[8,36]},"kissing_heart":{"a":"Face Throwing a Kiss","b":"1F618","j":["face","love","like","affection","valentines","infatuation","kiss"],"k":[30,48],"l":[":*",":-*"]},"warning":{"a":"Warning Sign","b":"26A0-FE0F","c":"26A0","j":["exclamation","wip","alert","error","problem","issue"],"k":[48,20],"o":4},"flag-am":{"a":"Armenia Flag","b":"1F1E6-1F1F2","k":[0,38]},"leopard":{"a":"Leopard","b":"1F406","j":["animal","nature"],"k":[12,36]},"level_slider":{"a":"Level Slider","b":"1F39A-FE0F","c":"1F39A","j":["scale"],"k":[8,42],"o":7},"horse":{"a":"Horse Face","b":"1F434","j":["animal","brown","nature"],"k":[13,30]},"children_crossing":{"a":"Children Crossing","b":"1F6B8","j":["school","warning","danger","sign","driving","yellow-diamond"],"k":[36,28]},"ribbon":{"a":"Ribbon","b":"1F380","j":["decoration","pink","girl","bowtie"],"k":[8,14]},"national_park":{"a":"National Park","b":"1F3DE-FE0F","c":"1F3DE","j":["photo","environment","nature"],"k":[11,47],"o":7},"control_knobs":{"a":"Control Knobs","b":"1F39B-FE0F","c":"1F39B","j":["dial"],"k":[8,43],"o":7},"kissing":{"a":"Kissing Face","b":"1F617","j":["love","like","face","3","valentines","infatuation","kiss"],"k":[30,47]},"tomato":{"a":"Tomato","b":"1F345","j":["fruit","vegetable","nature","food"],"k":[7,7]},"flag-ao":{"a":"Angola Flag","b":"1F1E6-1F1F4","k":[0,39]},"stadium":{"a":"Stadium","b":"1F3DF-FE0F","c":"1F3DF","j":["photo","place","sports","concert","venue"],"k":[11,48],"o":7},"flag-aq":{"a":"Antarctica Flag","b":"1F1E6-1F1F6","k":[0,40]},"gift":{"a":"Wrapped Present","b":"1F381","j":["present","birthday","christmas","xmas"],"k":[8,15]},"no_entry":{"a":"No Entry","b":"26D4","j":["limit","security","privacy","bad","denied","stop","circle"],"k":[48,35],"o":5},"kissing_smiling_eyes":{"a":"Kissing Face with Smiling Eyes","b":"1F619","j":["face","affection","valentines","infatuation","kiss"],"k":[30,49]},"coconut":{"a":"Coconut","b":"1F965","k":[42,17],"o":10},"racehorse":{"a":"Horse","b":"1F40E","j":["animal","gamble","luck"],"k":[12,44]},"microphone":{"a":"Microphone","b":"1F3A4","j":["sound","music","PA","sing","talkshow"],"k":[8,50]},"classical_building":{"a":"Classical Building","b":"1F3DB-FE0F","c":"1F3DB","j":["art","culture","history"],"k":[11,44],"o":7},"no_entry_sign":{"a":"No Entry Sign","b":"1F6AB","j":["forbid","stop","limit","denied","disallow","circle"],"k":[35,16]},"reminder_ribbon":{"a":"Reminder Ribbon","b":"1F397-FE0F","c":"1F397","j":["sports","cause","support","awareness"],"k":[8,40],"o":7},"kissing_closed_eyes":{"a":"Kissing Face with Closed Eyes","b":"1F61A","j":["face","love","like","affection","valentines","infatuation","kiss"],"k":[30,50]},"unicorn_face":{"a":"Unicorn Face","b":"1F984","k":[42,28],"o":8},"flag-ar":{"a":"Argentina Flag","b":"1F1E6-1F1F7","k":[0,41]},"headphones":{"a":"Headphone","b":"1F3A7","j":["music","score","gadgets"],"k":[9,1]},"avocado":{"a":"Avocado","b":"1F951","j":["fruit","food"],"k":[41,49],"o":9},"relaxed":{"a":"White Smiling Face","b":"263A-FE0F","c":"263A","j":["face","blush","massage","happiness"],"k":[47,41],"o":1},"zebra_face":{"a":"Zebra Face","b":"1F993","k":[42,43],"o":10},"eggplant":{"a":"Aubergine","b":"1F346","j":["vegetable","nature","food","aubergine"],"k":[7,8]},"radio":{"a":"Radio","b":"1F4FB","j":["communication","music","podcast","program"],"k":[26,50]},"building_construction":{"a":"Building Construction","b":"1F3D7-FE0F","c":"1F3D7","j":["wip","working","progress"],"k":[11,40],"o":7},"flag-as":{"a":"American Samoa Flag","b":"1F1E6-1F1F8","k":[0,42]},"admission_tickets":{"a":"Admission Tickets","b":"1F39F-FE0F","c":"1F39F","k":[8,45],"o":7},"no_bicycles":{"a":"No Bicycles","b":"1F6B3","j":["cyclist","prohibited","circle"],"k":[35,24]},"no_smoking":{"a":"No Smoking Symbol","b":"1F6AD","j":["cigarette","blue-square","smell","smoke"],"k":[35,18]},"slightly_smiling_face":{"a":"Slightly Smiling Face","b":"1F642","j":["face","smile"],"k":[31,38],"l":[":)","(:",":-)"],"o":7},"flag-at":{"a":"Austria Flag","b":"1F1E6-1F1F9","k":[0,43]},"ticket":{"a":"Ticket","b":"1F3AB","j":["event","concert","pass"],"k":[9,5]},"saxophone":{"a":"Saxophone","b":"1F3B7","j":["music","instrument","jazz","blues"],"k":[9,17]},"deer":{"a":"Deer","b":"1F98C","j":["animal","nature","horns","venison"],"k":[42,36],"o":9},"house_buildings":{"a":"House Buildings","b":"1F3D8-FE0F","c":"1F3D8","k":[11,41],"o":7},"potato":{"a":"Potato","b":"1F954","j":["food","tuber","vegatable","starch"],"k":[42,0],"o":9},"guitar":{"a":"Guitar","b":"1F3B8","j":["music","instrument"],"k":[9,18]},"carrot":{"a":"Carrot","b":"1F955","j":["vegetable","food","orange"],"k":[42,1],"o":9},"cityscape":{"a":"Cityscape","b":"1F3D9-FE0F","c":"1F3D9","j":["photo","night life","urban"],"k":[11,42],"o":7},"flag-au":{"a":"Australia Flag","b":"1F1E6-1F1FA","k":[0,44]},"do_not_litter":{"a":"Do Not Litter Symbol","b":"1F6AF","j":["trash","bin","garbage","circle"],"k":[35,20]},"hugging_face":{"a":"Hugging Face","b":"1F917","k":[37,31],"o":8},"cow":{"a":"Cow Face","b":"1F42E","j":["beef","ox","animal","nature","moo","milk"],"k":[13,24]},"medal":{"a":"Medal","b":"1F396-FE0F","c":"1F396","k":[8,39],"o":7},"musical_keyboard":{"a":"Musical Keyboard","b":"1F3B9","j":["piano","instrument","compose"],"k":[9,19]},"corn":{"a":"Ear of Maize","b":"1F33D","j":["food","vegetable","plant"],"k":[6,51]},"derelict_house_building":{"a":"Derelict House Building","b":"1F3DA-FE0F","c":"1F3DA","k":[11,43],"o":7},"non-potable_water":{"a":"Non-Potable Water Symbol","b":"1F6B1","j":["drink","faucet","tap","circle"],"k":[35,22]},"trophy":{"a":"Trophy","b":"1F3C6","j":["win","award","contest","place","ftw","ceremony"],"k":[10,19]},"flag-aw":{"a":"Aruba Flag","b":"1F1E6-1F1FC","k":[0,45]},"star-struck":{"a":"Grinning Face with Star Eyes","b":"1F929","k":[38,49],"n":["grinning_face_with_star_eyes"],"o":10},"ox":{"a":"Ox","b":"1F402","j":["animal","cow","beef"],"k":[12,32]},"trumpet":{"a":"Trumpet","b":"1F3BA","j":["music","brass"],"k":[9,20]},"hot_pepper":{"a":"Hot Pepper","b":"1F336-FE0F","c":"1F336","j":["food","spicy","chilli","chili"],"k":[6,44],"o":7},"sports_medal":{"a":"Sports Medal","b":"1F3C5","k":[10,18],"o":7},"flag-ax":{"a":"Åland Islands Flag","b":"1F1E6-1F1FD","k":[0,46]},"water_buffalo":{"a":"Water Buffalo","b":"1F403","j":["animal","nature","ox","cow"],"k":[12,33]},"no_pedestrians":{"a":"No Pedestrians","b":"1F6B7","j":["rules","crossing","walking","circle"],"k":[36,27]},"thinking_face":{"a":"Thinking Face","b":"1F914","k":[37,28],"o":8},"house":{"a":"House Building","b":"1F3E0","j":["building","home"],"k":[11,49]},"no_mobile_phones":{"a":"No Mobile Phones","b":"1F4F5","j":["iphone","mute","circle"],"k":[26,44]},"flag-az":{"a":"Azerbaijan Flag","b":"1F1E6-1F1FF","k":[0,47]},"first_place_medal":{"a":"First Place Medal","b":"1F947","k":[41,42],"o":9},"house_with_garden":{"a":"House with Garden","b":"1F3E1","j":["home","plant","nature"],"k":[11,50]},"violin":{"a":"Violin","b":"1F3BB","j":["music","instrument","orchestra","symphony"],"k":[9,21]},"face_with_raised_eyebrow":{"a":"Face with One Eyebrow Raised","b":"1F928","k":[38,48],"n":["face_with_one_eyebrow_raised"],"o":10},"cucumber":{"a":"Cucumber","b":"1F952","j":["fruit","food","pickle"],"k":[41,50],"o":9},"cow2":{"a":"Cow","b":"1F404","j":["beef","ox","animal","nature","moo","milk"],"k":[12,34]},"flag-ba":{"a":"Bosnia & Herzegovina Flag","b":"1F1E7-1F1E6","k":[0,48]},"pig":{"a":"Pig Face","b":"1F437","j":["animal","oink","nature"],"k":[13,33]},"drum_with_drumsticks":{"a":"Drum with Drumsticks","b":"1F941","k":[41,37],"o":9},"underage":{"a":"No One Under Eighteen Symbol","b":"1F51E","j":["18","drink","pub","night","minor","circle"],"k":[27,32]},"broccoli":{"a":"Broccoli","b":"1F966","k":[42,18],"o":10},"office":{"a":"Office Building","b":"1F3E2","j":["building","bureau","work"],"k":[11,51]},"second_place_medal":{"a":"Second Place Medal","b":"1F948","k":[41,43],"o":9},"neutral_face":{"a":"Neutral Face","b":"1F610","j":["indifference","meh",":|","neutral"],"k":[30,40],"l":[":|",":-|"]},"third_place_medal":{"a":"Third Place Medal","b":"1F949","k":[41,44],"o":9},"mushroom":{"a":"Mushroom","b":"1F344","j":["plant","vegetable"],"k":[7,6]},"flag-bb":{"a":"Barbados Flag","b":"1F1E7-1F1E7","k":[0,49]},"radioactive_sign":{"a":"Radioactive Sign","b":"2622-FE0F","c":"2622","k":[47,33],"o":1},"pig2":{"a":"Pig","b":"1F416","j":["animal","nature"],"k":[13,0]},"expressionless":{"a":"Expressionless Face","b":"1F611","j":["face","indifferent","-_-","meh","deadpan"],"k":[30,41]},"iphone":{"a":"Mobile Phone","b":"1F4F1","j":["technology","apple","gadgets","dial"],"k":[26,40]},"post_office":{"a":"Japanese Post Office","b":"1F3E3","j":["building","envelope","communication"],"k":[12,0]},"european_post_office":{"a":"European Post Office","b":"1F3E4","j":["building","email"],"k":[12,1]},"soccer":{"a":"Soccer Ball","b":"26BD","j":["sports","football"],"k":[48,26],"o":5},"boar":{"a":"Boar","b":"1F417","j":["animal","nature"],"k":[13,1]},"peanuts":{"a":"Peanuts","b":"1F95C","j":["food","nut"],"k":[42,8],"o":9},"calling":{"a":"Mobile Phone with Rightwards Arrow at Left","b":"1F4F2","j":["iphone","incoming"],"k":[26,41]},"biohazard_sign":{"a":"Biohazard Sign","b":"2623-FE0F","c":"2623","k":[47,34],"o":1},"flag-bd":{"a":"Bangladesh Flag","b":"1F1E7-1F1E9","k":[0,50]},"no_mouth":{"a":"Face Without Mouth","b":"1F636","j":["face","hellokitty"],"k":[31,26]},"face_with_rolling_eyes":{"a":"Face with Rolling Eyes","b":"1F644","k":[31,40],"o":8},"phone":{"a":"Black Telephone","b":"260E-FE0F","c":"260E","j":["technology","communication","dial","telephone"],"k":[47,21],"n":["telephone"],"o":1},"pig_nose":{"a":"Pig Nose","b":"1F43D","j":["animal","oink"],"k":[13,39]},"chestnut":{"a":"Chestnut","b":"1F330","j":["food","squirrel"],"k":[6,38]},"arrow_up":{"a":"Upwards Black Arrow","b":"2B06-FE0F","c":"2B06","j":["blue-square","continue","top","direction"],"k":[50,18],"o":4},"hospital":{"a":"Hospital","b":"1F3E5","j":["building","health","surgery","doctor"],"k":[12,2]},"flag-be":{"a":"Belgium Flag","b":"1F1E7-1F1EA","k":[0,51]},"baseball":{"a":"Baseball","b":"26BE","j":["sports","balls"],"k":[48,27],"o":5},"smirk":{"a":"Smirking Face","b":"1F60F","j":["face","smile","mean","prank","smug","sarcasm"],"k":[30,39]},"arrow_upper_right":{"a":"North East Arrow","b":"2197-FE0F","c":"2197","j":["blue-square","point","direction","diagonal","northeast"],"k":[46,36],"o":1},"flag-bf":{"a":"Burkina Faso Flag","b":"1F1E7-1F1EB","k":[1,0]},"basketball":{"a":"Basketball and Hoop","b":"1F3C0","j":["sports","balls","NBA"],"k":[9,26]},"ram":{"a":"Ram","b":"1F40F","j":["animal","sheep","nature"],"k":[12,45]},"bank":{"a":"Bank","b":"1F3E6","j":["building","money","sales","cash","business","enterprise"],"k":[12,3]},"bread":{"a":"Bread","b":"1F35E","j":["food","wheat","breakfast","toast"],"k":[7,32]},"telephone_receiver":{"a":"Telephone Receiver","b":"1F4DE","j":["technology","communication","dial"],"k":[26,21]},"croissant":{"a":"Croissant","b":"1F950","j":["food","bread","french"],"k":[41,48],"o":9},"pager":{"a":"Pager","b":"1F4DF","j":["bbcall","oldschool","90s"],"k":[26,22]},"sheep":{"a":"Sheep","b":"1F411","j":["animal","nature","wool","shipit"],"k":[12,47]},"arrow_right":{"a":"Black Rightwards Arrow","b":"27A1-FE0F","c":"27A1","j":["blue-square","next"],"k":[50,12],"o":1},"persevere":{"a":"Persevering Face","b":"1F623","j":["face","sick","no","upset","oops"],"k":[31,7]},"flag-bg":{"a":"Bulgaria Flag","b":"1F1E7-1F1EC","k":[1,1]},"volleyball":{"a":"Volleyball","b":"1F3D0","j":["sports","balls"],"k":[11,33],"o":8},"hotel":{"a":"Hotel","b":"1F3E8","j":["building","accomodation","checkin"],"k":[12,5]},"arrow_lower_right":{"a":"South East Arrow","b":"2198-FE0F","c":"2198","j":["blue-square","direction","diagonal","southeast"],"k":[46,37],"o":1},"goat":{"a":"Goat","b":"1F410","j":["animal","nature"],"k":[12,46]},"flag-bh":{"a":"Bahrain Flag","b":"1F1E7-1F1ED","k":[1,2]},"love_hotel":{"a":"Love Hotel","b":"1F3E9","j":["like","affection","dating"],"k":[12,6]},"disappointed_relieved":{"a":"Disappointed but Relieved Face","b":"1F625","j":["face","phew","sweat","nervous"],"k":[31,9]},"baguette_bread":{"a":"Baguette Bread","b":"1F956","j":["food","bread","french"],"k":[42,2],"o":9},"football":{"a":"American Football","b":"1F3C8","j":["sports","balls","NFL"],"k":[10,26]},"fax":{"a":"Fax Machine","b":"1F4E0","j":["communication","technology"],"k":[26,23]},"convenience_store":{"a":"Convenience Store","b":"1F3EA","j":["building","shopping","groceries"],"k":[12,7]},"dromedary_camel":{"a":"Dromedary Camel","b":"1F42A","j":["animal","hot","desert","hump"],"k":[13,20]},"arrow_down":{"a":"Downwards Black Arrow","b":"2B07-FE0F","c":"2B07","j":["blue-square","direction","bottom"],"k":[50,19],"o":4},"battery":{"a":"Battery","b":"1F50B","j":["power","energy","sustain"],"k":[27,13]},"rugby_football":{"a":"Rugby Football","b":"1F3C9","j":["sports","team"],"k":[10,27]},"pretzel":{"a":"Pretzel","b":"1F968","k":[42,20],"o":10},"open_mouth":{"a":"Face with Open Mouth","b":"1F62E","j":["face","surprise","impressed","wow","whoa",":O"],"k":[31,18],"l":[":o",":-o",":O",":-O"]},"flag-bi":{"a":"Burundi Flag","b":"1F1E7-1F1EE","k":[1,3]},"flag-bj":{"a":"Benin Flag","b":"1F1E7-1F1EF","k":[1,4]},"pancakes":{"a":"Pancakes","b":"1F95E","j":["food","breakfast","flapjacks","hotcakes"],"k":[42,10],"o":9},"school":{"a":"School","b":"1F3EB","j":["building","student","education","learn","teach"],"k":[12,8]},"tennis":{"a":"Tennis Racquet and Ball","b":"1F3BE","j":["sports","balls","green"],"k":[9,24]},"zipper_mouth_face":{"a":"Zipper-Mouth Face","b":"1F910","j":["face","sealed","zipper","secret"],"k":[37,24],"o":8},"camel":{"a":"Bactrian Camel","b":"1F42B","j":["animal","nature","hot","desert","hump"],"k":[13,21]},"arrow_lower_left":{"a":"South West Arrow","b":"2199-FE0F","c":"2199","j":["blue-square","direction","diagonal","southwest"],"k":[46,38],"o":1},"electric_plug":{"a":"Electric Plug","b":"1F50C","j":["charger","power"],"k":[27,14]},"cheese_wedge":{"a":"Cheese Wedge","b":"1F9C0","k":[42,48],"o":8},"hushed":{"a":"Hushed Face","b":"1F62F","j":["face","woo","shh"],"k":[31,19]},"computer":{"a":"Personal Computer","b":"1F4BB","j":["technology","laptop","screen","display","monitor"],"k":[25,38]},"giraffe_face":{"a":"Giraffe Face","b":"1F992","k":[42,42],"o":10},"8ball":{"a":"Billiards","b":"1F3B1","j":["pool","hobby","game","luck","magic"],"k":[9,11]},"arrow_left":{"a":"Leftwards Black Arrow","b":"2B05-FE0F","c":"2B05","j":["blue-square","previous","back"],"k":[50,17],"o":4},"department_store":{"a":"Department Store","b":"1F3EC","j":["building","shopping","mall"],"k":[12,9]},"meat_on_bone":{"a":"Meat on Bone","b":"1F356","j":["good","food","drumstick"],"k":[7,24]},"arrow_upper_left":{"a":"North West Arrow","b":"2196-FE0F","c":"2196","j":["blue-square","point","direction","diagonal","northwest"],"k":[46,35],"o":1},"flag-bm":{"a":"Bermuda Flag","b":"1F1E7-1F1F2","k":[1,6]},"sleepy":{"a":"Sleepy Face","b":"1F62A","j":["face","tired","rest","nap"],"k":[31,14]},"bowling":{"a":"Bowling","b":"1F3B3","j":["sports","fun","play"],"k":[9,13]},"factory":{"a":"Factory","b":"1F3ED","j":["building","industry","pollution","smoke"],"k":[12,10]},"desktop_computer":{"a":"Desktop Computer","b":"1F5A5-FE0F","c":"1F5A5","j":["technology","computing","screen"],"k":[29,51],"o":7},"elephant":{"a":"Elephant","b":"1F418","j":["animal","nature","nose","th","circus"],"k":[13,2]},"rhinoceros":{"a":"Rhinoceros","b":"1F98F","j":["animal","nature","horn"],"k":[42,39],"o":9},"arrow_up_down":{"a":"Up Down Arrow","b":"2195-FE0F","c":"2195","j":["blue-square","direction","way","vertical"],"k":[46,34],"o":1},"cricket_bat_and_ball":{"a":"Cricket Bat and Ball","b":"1F3CF","k":[11,32],"o":8},"printer":{"a":"Printer","b":"1F5A8-FE0F","c":"1F5A8","j":["paper","ink"],"k":[30,0],"o":7},"poultry_leg":{"a":"Poultry Leg","b":"1F357","j":["food","meat","drumstick","bird","chicken","turkey"],"k":[7,25]},"tired_face":{"a":"Tired Face","b":"1F62B","j":["sick","whine","upset","frustrated"],"k":[31,15]},"japanese_castle":{"a":"Japanese Castle","b":"1F3EF","j":["photo","building"],"k":[12,12]},"flag-bn":{"a":"Brunei Flag","b":"1F1E7-1F1F3","k":[1,7]},"field_hockey_stick_and_ball":{"a":"Field Hockey Stick and Ball","b":"1F3D1","k":[11,34],"o":8},"sleeping":{"a":"Sleeping Face","b":"1F634","j":["face","tired","sleepy","night","zzz"],"k":[31,24]},"left_right_arrow":{"a":"Left Right Arrow","b":"2194-FE0F","c":"2194","j":["shape","direction","horizontal","sideways"],"k":[46,33],"o":1},"keyboard":{"a":"Keyboard","b":"2328-FE0F","c":"2328","j":["technology","computer","type","input","text"],"k":[46,43],"o":1},"european_castle":{"a":"European Castle","b":"1F3F0","j":["building","royalty","history"],"k":[12,13]},"mouse":{"a":"Mouse Face","b":"1F42D","j":["animal","nature","cheese_wedge","rodent"],"k":[13,23]},"flag-bo":{"a":"Bolivia Flag","b":"1F1E7-1F1F4","k":[1,8]},"cut_of_meat":{"a":"Cut of Meat","b":"1F969","k":[42,21],"o":10},"ice_hockey_stick_and_puck":{"a":"Ice Hockey Stick and Puck","b":"1F3D2","k":[11,35],"o":8},"mouse2":{"a":"Mouse","b":"1F401","j":["animal","nature","rodent"],"k":[12,31]},"three_button_mouse":{"a":"Three Button Mouse","b":"1F5B1-FE0F","c":"1F5B1","k":[30,1],"o":7},"leftwards_arrow_with_hook":{"a":"Leftwards Arrow with Hook","b":"21A9-FE0F","c":"21A9","j":["back","return","blue-square","undo","enter"],"k":[46,39],"o":1},"bacon":{"a":"Bacon","b":"1F953","j":["food","breakfast","pork","pig","meat"],"k":[41,51],"o":9},"relieved":{"a":"Relieved Face","b":"1F60C","j":["face","relaxed","phew","massage","happiness"],"k":[30,36]},"wedding":{"a":"Wedding","b":"1F492","j":["love","like","affection","couple","marriage","bride","groom"],"k":[24,44]},"tokyo_tower":{"a":"Tokyo Tower","b":"1F5FC","j":["photo","japanese"],"k":[30,20]},"arrow_right_hook":{"a":"Rightwards Arrow with Hook","b":"21AA-FE0F","c":"21AA","j":["blue-square","return","rotate","direction"],"k":[46,40],"o":1},"hamburger":{"a":"Hamburger","b":"1F354","j":["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],"k":[7,22]},"stuck_out_tongue":{"a":"Face with Stuck-out Tongue","b":"1F61B","j":["face","prank","childish","playful","mischievous","smile","tongue"],"k":[30,51],"l":[":p",":-p",":P",":-P",":b",":-b"],"m":":p"},"trackball":{"a":"Trackball","b":"1F5B2-FE0F","c":"1F5B2","j":["technology","trackpad"],"k":[30,2],"o":7},"flag-br":{"a":"Brazil Flag","b":"1F1E7-1F1F7","k":[1,10]},"rat":{"a":"Rat","b":"1F400","j":["animal","mouse","rodent"],"k":[12,30]},"table_tennis_paddle_and_ball":{"a":"Table Tennis Paddle and Ball","b":"1F3D3","k":[11,36],"o":8},"minidisc":{"a":"Minidisc","b":"1F4BD","j":["technology","record","data","disk","90s"],"k":[25,40]},"stuck_out_tongue_winking_eye":{"a":"Face with Stuck-out Tongue and Winking Eye","b":"1F61C","j":["face","prank","childish","playful","mischievous","smile","wink","tongue"],"k":[31,0],"l":[";p",";-p",";b",";-b",";P",";-P"],"m":";p"},"fries":{"a":"French Fries","b":"1F35F","j":["chips","snack","fast food"],"k":[7,33]},"badminton_racquet_and_shuttlecock":{"a":"Badminton Racquet and Shuttlecock","b":"1F3F8","k":[12,22],"o":8},"statue_of_liberty":{"a":"Statue of Liberty","b":"1F5FD","j":["american","newyork"],"k":[30,21]},"flag-bs":{"a":"Bahamas Flag","b":"1F1E7-1F1F8","k":[1,11]},"arrow_heading_up":{"a":"Arrow Pointing Rightwards Then Curving Upwards","b":"2934-FE0F","c":"2934","j":["blue-square","direction","top"],"k":[50,15],"o":3},"hamster":{"a":"Hamster Face","b":"1F439","j":["animal","nature"],"k":[13,35]},"stuck_out_tongue_closed_eyes":{"a":"Face with Stuck-out Tongue and Tightly-Closed Eyes","b":"1F61D","j":["face","prank","playful","mischievous","smile","tongue"],"k":[31,1]},"pizza":{"a":"Slice of Pizza","b":"1F355","j":["food","party"],"k":[7,23]},"boxing_glove":{"a":"Boxing Glove","b":"1F94A","j":["sports","fighting"],"k":[41,45],"o":9},"floppy_disk":{"a":"Floppy Disk","b":"1F4BE","j":["oldschool","technology","save","90s","80s"],"k":[25,41]},"arrow_heading_down":{"a":"Arrow Pointing Rightwards Then Curving Downwards","b":"2935-FE0F","c":"2935","j":["blue-square","direction","bottom"],"k":[50,16],"o":3},"flag-bt":{"a":"Bhutan Flag","b":"1F1E7-1F1F9","k":[1,12]},"rabbit":{"a":"Rabbit Face","b":"1F430","j":["animal","nature","pet","spring","magic","bunny"],"k":[13,26]},"church":{"a":"Church","b":"26EA","j":["building","religion","christ"],"k":[48,37],"o":5},"drooling_face":{"a":"Drooling Face","b":"1F924","j":["face"],"k":[38,27],"o":9},"flag-bv":{"a":"Bouvet Island Flag","b":"1F1E7-1F1FB","k":[1,13]},"mosque":{"a":"Mosque","b":"1F54C","j":["islam","worship","minaret"],"k":[28,15],"o":8},"rabbit2":{"a":"Rabbit","b":"1F407","j":["animal","nature","pet","magic","spring"],"k":[12,37]},"hotdog":{"a":"Hot Dog","b":"1F32D","j":["food","frankfurter"],"k":[6,35],"o":8},"martial_arts_uniform":{"a":"Martial Arts Uniform","b":"1F94B","j":["judo","karate","taekwondo"],"k":[41,46],"o":9},"arrows_clockwise":{"a":"Clockwise Downwards and Upwards Open Circle Arrows","b":"1F503","j":["sync","cycle","round","repeat"],"k":[27,5]},"cd":{"a":"Optical Disc","b":"1F4BF","j":["technology","dvd","disk","disc","90s"],"k":[25,42]},"arrows_counterclockwise":{"a":"Anticlockwise Downwards and Upwards Open Circle Arrows","b":"1F504","j":["blue-square","sync","cycle"],"k":[27,6]},"sandwich":{"a":"Sandwich","b":"1F96A","k":[42,22],"o":10},"chipmunk":{"a":"Chipmunk","b":"1F43F-FE0F","c":"1F43F","j":["animal","nature","rodent","squirrel"],"k":[13,41],"o":7},"synagogue":{"a":"Synagogue","b":"1F54D","j":["judaism","worship","temple","jewish"],"k":[28,16],"o":8},"unamused":{"a":"Unamused Face","b":"1F612","j":["indifference","bored","straight face","serious","sarcasm"],"k":[30,42],"m":":("},"goal_net":{"a":"Goal Net","b":"1F945","j":["sports"],"k":[41,41],"o":9},"flag-bw":{"a":"Botswana Flag","b":"1F1E7-1F1FC","k":[1,14]},"dvd":{"a":"Dvd","b":"1F4C0","j":["cd","disk","disc"],"k":[25,43]},"hedgehog":{"a":"Hedgehog","b":"1F994","k":[42,44],"o":10},"dart":{"a":"Direct Hit","b":"1F3AF","j":["game","play","bar"],"k":[9,9]},"taco":{"a":"Taco","b":"1F32E","j":["food","mexican"],"k":[6,36],"o":8},"back":{"a":"Back with Leftwards Arrow Above","b":"1F519","j":["arrow","words","return"],"k":[27,27]},"flag-by":{"a":"Belarus Flag","b":"1F1E7-1F1FE","k":[1,15]},"shinto_shrine":{"a":"Shinto Shrine","b":"26E9-FE0F","c":"26E9","j":["temple","japan","kyoto"],"k":[48,36],"o":5},"movie_camera":{"a":"Movie Camera","b":"1F3A5","j":["film","record"],"k":[8,51]},"sweat":{"a":"Face with Cold Sweat","b":"1F613","j":["face","hot","sad","tired","exercise"],"k":[30,43]},"burrito":{"a":"Burrito","b":"1F32F","j":["food","mexican"],"k":[6,37],"o":8},"flag-bz":{"a":"Belize Flag","b":"1F1E7-1F1FF","k":[1,16]},"pensive":{"a":"Pensive Face","b":"1F614","j":["face","sad","depressed","upset"],"k":[30,44]},"kaaba":{"a":"Kaaba","b":"1F54B","j":["mecca","mosque","islam"],"k":[28,14],"o":8},"film_frames":{"a":"Film Frames","b":"1F39E-FE0F","c":"1F39E","k":[8,44],"o":7},"bat":{"a":"Bat","b":"1F987","j":["animal","nature","blind","vampire"],"k":[42,31],"o":9},"golf":{"a":"Flag in Hole","b":"26F3","j":["sports","business","flag","hole","summer"],"k":[48,41],"o":5},"end":{"a":"End with Leftwards Arrow Above","b":"1F51A","j":["words","arrow"],"k":[27,28]},"film_projector":{"a":"Film Projector","b":"1F4FD-FE0F","c":"1F4FD","j":["video","tape","record","movie"],"k":[27,0],"o":7},"bear":{"a":"Bear Face","b":"1F43B","j":["animal","nature","wild"],"k":[13,37]},"ice_skate":{"a":"Ice Skate","b":"26F8-FE0F","c":"26F8","j":["sports"],"k":[48,45],"o":5},"fountain":{"a":"Fountain","b":"26F2","j":["photo","summer","water","fresh"],"k":[48,40],"o":5},"confused":{"a":"Confused Face","b":"1F615","j":["face","indifference","huh","weird","hmmm",":/"],"k":[30,45],"l":[":\\",":-\\",":/",":-/"]},"flag-ca":{"a":"Canada Flag","b":"1F1E8-1F1E6","k":[1,17]},"on":{"a":"On with Exclamation Mark with Left Right Arrow Above","b":"1F51B","j":["arrow","words"],"k":[27,29]},"stuffed_flatbread":{"a":"Stuffed Flatbread","b":"1F959","j":["food","flatbread","stuffed","gyro"],"k":[42,5],"o":9},"soon":{"a":"Soon with Rightwards Arrow Above","b":"1F51C","j":["arrow","words"],"k":[27,30]},"upside_down_face":{"a":"Upside-Down Face","b":"1F643","j":["face","flipped","silly","smile"],"k":[31,39],"o":8},"fishing_pole_and_fish":{"a":"Fishing Pole and Fish","b":"1F3A3","j":["food","hobby","summer"],"k":[8,49]},"tent":{"a":"Tent","b":"26FA","j":["photo","camping","outdoors"],"k":[49,12],"o":5},"clapper":{"a":"Clapper Board","b":"1F3AC","j":["movie","film","record"],"k":[9,6]},"egg":{"a":"Egg","b":"1F95A","j":["food","chicken","breakfast"],"k":[42,6],"o":9},"flag-cc":{"a":"Cocos (keeling) Islands Flag","b":"1F1E8-1F1E8","k":[1,18]},"koala":{"a":"Koala","b":"1F428","j":["animal","nature"],"k":[13,18]},"foggy":{"a":"Foggy","b":"1F301","j":["photo","mountain"],"k":[5,45]},"tv":{"a":"Television","b":"1F4FA","j":["technology","program","oldschool","show","television"],"k":[26,49]},"panda_face":{"a":"Panda Face","b":"1F43C","j":["animal","nature","panda"],"k":[13,38]},"fried_egg":{"a":"Cooking","b":"1F373","j":["food","breakfast","kitchen","egg"],"k":[8,1],"n":["cooking"]},"top":{"a":"Top with Upwards Arrow Above","b":"1F51D","j":["words","blue-square"],"k":[27,31]},"flag-cd":{"a":"Congo - Kinshasa Flag","b":"1F1E8-1F1E9","k":[1,19]},"money_mouth_face":{"a":"Money-Mouth Face","b":"1F911","j":["face","rich","dollar","money"],"k":[37,25],"o":8},"running_shirt_with_sash":{"a":"Running Shirt with Sash","b":"1F3BD","j":["play","pageant"],"k":[9,23]},"astonished":{"a":"Astonished Face","b":"1F632","j":["face","xox","surprised","poisoned"],"k":[31,22]},"feet":{"a":"Paw Prints","b":"1F43E","k":[13,40],"n":["paw_prints"]},"camera":{"a":"Camera","b":"1F4F7","j":["gadgets","photography"],"k":[26,46]},"flag-cf":{"a":"Central African Republic Flag","b":"1F1E8-1F1EB","k":[1,20]},"place_of_worship":{"a":"Place of Worship","b":"1F6D0","j":["religion","church","temple","prayer"],"k":[37,5],"o":8},"night_with_stars":{"a":"Night with Stars","b":"1F303","j":["evening","city","downtown"],"k":[5,47]},"ski":{"a":"Ski and Ski Boot","b":"1F3BF","j":["sports","winter","cold","snow"],"k":[9,25]},"shallow_pan_of_food":{"a":"Shallow Pan of Food","b":"1F958","j":["food","cooking","casserole","paella"],"k":[42,4],"o":9},"camera_with_flash":{"a":"Camera with Flash","b":"1F4F8","k":[26,47],"o":7},"sunrise_over_mountains":{"a":"Sunrise over Mountains","b":"1F304","j":["view","vacation","photo"],"k":[5,48]},"turkey":{"a":"Turkey","b":"1F983","j":["animal","bird"],"k":[42,27],"o":8},"white_frowning_face":{"a":"White Frowning Face","b":"2639-FE0F","c":"2639","k":[47,40],"o":1},"flag-cg":{"a":"Congo - Brazzaville Flag","b":"1F1E8-1F1EC","k":[1,21]},"stew":{"a":"Pot of Food","b":"1F372","j":["food","meat","soup"],"k":[8,0]},"sled":{"a":"Sled","b":"1F6F7","k":[37,22],"o":10},"atom_symbol":{"a":"Atom Symbol","b":"269B-FE0F","c":"269B","j":["science","physics","chemistry"],"k":[48,18],"o":4},"curling_stone":{"a":"Curling Stone","b":"1F94C","k":[41,47],"o":10},"slightly_frowning_face":{"a":"Slightly Frowning Face","b":"1F641","j":["face","frowning","disappointed","sad","upset"],"k":[31,37],"o":7},"sunrise":{"a":"Sunrise","b":"1F305","j":["morning","view","vacation","photo"],"k":[5,49]},"om_symbol":{"a":"Om Symbol","b":"1F549-FE0F","c":"1F549","k":[28,12],"o":7},"chicken":{"a":"Chicken","b":"1F414","j":["animal","cluck","nature","bird"],"k":[12,50]},"bowl_with_spoon":{"a":"Bowl with Spoon","b":"1F963","k":[42,15],"o":10},"flag-ch":{"a":"Switzerland Flag","b":"1F1E8-1F1ED","k":[1,22]},"video_camera":{"a":"Video Camera","b":"1F4F9","j":["film","record"],"k":[26,48]},"video_game":{"a":"Video Game","b":"1F3AE","j":["play","console","PS4","controller"],"k":[9,8]},"rooster":{"a":"Rooster","b":"1F413","j":["animal","nature","chicken"],"k":[12,49]},"vhs":{"a":"Videocassette","b":"1F4FC","j":["record","video","oldschool","90s","80s"],"k":[26,51]},"city_sunset":{"a":"Cityscape at Dusk","b":"1F306","j":["photo","evening","sky","buildings"],"k":[5,50]},"confounded":{"a":"Confounded Face","b":"1F616","j":["face","confused","sick","unwell","oops",":S"],"k":[30,46]},"green_salad":{"a":"Green Salad","b":"1F957","j":["food","healthy","lettuce"],"k":[42,3],"o":9},"star_of_david":{"a":"Star of David","b":"2721-FE0F","c":"2721","j":["judaism"],"k":[49,47],"o":1},"flag-ci":{"a":"Côte D’ivoire Flag","b":"1F1E8-1F1EE","k":[1,23]},"popcorn":{"a":"Popcorn","b":"1F37F","j":["food","movie theater","films","snack"],"k":[8,13],"o":8},"city_sunrise":{"a":"Sunset over Buildings","b":"1F307","j":["photo","good morning","dawn"],"k":[5,51]},"disappointed":{"a":"Disappointed Face","b":"1F61E","j":["face","sad","upset","depressed",":("],"k":[31,2],"l":["):",":(",":-("],"m":":("},"mag":{"a":"Left-Pointing Magnifying Glass","b":"1F50D","j":["search","zoom","find","detective"],"k":[27,15]},"hatching_chick":{"a":"Hatching Chick","b":"1F423","j":["animal","chicken","egg","born","baby","bird"],"k":[13,13]},"joystick":{"a":"Joystick","b":"1F579-FE0F","c":"1F579","j":["game","play"],"k":[29,20],"o":7},"wheel_of_dharma":{"a":"Wheel of Dharma","b":"2638-FE0F","c":"2638","j":["hinduism","buddhism","sikhism","jainism"],"k":[47,39],"o":1},"flag-ck":{"a":"Cook Islands Flag","b":"1F1E8-1F1F0","k":[1,24]},"canned_food":{"a":"Canned Food","b":"1F96B","k":[42,23],"o":10},"worried":{"a":"Worried Face","b":"1F61F","j":["face","concern","nervous",":("],"k":[31,3]},"baby_chick":{"a":"Baby Chick","b":"1F424","j":["animal","chicken","bird"],"k":[13,14]},"flag-cl":{"a":"Chile Flag","b":"1F1E8-1F1F1","k":[1,25]},"game_die":{"a":"Game Die","b":"1F3B2","j":["dice","random","tabletop","play","luck"],"k":[9,12]},"mag_right":{"a":"Right-Pointing Magnifying Glass","b":"1F50E","j":["search","zoom","find","detective"],"k":[27,16]},"yin_yang":{"a":"Yin Yang","b":"262F-FE0F","c":"262F","j":["balance"],"k":[47,38],"o":1},"bridge_at_night":{"a":"Bridge at Night","b":"1F309","j":["photo","sanfrancisco"],"k":[6,1]},"spades":{"a":"Black Spade Suit","b":"2660-FE0F","c":"2660","j":["poker","cards","suits","magic"],"k":[48,4],"o":1},"hatched_chick":{"a":"Front-Facing Baby Chick","b":"1F425","j":["animal","chicken","baby","bird"],"k":[13,15]},"flag-cm":{"a":"Cameroon Flag","b":"1F1E8-1F1F2","k":[1,26]},"latin_cross":{"a":"Latin Cross","b":"271D-FE0F","c":"271D","j":["christianity"],"k":[49,46],"o":1},"triumph":{"a":"Face with Look of Triumph","b":"1F624","j":["face","gas","phew","proud","pride"],"k":[31,8]},"hotsprings":{"a":"Hot Springs","b":"2668-FE0F","c":"2668","j":["bath","warm","relax"],"k":[48,8],"o":1},"bento":{"a":"Bento Box","b":"1F371","j":["food","japanese","box"],"k":[7,51]},"microscope":{"a":"Microscope","b":"1F52C","j":["laboratory","experiment","zoomin","science","study"],"k":[27,46]},"cry":{"a":"Crying Face","b":"1F622","j":["face","tears","sad","depressed","upset",":'("],"k":[31,6],"l":[":'("],"m":":'("},"bird":{"a":"Bird","b":"1F426","j":["animal","nature","fly","tweet","spring"],"k":[13,16]},"cn":{"a":"China Flag","b":"1F1E8-1F1F3","j":["china","chinese","prc","flag","country","nation","banner"],"k":[1,27],"n":["flag-cn"]},"telescope":{"a":"Telescope","b":"1F52D","j":["stars","space","zoom","science","astronomy"],"k":[27,47]},"rice_cracker":{"a":"Rice Cracker","b":"1F358","j":["food","japanese"],"k":[7,26]},"hearts":{"a":"Black Heart Suit","b":"2665-FE0F","c":"2665","j":["poker","cards","magic","suits"],"k":[48,6],"o":1},"orthodox_cross":{"a":"Orthodox Cross","b":"2626-FE0F","c":"2626","j":["suppedaneum","religion"],"k":[47,35],"o":1},"milky_way":{"a":"Milky Way","b":"1F30C","j":["photo","space","stars"],"k":[6,4]},"rice_ball":{"a":"Rice Ball","b":"1F359","j":["food","japanese"],"k":[7,27]},"satellite_antenna":{"a":"Satellite Antenna","b":"1F4E1","k":[26,24]},"flag-co":{"a":"Colombia Flag","b":"1F1E8-1F1F4","k":[1,28]},"carousel_horse":{"a":"Carousel Horse","b":"1F3A0","j":["photo","carnival"],"k":[8,46]},"sob":{"a":"Loudly Crying Face","b":"1F62D","j":["face","cry","tears","sad","upset","depressed"],"k":[31,17],"m":":'("},"diamonds":{"a":"Black Diamond Suit","b":"2666-FE0F","c":"2666","j":["poker","cards","magic","suits"],"k":[48,7],"o":1},"star_and_crescent":{"a":"Star and Crescent","b":"262A-FE0F","c":"262A","j":["islam"],"k":[47,36],"o":1},"penguin":{"a":"Penguin","b":"1F427","j":["animal","nature"],"k":[13,17]},"dove_of_peace":{"a":"Dove of Peace","b":"1F54A-FE0F","c":"1F54A","k":[28,13],"o":7},"flag-cp":{"a":"Clipperton Island Flag","b":"1F1E8-1F1F5","k":[1,29]},"ferris_wheel":{"a":"Ferris Wheel","b":"1F3A1","j":["photo","carnival","londoneye"],"k":[8,47]},"clubs":{"a":"Black Club Suit","b":"2663-FE0F","c":"2663","j":["poker","cards","magic","suits"],"k":[48,5],"o":1},"peace_symbol":{"a":"Peace Symbol","b":"262E-FE0F","c":"262E","j":["hippie"],"k":[47,37],"o":1},"candle":{"a":"Candle","b":"1F56F-FE0F","c":"1F56F","j":["fire","wax"],"k":[28,42],"o":7},"frowning":{"a":"Frowning Face with Open Mouth","b":"1F626","j":["face","aw","what"],"k":[31,10]},"rice":{"a":"Cooked Rice","b":"1F35A","j":["food","china","asian"],"k":[7,28]},"flag-cr":{"a":"Costa Rica Flag","b":"1F1E8-1F1F7","k":[1,30]},"roller_coaster":{"a":"Roller Coaster","b":"1F3A2","j":["carnival","playground","photo","fun"],"k":[8,48]},"menorah_with_nine_branches":{"a":"Menorah with Nine Branches","b":"1F54E","k":[28,17],"o":8},"black_joker":{"a":"Playing Card Black Joker","b":"1F0CF","j":["poker","cards","game","play","magic"],"k":[0,15]},"eagle":{"a":"Eagle","b":"1F985","j":["animal","nature","bird"],"k":[42,29],"o":9},"curry":{"a":"Curry and Rice","b":"1F35B","j":["food","spicy","hot","indian"],"k":[7,29]},"bulb":{"a":"Electric Light Bulb","b":"1F4A1","j":["light","electricity","idea"],"k":[25,7]},"anguished":{"a":"Anguished Face","b":"1F627","j":["face","stunned","nervous"],"k":[31,11],"l":["D:"]},"flag-cu":{"a":"Cuba Flag","b":"1F1E8-1F1FA","k":[1,31]},"barber":{"a":"Barber Pole","b":"1F488","j":["hair","salon","style"],"k":[24,34]},"duck":{"a":"Duck","b":"1F986","j":["animal","nature","bird","mallard"],"k":[42,30],"o":9},"six_pointed_star":{"a":"Six Pointed Star with Middle Dot","b":"1F52F","j":["purple-square","religion","jewish","hexagram"],"k":[27,49]},"ramen":{"a":"Steaming Bowl","b":"1F35C","j":["food","japanese","noodle","chopsticks"],"k":[7,30]},"flashlight":{"a":"Electric Torch","b":"1F526","j":["dark","camping","sight","night"],"k":[27,40]},"mahjong":{"a":"Mahjong Tile Red Dragon","b":"1F004","j":["game","play","chinese","kanji"],"k":[0,14],"o":5},"fearful":{"a":"Fearful Face","b":"1F628","j":["face","scared","terrified","nervous","oops","huh"],"k":[31,12]},"aries":{"a":"Aries","b":"2648","j":["sign","purple-square","zodiac","astrology"],"k":[47,44],"o":1},"spaghetti":{"a":"Spaghetti","b":"1F35D","j":["food","italian","noodle"],"k":[7,31]},"circus_tent":{"a":"Circus Tent","b":"1F3AA","j":["festival","carnival","party"],"k":[9,4]},"izakaya_lantern":{"a":"Izakaya Lantern","b":"1F3EE","j":["light","paper","halloween","spooky"],"k":[12,11],"n":["lantern"]},"flag-cv":{"a":"Cape Verde Flag","b":"1F1E8-1F1FB","k":[1,32]},"weary":{"a":"Weary Face","b":"1F629","j":["face","tired","sleepy","sad","frustrated","upset"],"k":[31,13]},"flower_playing_cards":{"a":"Flower Playing Cards","b":"1F3B4","j":["game","sunset","red"],"k":[9,14]},"owl":{"a":"Owl","b":"1F989","j":["animal","nature","bird","hoot"],"k":[42,33],"o":9},"performing_arts":{"a":"Performing Arts","b":"1F3AD","j":["acting","theater","drama"],"k":[9,7]},"frog":{"a":"Frog Face","b":"1F438","j":["animal","nature","croak","toad"],"k":[13,34]},"flag-cw":{"a":"Curaçao Flag","b":"1F1E8-1F1FC","k":[1,33]},"notebook_with_decorative_cover":{"a":"Notebook with Decorative Cover","b":"1F4D4","j":["classroom","notes","record","paper","study"],"k":[26,11]},"exploding_head":{"a":"Shocked Face with Exploding Head","b":"1F92F","k":[39,3],"n":["shocked_face_with_exploding_head"],"o":10},"taurus":{"a":"Taurus","b":"2649","j":["purple-square","sign","zodiac","astrology"],"k":[47,45],"o":1},"sweet_potato":{"a":"Roasted Sweet Potato","b":"1F360","j":["food","nature"],"k":[7,34]},"closed_book":{"a":"Closed Book","b":"1F4D5","j":["read","library","knowledge","textbook","learn"],"k":[26,12]},"gemini":{"a":"Gemini","b":"264A","j":["sign","zodiac","purple-square","astrology"],"k":[47,46],"o":1},"frame_with_picture":{"a":"Frame with Picture","b":"1F5BC-FE0F","c":"1F5BC","k":[30,3],"o":7},"flag-cx":{"a":"Christmas Island Flag","b":"1F1E8-1F1FD","k":[1,34]},"grimacing":{"a":"Grimacing Face","b":"1F62C","j":["face","grimace","teeth"],"k":[31,16]},"crocodile":{"a":"Crocodile","b":"1F40A","j":["animal","nature","reptile","lizard","alligator"],"k":[12,40]},"oden":{"a":"Oden","b":"1F362","j":["food","japanese"],"k":[7,36]},"flag-cy":{"a":"Cyprus Flag","b":"1F1E8-1F1FE","k":[1,35]},"book":{"a":"Open Book","b":"1F4D6","k":[26,13],"n":["open_book"]},"turtle":{"a":"Turtle","b":"1F422","j":["animal","slow","nature","tortoise"],"k":[13,12]},"art":{"a":"Artist Palette","b":"1F3A8","j":["design","paint","draw","colors"],"k":[9,2]},"sushi":{"a":"Sushi","b":"1F363","j":["food","fish","japanese","rice"],"k":[7,37]},"cold_sweat":{"a":"Face with Open Mouth and Cold Sweat","b":"1F630","j":["face","nervous","sweat"],"k":[31,20]},"cancer":{"a":"Cancer","b":"264B","j":["sign","zodiac","purple-square","astrology"],"k":[47,47],"o":1},"fried_shrimp":{"a":"Fried Shrimp","b":"1F364","j":["food","animal","appetizer","summer"],"k":[7,38]},"slot_machine":{"a":"Slot Machine","b":"1F3B0","j":["bet","gamble","vegas","fruit machine","luck","casino"],"k":[9,10]},"scream":{"a":"Face Screaming in Fear","b":"1F631","j":["face","munch","scared","omg"],"k":[31,21]},"green_book":{"a":"Green Book","b":"1F4D7","j":["read","library","knowledge","study"],"k":[26,14]},"leo":{"a":"Leo","b":"264C","j":["sign","purple-square","zodiac","astrology"],"k":[47,48],"o":1},"flag-cz":{"a":"Czechia Flag","b":"1F1E8-1F1FF","k":[1,36]},"lizard":{"a":"Lizard","b":"1F98E","j":["animal","nature","reptile"],"k":[42,38],"o":9},"virgo":{"a":"Virgo","b":"264D","j":["sign","zodiac","purple-square","astrology"],"k":[47,49],"o":1},"steam_locomotive":{"a":"Steam Locomotive","b":"1F682","j":["transportation","vehicle","train"],"k":[34,10]},"de":{"a":"Germany Flag","b":"1F1E9-1F1EA","j":["german","nation","flag","country","banner"],"k":[1,37],"n":["flag-de"]},"flushed":{"a":"Flushed Face","b":"1F633","j":["face","blush","shy","flattered"],"k":[31,23]},"blue_book":{"a":"Blue Book","b":"1F4D8","j":["read","library","knowledge","learn","study"],"k":[26,15]},"snake":{"a":"Snake","b":"1F40D","j":["animal","evil","nature","hiss","python"],"k":[12,43]},"fish_cake":{"a":"Fish Cake with Swirl Design","b":"1F365","j":["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],"k":[7,39]},"railway_car":{"a":"Railway Car","b":"1F683","j":["transportation","vehicle"],"k":[34,11]},"dango":{"a":"Dango","b":"1F361","j":["food","dessert","sweet","japanese","barbecue","meat"],"k":[7,35]},"orange_book":{"a":"Orange Book","b":"1F4D9","j":["read","library","knowledge","textbook","study"],"k":[26,16]},"libra":{"a":"Libra","b":"264E","j":["sign","purple-square","zodiac","astrology"],"k":[47,50],"o":1},"dragon_face":{"a":"Dragon Face","b":"1F432","j":["animal","myth","nature","chinese","green"],"k":[13,28]},"zany_face":{"a":"Grinning Face with One Large and One Small Eye","b":"1F92A","k":[38,50],"n":["grinning_face_with_one_large_and_one_small_eye"],"o":10},"books":{"a":"Books","b":"1F4DA","j":["literature","library","study"],"k":[26,17]},"dragon":{"a":"Dragon","b":"1F409","j":["animal","myth","nature","chinese","green"],"k":[12,39]},"flag-dj":{"a":"Djibouti Flag","b":"1F1E9-1F1EF","k":[1,39]},"dumpling":{"a":"Dumpling","b":"1F95F","k":[42,11],"o":10},"dizzy_face":{"a":"Dizzy Face","b":"1F635","j":["spent","unconscious","xox","dizzy"],"k":[31,25]},"scorpius":{"a":"Scorpius","b":"264F","j":["sign","zodiac","purple-square","astrology","scorpio"],"k":[47,51],"o":1},"bullettrain_side":{"a":"High-Speed Train","b":"1F684","j":["transportation","vehicle"],"k":[34,12]},"bullettrain_front":{"a":"High-Speed Train with Bullet Nose","b":"1F685","j":["transportation","vehicle","speed","fast","public","travel"],"k":[34,13]},"notebook":{"a":"Notebook","b":"1F4D3","j":["stationery","record","notes","paper","study"],"k":[26,10]},"fortune_cookie":{"a":"Fortune Cookie","b":"1F960","k":[42,12],"o":10},"sagittarius":{"a":"Sagittarius","b":"2650","j":["sign","zodiac","purple-square","astrology"],"k":[48,0],"o":1},"sauropod":{"a":"Sauropod","b":"1F995","k":[42,45],"o":10},"flag-dk":{"a":"Denmark Flag","b":"1F1E9-1F1F0","k":[1,40]},"rage":{"a":"Pouting Face","b":"1F621","j":["angry","mad","hate","despise"],"k":[31,5]},"ledger":{"a":"Ledger","b":"1F4D2","j":["notes","paper"],"k":[26,9]},"angry":{"a":"Angry Face","b":"1F620","j":["mad","face","annoyed","frustrated"],"k":[31,4],"l":[">:(",">:-("]},"t-rex":{"a":"T-Rex","b":"1F996","k":[42,46],"o":10},"capricorn":{"a":"Capricorn","b":"2651","j":["sign","zodiac","purple-square","astrology"],"k":[48,1],"o":1},"takeout_box":{"a":"Takeout Box","b":"1F961","k":[42,13],"o":10},"flag-dm":{"a":"Dominica Flag","b":"1F1E9-1F1F2","k":[1,41]},"train2":{"a":"Train","b":"1F686","j":["transportation","vehicle"],"k":[34,14]},"page_with_curl":{"a":"Page with Curl","b":"1F4C3","j":["documents","office","paper"],"k":[25,46]},"whale":{"a":"Spouting Whale","b":"1F433","j":["animal","nature","sea","ocean"],"k":[13,29]},"face_with_symbols_on_mouth":{"a":"Serious Face with Symbols Covering Mouth","b":"1F92C","k":[39,0],"n":["serious_face_with_symbols_covering_mouth"],"o":10},"flag-do":{"a":"Dominican Republic Flag","b":"1F1E9-1F1F4","k":[1,42]},"metro":{"a":"Metro","b":"1F687","j":["transportation","blue-square","mrt","underground","tube"],"k":[34,15]},"icecream":{"a":"Soft Ice Cream","b":"1F366","j":["food","hot","dessert","summer"],"k":[7,40]},"aquarius":{"a":"Aquarius","b":"2652","j":["sign","purple-square","zodiac","astrology"],"k":[48,2],"o":1},"flag-dz":{"a":"Algeria Flag","b":"1F1E9-1F1FF","k":[1,43]},"whale2":{"a":"Whale","b":"1F40B","j":["animal","nature","sea","ocean"],"k":[12,41]},"mask":{"a":"Face with Medical Mask","b":"1F637","j":["face","sick","ill","disease"],"k":[31,27]},"scroll":{"a":"Scroll","b":"1F4DC","j":["documents","ancient","history","paper"],"k":[26,19]},"shaved_ice":{"a":"Shaved Ice","b":"1F367","j":["hot","dessert","summer"],"k":[7,41]},"pisces":{"a":"Pisces","b":"2653","j":["purple-square","sign","zodiac","astrology"],"k":[48,3],"o":1},"light_rail":{"a":"Light Rail","b":"1F688","j":["transportation","vehicle"],"k":[34,16]},"dolphin":{"a":"Dolphin","b":"1F42C","j":["animal","nature","fish","sea","ocean","flipper","fins","beach"],"k":[13,22],"n":["flipper"]},"face_with_thermometer":{"a":"Face with Thermometer","b":"1F912","j":["sick","temperature","thermometer","cold","fever"],"k":[37,26],"o":8},"ophiuchus":{"a":"Ophiuchus","b":"26CE","j":["sign","purple-square","constellation","astrology"],"k":[48,31]},"station":{"a":"Station","b":"1F689","j":["transportation","vehicle","public"],"k":[34,17]},"ice_cream":{"a":"Ice Cream","b":"1F368","j":["food","hot","dessert"],"k":[7,42]},"page_facing_up":{"a":"Page Facing Up","b":"1F4C4","j":["documents","office","paper","information"],"k":[25,47]},"doughnut":{"a":"Doughnut","b":"1F369","j":["food","dessert","snack","sweet","donut"],"k":[7,43]},"face_with_head_bandage":{"a":"Face with Head-Bandage","b":"1F915","j":["injured","clumsy","bandage","hurt"],"k":[37,29],"o":8},"fish":{"a":"Fish","b":"1F41F","j":["animal","food","nature"],"k":[13,9]},"newspaper":{"a":"Newspaper","b":"1F4F0","j":["press","headline"],"k":[26,39]},"tram":{"a":"Tram","b":"1F68A","j":["transportation","vehicle"],"k":[34,18]},"flag-ec":{"a":"Ecuador Flag","b":"1F1EA-1F1E8","k":[1,45]},"twisted_rightwards_arrows":{"a":"Twisted Rightwards Arrows","b":"1F500","j":["blue-square","shuffle","music","random"],"k":[27,2]},"flag-ee":{"a":"Estonia Flag","b":"1F1EA-1F1EA","k":[1,46]},"cookie":{"a":"Cookie","b":"1F36A","j":["food","snack","oreo","chocolate","sweet","dessert"],"k":[7,44]},"monorail":{"a":"Monorail","b":"1F69D","j":["transportation","vehicle"],"k":[34,37]},"tropical_fish":{"a":"Tropical Fish","b":"1F420","j":["animal","swim","ocean","beach","nemo"],"k":[13,10]},"rolled_up_newspaper":{"a":"Rolled Up Newspaper","b":"1F5DE-FE0F","c":"1F5DE","k":[30,12],"o":7},"nauseated_face":{"a":"Nauseated Face","b":"1F922","j":["face","vomit","gross","green","sick","throw up","ill"],"k":[38,25],"o":9},"repeat":{"a":"Clockwise Rightwards and Leftwards Open Circle Arrows","b":"1F501","j":["loop","record"],"k":[27,3]},"bookmark_tabs":{"a":"Bookmark Tabs","b":"1F4D1","j":["favorite","save","order","tidy"],"k":[26,8]},"repeat_one":{"a":"Clockwise Rightwards and Leftwards Open Circle Arrows with Circled One Overlay","b":"1F502","j":["blue-square","loop"],"k":[27,4]},"flag-eg":{"a":"Egypt Flag","b":"1F1EA-1F1EC","k":[1,47]},"mountain_railway":{"a":"Mountain Railway","b":"1F69E","j":["transportation","vehicle"],"k":[34,38]},"birthday":{"a":"Birthday Cake","b":"1F382","j":["food","dessert","cake"],"k":[8,16]},"blowfish":{"a":"Blowfish","b":"1F421","j":["animal","nature","food","sea","ocean"],"k":[13,11]},"face_vomiting":{"a":"Face with Open Mouth Vomiting","b":"1F92E","k":[39,2],"n":["face_with_open_mouth_vomiting"],"o":10},"arrow_forward":{"a":"Black Right-Pointing Triangle","b":"25B6-FE0F","c":"25B6","j":["blue-square","right","direction","play"],"k":[47,10],"o":1},"bookmark":{"a":"Bookmark","b":"1F516","j":["favorite","label","save"],"k":[27,24]},"shark":{"a":"Shark","b":"1F988","j":["animal","nature","fish","sea","ocean","jaws","fins","beach"],"k":[42,32],"o":9},"train":{"a":"Tram Car","b":"1F68B","j":["transportation","vehicle","carriage","public","travel"],"k":[34,19]},"sneezing_face":{"a":"Sneezing Face","b":"1F927","j":["face","gesundheit","sneeze","sick","allergy"],"k":[38,47],"o":9},"cake":{"a":"Shortcake","b":"1F370","j":["food","dessert"],"k":[7,50]},"bus":{"a":"Bus","b":"1F68C","j":["car","vehicle","transportation"],"k":[34,20]},"pie":{"a":"Pie","b":"1F967","k":[42,19],"o":10},"innocent":{"a":"Smiling Face with Halo","b":"1F607","j":["face","angel","heaven","halo"],"k":[30,31]},"fast_forward":{"a":"Black Right-Pointing Double Triangle","b":"23E9","j":["blue-square","play","speed","continue"],"k":[46,45]},"label":{"a":"Label","b":"1F3F7-FE0F","c":"1F3F7","j":["sale","tag"],"k":[12,21],"o":7},"octopus":{"a":"Octopus","b":"1F419","j":["animal","creature","ocean","sea","nature","beach"],"k":[13,3]},"flag-er":{"a":"Eritrea Flag","b":"1F1EA-1F1F7","k":[1,49]},"black_right_pointing_double_triangle_with_vertical_bar":{"a":"Black Right Pointing Double Triangle with Vertical Bar","b":"23ED-FE0F","c":"23ED","k":[46,49]},"chocolate_bar":{"a":"Chocolate Bar","b":"1F36B","j":["food","snack","dessert","sweet"],"k":[7,45]},"oncoming_bus":{"a":"Oncoming Bus","b":"1F68D","j":["vehicle","transportation"],"k":[34,21]},"shell":{"a":"Spiral Shell","b":"1F41A","j":["nature","sea","beach"],"k":[13,4]},"face_with_cowboy_hat":{"a":"Face with Cowboy Hat","b":"1F920","k":[38,23],"o":9},"moneybag":{"a":"Money Bag","b":"1F4B0","j":["dollar","payment","coins","sale"],"k":[25,27]},"es":{"a":"Spain Flag","b":"1F1EA-1F1F8","j":["spain","flag","nation","country","banner"],"k":[1,50],"n":["flag-es"]},"crab":{"a":"Crab","b":"1F980","j":["animal","crustacean"],"k":[42,24],"o":8},"yen":{"a":"Banknote with Yen Sign","b":"1F4B4","j":["money","sales","japanese","dollar","currency"],"k":[25,31]},"flag-et":{"a":"Ethiopia Flag","b":"1F1EA-1F1F9","k":[1,51]},"clown_face":{"a":"Clown Face","b":"1F921","j":["face"],"k":[38,24],"o":9},"black_right_pointing_triangle_with_double_vertical_bar":{"a":"Black Right Pointing Triangle with Double Vertical Bar","b":"23EF-FE0F","c":"23EF","k":[46,51]},"trolleybus":{"a":"Trolleybus","b":"1F68E","j":["bart","transportation","vehicle"],"k":[34,22]},"candy":{"a":"Candy","b":"1F36C","j":["snack","dessert","sweet","lolly"],"k":[7,46]},"lying_face":{"a":"Lying Face","b":"1F925","j":["face","lie","pinocchio"],"k":[38,28],"o":9},"arrow_backward":{"a":"Black Left-Pointing Triangle","b":"25C0-FE0F","c":"25C0","j":["blue-square","left","direction"],"k":[47,11],"o":1},"dollar":{"a":"Banknote with Dollar Sign","b":"1F4B5","j":["money","sales","bill","currency"],"k":[25,32]},"shrimp":{"a":"Shrimp","b":"1F990","j":["animal","ocean","nature","seafood"],"k":[42,40],"o":9},"minibus":{"a":"Minibus","b":"1F690","j":["vehicle","car","transportation"],"k":[34,24]},"flag-eu":{"a":"European Union Flag","b":"1F1EA-1F1FA","k":[2,0]},"lollipop":{"a":"Lollipop","b":"1F36D","j":["food","snack","candy","sweet"],"k":[7,47]},"squid":{"a":"Squid","b":"1F991","j":["animal","nature","ocean","sea"],"k":[42,41],"o":9},"euro":{"a":"Banknote with Euro Sign","b":"1F4B6","j":["money","sales","dollar","currency"],"k":[25,33]},"flag-fi":{"a":"Finland Flag","b":"1F1EB-1F1EE","k":[2,1]},"ambulance":{"a":"Ambulance","b":"1F691","j":["health","911","hospital"],"k":[34,25]},"custard":{"a":"Custard","b":"1F36E","j":["dessert","food"],"k":[7,48]},"shushing_face":{"a":"Face with Finger Covering Closed Lips","b":"1F92B","k":[38,51],"n":["face_with_finger_covering_closed_lips"],"o":10},"rewind":{"a":"Black Left-Pointing Double Triangle","b":"23EA","j":["play","blue-square"],"k":[46,46]},"black_left_pointing_double_triangle_with_vertical_bar":{"a":"Black Left Pointing Double Triangle with Vertical Bar","b":"23EE-FE0F","c":"23EE","k":[46,50]},"face_with_hand_over_mouth":{"a":"Smiling Face with Smiling Eyes and Hand Covering Mouth","b":"1F92D","k":[39,1],"n":["smiling_face_with_smiling_eyes_and_hand_covering_mouth"],"o":10},"flag-fj":{"a":"Fiji Flag","b":"1F1EB-1F1EF","k":[2,2]},"honey_pot":{"a":"Honey Pot","b":"1F36F","j":["bees","sweet","kitchen"],"k":[7,49]},"snail":{"a":"Snail","b":"1F40C","j":["slow","animal","shell"],"k":[12,42]},"pound":{"a":"Banknote with Pound Sign","b":"1F4B7","j":["british","sterling","money","sales","bills","uk","england","currency"],"k":[25,34]},"fire_engine":{"a":"Fire Engine","b":"1F692","j":["transportation","cars","vehicle"],"k":[34,26]},"baby_bottle":{"a":"Baby Bottle","b":"1F37C","j":["food","container","milk"],"k":[8,10]},"butterfly":{"a":"Butterfly","b":"1F98B","j":["animal","insect","nature","caterpillar"],"k":[42,35],"o":9},"money_with_wings":{"a":"Money with Wings","b":"1F4B8","j":["dollar","bills","payment","sale"],"k":[25,35]},"face_with_monocle":{"a":"Face with Monocle","b":"1F9D0","k":[42,49],"o":10},"police_car":{"a":"Police Car","b":"1F693","j":["vehicle","cars","transportation","law","legal","enforcement"],"k":[34,27]},"arrow_up_small":{"a":"Up-Pointing Small Red Triangle","b":"1F53C","j":["blue-square","triangle","direction","point","forward","top"],"k":[28,10]},"flag-fm":{"a":"Micronesia Flag","b":"1F1EB-1F1F2","k":[2,4]},"glass_of_milk":{"a":"Glass of Milk","b":"1F95B","k":[42,7],"o":9},"credit_card":{"a":"Credit Card","b":"1F4B3","j":["money","sales","dollar","bill","payment","shopping"],"k":[25,30]},"oncoming_police_car":{"a":"Oncoming Police Car","b":"1F694","j":["vehicle","law","legal","enforcement","911"],"k":[34,28]},"bug":{"a":"Bug","b":"1F41B","j":["animal","insect","nature","worm"],"k":[13,5]},"nerd_face":{"a":"Nerd Face","b":"1F913","j":["face","nerdy","geek","dork"],"k":[37,27],"o":8},"arrow_double_up":{"a":"Black Up-Pointing Double Triangle","b":"23EB","j":["blue-square","direction","top"],"k":[46,47]},"chart":{"a":"Chart with Upwards Trend and Yen Sign","b":"1F4B9","j":["green-square","graph","presentation","stats"],"k":[25,36]},"flag-fo":{"a":"Faroe Islands Flag","b":"1F1EB-1F1F4","k":[2,5]},"ant":{"a":"Ant","b":"1F41C","j":["animal","insect","nature","bug"],"k":[13,6]},"arrow_down_small":{"a":"Down-Pointing Small Red Triangle","b":"1F53D","j":["blue-square","direction","bottom"],"k":[28,11]},"smiling_imp":{"a":"Smiling Face with Horns","b":"1F608","j":["devil","horns"],"k":[30,32]},"taxi":{"a":"Taxi","b":"1F695","j":["uber","vehicle","cars","transportation"],"k":[34,29]},"coffee":{"a":"Hot Beverage","b":"2615","j":["beverage","caffeine","latte","espresso"],"k":[47,24],"o":4},"fr":{"a":"France Flag","b":"1F1EB-1F1F7","j":["banner","flag","nation","france","french","country"],"k":[2,6],"n":["flag-fr"]},"oncoming_taxi":{"a":"Oncoming Taxi","b":"1F696","j":["vehicle","cars","uber"],"k":[34,30]},"arrow_double_down":{"a":"Black Down-Pointing Double Triangle","b":"23EC","j":["blue-square","direction","bottom"],"k":[46,48]},"imp":{"a":"Imp","b":"1F47F","j":["devil","angry","horns"],"k":[22,51]},"currency_exchange":{"a":"Currency Exchange","b":"1F4B1","j":["money","sales","dollar","travel"],"k":[25,28]},"tea":{"a":"Teacup Without Handle","b":"1F375","j":["drink","bowl","breakfast","green","british"],"k":[8,3]},"bee":{"a":"Honeybee","b":"1F41D","k":[13,7],"n":["honeybee"]},"heavy_dollar_sign":{"a":"Heavy Dollar Sign","b":"1F4B2","j":["money","sales","payment","currency","buck"],"k":[25,29]},"car":{"a":"Automobile","b":"1F697","k":[34,31],"n":["red_car"]},"sake":{"a":"Sake Bottle and Cup","b":"1F376","j":["wine","drink","drunk","beverage","japanese","alcohol","booze"],"k":[8,4]},"flag-ga":{"a":"Gabon Flag","b":"1F1EC-1F1E6","k":[2,7]},"beetle":{"a":"Lady Beetle","b":"1F41E","j":["animal","insect","nature","ladybug"],"k":[13,8]},"japanese_ogre":{"a":"Japanese Ogre","b":"1F479","j":["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],"k":[22,40]},"double_vertical_bar":{"a":"Double Vertical Bar","b":"23F8-FE0F","c":"23F8","k":[47,4],"o":7},"champagne":{"a":"Bottle with Popping Cork","b":"1F37E","j":["drink","wine","bottle","celebration"],"k":[8,12],"o":8},"japanese_goblin":{"a":"Japanese Goblin","b":"1F47A","j":["red","evil","mask","monster","scary","creepy","japanese","goblin"],"k":[22,41]},"black_square_for_stop":{"a":"Black Square for Stop","b":"23F9-FE0F","c":"23F9","k":[47,5],"o":7},"oncoming_automobile":{"a":"Oncoming Automobile","b":"1F698","j":["car","vehicle","transportation"],"k":[34,32]},"email":{"a":"Envelope","b":"2709-FE0F","c":"2709","j":["letter","postal","inbox","communication"],"k":[49,17],"n":["envelope"],"o":1},"cricket":{"a":"Cricket","b":"1F997","j":["sports"],"k":[42,47],"o":10},"gb":{"a":"United Kingdom Flag","b":"1F1EC-1F1E7","k":[2,8],"n":["uk","flag-gb"]},"black_circle_for_record":{"a":"Black Circle for Record","b":"23FA-FE0F","c":"23FA","k":[47,6],"o":7},"flag-gd":{"a":"Grenada Flag","b":"1F1EC-1F1E9","k":[2,9]},"spider":{"a":"Spider","b":"1F577-FE0F","c":"1F577","j":["animal","arachnid"],"k":[29,18],"o":7},"blue_car":{"a":"Recreational Vehicle","b":"1F699","j":["transportation","vehicle"],"k":[34,33]},"skull":{"a":"Skull","b":"1F480","j":["dead","skeleton","creepy","death"],"k":[23,0]},"e-mail":{"a":"E-Mail Symbol","b":"1F4E7","j":["communication","inbox"],"k":[26,30]},"wine_glass":{"a":"Wine Glass","b":"1F377","j":["drink","beverage","drunk","alcohol","booze"],"k":[8,5]},"spider_web":{"a":"Spider Web","b":"1F578-FE0F","c":"1F578","j":["animal","insect","arachnid","silk"],"k":[29,19],"o":7},"cocktail":{"a":"Cocktail Glass","b":"1F378","j":["drink","drunk","alcohol","beverage","booze","mojito"],"k":[8,6]},"skull_and_crossbones":{"a":"Skull and Crossbones","b":"2620-FE0F","c":"2620","j":["poison","danger","deadly","scary","death","pirate","evil"],"k":[47,32],"o":1},"flag-ge":{"a":"Georgia Flag","b":"1F1EC-1F1EA","k":[2,10]},"eject":{"a":"Eject","b":"23CF-FE0F","c":"23CF","k":[46,44],"o":4},"truck":{"a":"Delivery Truck","b":"1F69A","j":["cars","transportation"],"k":[34,34]},"incoming_envelope":{"a":"Incoming Envelope","b":"1F4E8","j":["email","inbox"],"k":[26,31]},"tropical_drink":{"a":"Tropical Drink","b":"1F379","j":["beverage","cocktail","summer","beach","alcohol","booze","mojito"],"k":[8,7]},"scorpion":{"a":"Scorpion","b":"1F982","j":["animal","arachnid"],"k":[42,26],"o":8},"cinema":{"a":"Cinema","b":"1F3A6","j":["blue-square","record","film","movie","curtain","stage","theater"],"k":[9,0]},"articulated_lorry":{"a":"Articulated Lorry","b":"1F69B","j":["vehicle","cars","transportation","express"],"k":[34,35]},"envelope_with_arrow":{"a":"Envelope with Downwards Arrow Above","b":"1F4E9","j":["email","communication"],"k":[26,32]},"ghost":{"a":"Ghost","b":"1F47B","j":["halloween","spooky","scary"],"k":[22,42]},"bouquet":{"a":"Bouquet","b":"1F490","j":["flowers","nature","spring"],"k":[24,42]},"tractor":{"a":"Tractor","b":"1F69C","j":["vehicle","car","farming","agriculture"],"k":[34,36]},"beer":{"a":"Beer Mug","b":"1F37A","j":["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],"k":[8,8]},"outbox_tray":{"a":"Outbox Tray","b":"1F4E4","j":["inbox","email"],"k":[26,27]},"low_brightness":{"a":"Low Brightness Symbol","b":"1F505","j":["sun","afternoon","warm","summer"],"k":[27,7]},"alien":{"a":"Extraterrestrial Alien","b":"1F47D","j":["UFO","paul","weird","outer_space"],"k":[22,49]},"flag-gg":{"a":"Guernsey Flag","b":"1F1EC-1F1EC","k":[2,12]},"cherry_blossom":{"a":"Cherry Blossom","b":"1F338","j":["nature","plant","spring","flower"],"k":[6,46]},"inbox_tray":{"a":"Inbox Tray","b":"1F4E5","j":["email","documents"],"k":[26,28]},"flag-gh":{"a":"Ghana Flag","b":"1F1EC-1F1ED","k":[2,13]},"bike":{"a":"Bicycle","b":"1F6B2","j":["sports","bicycle","exercise","hipster"],"k":[35,23]},"space_invader":{"a":"Alien Monster","b":"1F47E","j":["game","arcade","play"],"k":[22,50]},"beers":{"a":"Clinking Beer Mugs","b":"1F37B","j":["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],"k":[8,9]},"high_brightness":{"a":"High Brightness Symbol","b":"1F506","j":["sun","light"],"k":[27,8]},"package":{"a":"Package","b":"1F4E6","j":["mail","gift","cardboard","box","moving"],"k":[26,29]},"scooter":{"a":"Scooter","b":"1F6F4","k":[37,19],"o":9},"white_flower":{"a":"White Flower","b":"1F4AE","j":["japanese","spring"],"k":[25,25]},"clinking_glasses":{"a":"Clinking Glasses","b":"1F942","j":["beverage","drink","party","alcohol","celebrate","cheers"],"k":[41,38],"o":9},"robot_face":{"a":"Robot Face","b":"1F916","k":[37,30],"o":8},"signal_strength":{"a":"Antenna with Bars","b":"1F4F6","j":["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],"k":[26,45]},"flag-gi":{"a":"Gibraltar Flag","b":"1F1EC-1F1EE","k":[2,14]},"flag-gl":{"a":"Greenland Flag","b":"1F1EC-1F1F1","k":[2,15]},"motor_scooter":{"a":"Motor Scooter","b":"1F6F5","j":["vehicle","vespa","sasha"],"k":[37,20],"o":9},"mailbox":{"a":"Closed Mailbox with Raised Flag","b":"1F4EB","j":["email","inbox","communication"],"k":[26,34]},"vibration_mode":{"a":"Vibration Mode","b":"1F4F3","j":["orange-square","phone"],"k":[26,42]},"hankey":{"a":"Pile of Poo","b":"1F4A9","k":[25,15],"n":["poop","shit"]},"rosette":{"a":"Rosette","b":"1F3F5-FE0F","c":"1F3F5","j":["flower","decoration","military"],"k":[12,20],"o":7},"tumbler_glass":{"a":"Tumbler Glass","b":"1F943","j":["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],"k":[41,39],"o":9},"cup_with_straw":{"a":"Cup with Straw","b":"1F964","k":[42,16],"o":10},"flag-gm":{"a":"Gambia Flag","b":"1F1EC-1F1F2","k":[2,16]},"mailbox_closed":{"a":"Closed Mailbox with Lowered Flag","b":"1F4EA","j":["email","communication","inbox"],"k":[26,33]},"mobile_phone_off":{"a":"Mobile Phone off","b":"1F4F4","j":["mute","orange-square","silence","quiet"],"k":[26,43]},"busstop":{"a":"Bus Stop","b":"1F68F","j":["transportation","wait"],"k":[34,23]},"smiley_cat":{"a":"Smiling Cat Face with Open Mouth","b":"1F63A","j":["animal","cats","happy","smile"],"k":[31,30]},"rose":{"a":"Rose","b":"1F339","j":["flowers","valentines","love","spring"],"k":[6,47]},"motorway":{"a":"Motorway","b":"1F6E3-FE0F","c":"1F6E3","j":["road","cupertino","interstate","highway"],"k":[37,11],"o":7},"smile_cat":{"a":"Grinning Cat Face with Smiling Eyes","b":"1F638","j":["animal","cats","smile"],"k":[31,28]},"flag-gn":{"a":"Guinea Flag","b":"1F1EC-1F1F3","k":[2,17]},"wilted_flower":{"a":"Wilted Flower","b":"1F940","j":["plant","nature","flower"],"k":[41,36],"o":9},"mailbox_with_mail":{"a":"Open Mailbox with Raised Flag","b":"1F4EC","j":["email","inbox","communication"],"k":[26,35]},"chopsticks":{"a":"Chopsticks","b":"1F962","k":[42,14],"o":10},"female_sign":{"a":"Female Sign","b":"2640-FE0F","c":"2640","k":[47,42],"o":1},"mailbox_with_no_mail":{"a":"Open Mailbox with Lowered Flag","b":"1F4ED","j":["email","inbox"],"k":[26,36]},"knife_fork_plate":{"a":"Knife Fork Plate","b":"1F37D-FE0F","c":"1F37D","k":[8,11],"o":7},"hibiscus":{"a":"Hibiscus","b":"1F33A","j":["plant","vegetable","flowers","beach"],"k":[6,48]},"railway_track":{"a":"Railway Track","b":"1F6E4-FE0F","c":"1F6E4","j":["train","transportation"],"k":[37,12],"o":7},"male_sign":{"a":"Male Sign","b":"2642-FE0F","c":"2642","k":[47,43],"o":1},"joy_cat":{"a":"Cat Face with Tears of Joy","b":"1F639","j":["animal","cats","haha","happy","tears"],"k":[31,29]},"fuelpump":{"a":"Fuel Pump","b":"26FD","j":["gas station","petroleum"],"k":[49,13],"o":5},"sunflower":{"a":"Sunflower","b":"1F33B","j":["nature","plant","fall"],"k":[6,49]},"postbox":{"a":"Postbox","b":"1F4EE","j":["email","letter","envelope"],"k":[26,37]},"flag-gq":{"a":"Equatorial Guinea Flag","b":"1F1EC-1F1F6","k":[2,19]},"heart_eyes_cat":{"a":"Smiling Cat Face with Heart-Shaped Eyes","b":"1F63B","j":["animal","love","like","affection","cats","valentines","heart"],"k":[31,31]},"fork_and_knife":{"a":"Fork and Knife","b":"1F374","j":["cutlery","kitchen"],"k":[8,2]},"medical_symbol":{"a":"Medical Symbol","b":"2695-FE0F","c":"2695","k":[48,14],"n":["staff_of_aesculapius"],"o":4},"recycle":{"a":"Black Universal Recycling Symbol","b":"267B-FE0F","c":"267B","j":["arrow","environment","garbage","trash"],"k":[48,9],"o":3},"spoon":{"a":"Spoon","b":"1F944","j":["cutlery","kitchen","tableware"],"k":[41,40],"o":9},"blossom":{"a":"Blossom","b":"1F33C","j":["nature","flowers","yellow"],"k":[6,50]},"rotating_light":{"a":"Police Cars Revolving Light","b":"1F6A8","j":["police","ambulance","911","emergency","alert","error","pinged","law","legal"],"k":[35,13]},"smirk_cat":{"a":"Cat Face with Wry Smile","b":"1F63C","j":["animal","cats","smirk"],"k":[31,32]},"ballot_box_with_ballot":{"a":"Ballot Box with Ballot","b":"1F5F3-FE0F","c":"1F5F3","k":[30,17],"o":7},"flag-gr":{"a":"Greece Flag","b":"1F1EC-1F1F7","k":[2,20]},"kissing_cat":{"a":"Kissing Cat Face with Closed Eyes","b":"1F63D","j":["animal","cats","kiss"],"k":[31,33]},"pencil2":{"a":"Pencil","b":"270F-FE0F","c":"270F","j":["stationery","write","paper","writing","school","study"],"k":[49,42],"o":1},"traffic_light":{"a":"Horizontal Traffic Light","b":"1F6A5","j":["transportation","signal"],"k":[35,10]},"fleur_de_lis":{"a":"Fleur De Lis","b":"269C-FE0F","c":"269C","j":["decorative","scout"],"k":[48,19],"o":4},"tulip":{"a":"Tulip","b":"1F337","j":["flowers","plant","nature","summer","spring"],"k":[6,45]},"hocho":{"a":"Hocho","b":"1F52A","j":["knife","blade","cutlery","kitchen","weapon"],"k":[27,44],"n":["knife"]},"seedling":{"a":"Seedling","b":"1F331","j":["plant","nature","grass","lawn","spring"],"k":[6,39]},"amphora":{"a":"Amphora","b":"1F3FA","j":["vase","jar"],"k":[12,24],"o":8},"scream_cat":{"a":"Weary Cat Face","b":"1F640","j":["animal","cats","munch","scared","scream"],"k":[31,36]},"vertical_traffic_light":{"a":"Vertical Traffic Light","b":"1F6A6","j":["transportation","driving"],"k":[35,11]},"black_nib":{"a":"Black Nib","b":"2712-FE0F","c":"2712","j":["pen","stationery","writing","write"],"k":[49,43],"o":1},"flag-gt":{"a":"Guatemala Flag","b":"1F1EC-1F1F9","k":[2,22]},"trident":{"a":"Trident Emblem","b":"1F531","j":["weapon","spear"],"k":[27,51]},"flag-gu":{"a":"Guam Flag","b":"1F1EC-1F1FA","k":[2,23]},"name_badge":{"a":"Name Badge","b":"1F4DB","j":["fire","forbid"],"k":[26,18]},"construction":{"a":"Construction Sign","b":"1F6A7","j":["wip","progress","caution","warning"],"k":[35,12]},"lower_left_fountain_pen":{"a":"Lower Left Fountain Pen","b":"1F58B-FE0F","c":"1F58B","k":[29,29],"o":7},"evergreen_tree":{"a":"Evergreen Tree","b":"1F332","j":["plant","nature"],"k":[6,40]},"crying_cat_face":{"a":"Crying Cat Face","b":"1F63F","j":["animal","tears","weep","sad","cats","upset","cry"],"k":[31,35]},"flag-gw":{"a":"Guinea-Bissau Flag","b":"1F1EC-1F1FC","k":[2,24]},"lower_left_ballpoint_pen":{"a":"Lower Left Ballpoint Pen","b":"1F58A-FE0F","c":"1F58A","k":[29,28],"o":7},"pouting_cat":{"a":"Pouting Cat Face","b":"1F63E","j":["animal","cats"],"k":[31,34]},"deciduous_tree":{"a":"Deciduous Tree","b":"1F333","j":["plant","nature"],"k":[6,41]},"octagonal_sign":{"a":"Octagonal Sign","b":"1F6D1","k":[37,6],"o":9},"beginner":{"a":"Japanese Symbol for Beginner","b":"1F530","j":["badge","shield"],"k":[27,50]},"flag-gy":{"a":"Guyana Flag","b":"1F1EC-1F1FE","k":[2,25]},"lower_left_paintbrush":{"a":"Lower Left Paintbrush","b":"1F58C-FE0F","c":"1F58C","k":[29,30],"o":7},"o":{"a":"Heavy Large Circle","b":"2B55","j":["circle","round"],"k":[50,23],"o":5},"palm_tree":{"a":"Palm Tree","b":"1F334","j":["plant","vegetable","nature","summer","beach","mojito","tropical"],"k":[6,42]},"anchor":{"a":"Anchor","b":"2693","j":["ship","ferry","sea","boat"],"k":[48,12],"o":4},"see_no_evil":{"a":"See-No-Evil Monkey","b":"1F648","j":["monkey","animal","nature","haha"],"k":[32,43]},"boat":{"a":"Sailboat","b":"26F5","k":[48,43],"n":["sailboat"],"o":5},"white_check_mark":{"a":"White Heavy Check Mark","b":"2705","j":["green-square","ok","agree","vote","election","answer","tick"],"k":[49,15]},"flag-hk":{"a":"Hong Kong Sar China Flag","b":"1F1ED-1F1F0","k":[2,26]},"lower_left_crayon":{"a":"Lower Left Crayon","b":"1F58D-FE0F","c":"1F58D","k":[29,31],"o":7},"hear_no_evil":{"a":"Hear-No-Evil Monkey","b":"1F649","j":["animal","monkey","nature"],"k":[32,44]},"cactus":{"a":"Cactus","b":"1F335","j":["vegetable","plant","nature"],"k":[6,43]},"ear_of_rice":{"a":"Ear of Rice","b":"1F33E","j":["nature","plant"],"k":[7,0]},"speak_no_evil":{"a":"Speak-No-Evil Monkey","b":"1F64A","j":["monkey","animal","nature","omg"],"k":[32,45]},"flag-hm":{"a":"Heard & Mcdonald Islands Flag","b":"1F1ED-1F1F2","k":[2,27]},"ballot_box_with_check":{"a":"Ballot Box with Check","b":"2611-FE0F","c":"2611","j":["ok","agree","confirm","black-square","vote","election","yes","tick"],"k":[47,22],"o":1},"canoe":{"a":"Canoe","b":"1F6F6","j":["boat","paddle","water","ship"],"k":[37,21],"o":9},"memo":{"a":"Memo","b":"1F4DD","j":["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],"k":[26,20],"n":["pencil"]},"herb":{"a":"Herb","b":"1F33F","j":["vegetable","plant","medicine","weed","grass","lawn"],"k":[7,1]},"flag-hn":{"a":"Honduras Flag","b":"1F1ED-1F1F3","k":[2,28]},"heavy_check_mark":{"a":"Heavy Check Mark","b":"2714-FE0F","c":"2714","j":["ok","nike","answer","yes","tick"],"k":[49,44],"o":1},"briefcase":{"a":"Briefcase","b":"1F4BC","j":["business","documents","work","law","legal","job","career"],"k":[25,39]},"speedboat":{"a":"Speedboat","b":"1F6A4","j":["ship","transportation","vehicle","summer"],"k":[35,9]},"baby":{"skin_variations":{"1F3FB":{"unified":"1F476-1F3FB","non_qualified":null,"image":"1f476-1f3fb.png","sheet_x":22,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F476-1F3FC","non_qualified":null,"image":"1f476-1f3fc.png","sheet_x":22,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F476-1F3FD","non_qualified":null,"image":"1f476-1f3fd.png","sheet_x":22,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F476-1F3FE","non_qualified":null,"image":"1f476-1f3fe.png","sheet_x":22,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F476-1F3FF","non_qualified":null,"image":"1f476-1f3ff.png","sheet_x":22,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Baby","b":"1F476","j":["child","boy","girl","toddler"],"k":[22,10]},"heavy_multiplication_x":{"a":"Heavy Multiplication X","b":"2716-FE0F","c":"2716","j":["math","calculation"],"k":[49,45],"o":1},"child":{"skin_variations":{"1F3FB":{"unified":"1F9D2-1F3FB","non_qualified":null,"image":"1f9d2-1f3fb.png","sheet_x":43,"sheet_y":5,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F9D2-1F3FC","non_qualified":null,"image":"1f9d2-1f3fc.png","sheet_x":43,"sheet_y":6,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F9D2-1F3FD","non_qualified":null,"image":"1f9d2-1f3fd.png","sheet_x":43,"sheet_y":7,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F9D2-1F3FE","non_qualified":null,"image":"1f9d2-1f3fe.png","sheet_x":43,"sheet_y":8,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F9D2-1F3FF","non_qualified":null,"image":"1f9d2-1f3ff.png","sheet_x":43,"sheet_y":9,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Child","b":"1F9D2","k":[43,4],"o":10},"shamrock":{"a":"Shamrock","b":"2618-FE0F","c":"2618","j":["vegetable","plant","nature","irish","clover"],"k":[47,25],"o":4},"passenger_ship":{"a":"Passenger Ship","b":"1F6F3-FE0F","c":"1F6F3","j":["yacht","cruise","ferry"],"k":[37,18],"o":7},"flag-hr":{"a":"Croatia Flag","b":"1F1ED-1F1F7","k":[2,29]},"file_folder":{"a":"File Folder","b":"1F4C1","j":["documents","business","office"],"k":[25,44]},"x":{"a":"Cross Mark","b":"274C","j":["no","delete","remove","cancel"],"k":[50,1]},"four_leaf_clover":{"a":"Four Leaf Clover","b":"1F340","j":["vegetable","plant","nature","lucky","irish"],"k":[7,2]},"open_file_folder":{"a":"Open File Folder","b":"1F4C2","j":["documents","load"],"k":[25,45]},"boy":{"skin_variations":{"1F3FB":{"unified":"1F466-1F3FB","non_qualified":null,"image":"1f466-1f3fb.png","sheet_x":15,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F466-1F3FC","non_qualified":null,"image":"1f466-1f3fc.png","sheet_x":15,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F466-1F3FD","non_qualified":null,"image":"1f466-1f3fd.png","sheet_x":15,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F466-1F3FE","non_qualified":null,"image":"1f466-1f3fe.png","sheet_x":15,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F466-1F3FF","non_qualified":null,"image":"1f466-1f3ff.png","sheet_x":15,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Boy","b":"1F466","j":["man","male","guy","teenager"],"k":[15,42]},"ferry":{"a":"Ferry","b":"26F4-FE0F","c":"26F4","j":["boat","ship","yacht"],"k":[48,42],"o":5},"flag-ht":{"a":"Haiti Flag","b":"1F1ED-1F1F9","k":[2,30]},"girl":{"skin_variations":{"1F3FB":{"unified":"1F467-1F3FB","non_qualified":null,"image":"1f467-1f3fb.png","sheet_x":15,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F467-1F3FC","non_qualified":null,"image":"1f467-1f3fc.png","sheet_x":15,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F467-1F3FD","non_qualified":null,"image":"1f467-1f3fd.png","sheet_x":15,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F467-1F3FE","non_qualified":null,"image":"1f467-1f3fe.png","sheet_x":16,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F467-1F3FF","non_qualified":null,"image":"1f467-1f3ff.png","sheet_x":16,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Girl","b":"1F467","j":["female","woman","teenager"],"k":[15,48]},"negative_squared_cross_mark":{"a":"Negative Squared Cross Mark","b":"274E","j":["x","green-square","no","deny"],"k":[50,2]},"flag-hu":{"a":"Hungary Flag","b":"1F1ED-1F1FA","k":[2,31]},"card_index_dividers":{"a":"Card Index Dividers","b":"1F5C2-FE0F","c":"1F5C2","j":["organizing","business","stationery"],"k":[30,4],"o":7},"maple_leaf":{"a":"Maple Leaf","b":"1F341","j":["nature","plant","vegetable","ca","fall"],"k":[7,3]},"motor_boat":{"a":"Motor Boat","b":"1F6E5-FE0F","c":"1F6E5","j":["ship"],"k":[37,13],"o":7},"flag-ic":{"a":"Canary Islands Flag","b":"1F1EE-1F1E8","k":[2,32]},"fallen_leaf":{"a":"Fallen Leaf","b":"1F342","j":["nature","plant","vegetable","leaves"],"k":[7,4]},"adult":{"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fb.png","sheet_x":42,"sheet_y":51,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fc.png","sheet_x":43,"sheet_y":0,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fd.png","sheet_x":43,"sheet_y":1,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fe.png","sheet_x":43,"sheet_y":2,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3ff.png","sheet_x":43,"sheet_y":3,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Adult","b":"1F9D1","k":[42,50],"o":10},"ship":{"a":"Ship","b":"1F6A2","j":["transportation","titanic","deploy"],"k":[34,42]},"heavy_plus_sign":{"a":"Heavy Plus Sign","b":"2795","j":["math","calculation","addition","more","increase"],"k":[50,9]},"date":{"a":"Calendar","b":"1F4C5","j":["calendar","schedule"],"k":[25,48]},"man":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fb.png","sheet_x":18,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fc.png","sheet_x":18,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fd.png","sheet_x":18,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fe.png","sheet_x":18,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F468-1F3FF","non_qualified":null,"image":"1f468-1f3ff.png","sheet_x":18,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Man","b":"1F468","j":["mustache","father","dad","guy","classy","sir","moustache"],"k":[18,11]},"flag-id":{"a":"Indonesia Flag","b":"1F1EE-1F1E9","k":[2,33]},"leaves":{"a":"Leaf Fluttering in Wind","b":"1F343","j":["nature","plant","tree","vegetable","grass","lawn","spring"],"k":[7,5]},"heavy_minus_sign":{"a":"Heavy Minus Sign","b":"2796","j":["math","calculation","subtract","less"],"k":[50,10]},"calendar":{"a":"Tear-off Calendar","b":"1F4C6","j":["schedule","date","planning"],"k":[25,49]},"airplane":{"a":"Airplane","b":"2708-FE0F","c":"2708","j":["vehicle","transportation","flight","fly"],"k":[49,16],"o":1},"spiral_note_pad":{"a":"Spiral Note Pad","b":"1F5D2-FE0F","c":"1F5D2","k":[30,8],"o":7},"heavy_division_sign":{"a":"Heavy Division Sign","b":"2797","j":["divide","math","calculation"],"k":[50,11]},"small_airplane":{"a":"Small Airplane","b":"1F6E9-FE0F","c":"1F6E9","j":["flight","transportation","fly","vehicle"],"k":[37,14],"o":7},"woman":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fb.png","sheet_x":20,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fc.png","sheet_x":20,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fd.png","sheet_x":20,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fe.png","sheet_x":20,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F469-1F3FF","non_qualified":null,"image":"1f469-1f3ff.png","sheet_x":20,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Woman","b":"1F469","j":["female","girls","lady"],"k":[20,23]},"flag-ie":{"a":"Ireland Flag","b":"1F1EE-1F1EA","k":[2,34]},"curly_loop":{"a":"Curly Loop","b":"27B0","j":["scribble","draw","shape","squiggle"],"k":[50,13]},"flag-il":{"a":"Israel Flag","b":"1F1EE-1F1F1","k":[2,35]},"airplane_departure":{"a":"Airplane Departure","b":"1F6EB","k":[37,15],"o":7},"spiral_calendar_pad":{"a":"Spiral Calendar Pad","b":"1F5D3-FE0F","c":"1F5D3","k":[30,9],"o":7},"older_adult":{"skin_variations":{"1F3FB":{"unified":"1F9D3-1F3FB","non_qualified":null,"image":"1f9d3-1f3fb.png","sheet_x":43,"sheet_y":11,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F9D3-1F3FC","non_qualified":null,"image":"1f9d3-1f3fc.png","sheet_x":43,"sheet_y":12,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F9D3-1F3FD","non_qualified":null,"image":"1f9d3-1f3fd.png","sheet_x":43,"sheet_y":13,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F9D3-1F3FE","non_qualified":null,"image":"1f9d3-1f3fe.png","sheet_x":43,"sheet_y":14,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F9D3-1F3FF","non_qualified":null,"image":"1f9d3-1f3ff.png","sheet_x":43,"sheet_y":15,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Older Adult","b":"1F9D3","k":[43,10],"o":10},"airplane_arriving":{"a":"Airplane Arriving","b":"1F6EC","k":[37,16],"o":7},"card_index":{"a":"Card Index","b":"1F4C7","j":["business","stationery"],"k":[25,50]},"loop":{"a":"Double Curly Loop","b":"27BF","j":["tape","cassette"],"k":[50,14]},"older_man":{"skin_variations":{"1F3FB":{"unified":"1F474-1F3FB","non_qualified":null,"image":"1f474-1f3fb.png","sheet_x":21,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F474-1F3FC","non_qualified":null,"image":"1f474-1f3fc.png","sheet_x":22,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F474-1F3FD","non_qualified":null,"image":"1f474-1f3fd.png","sheet_x":22,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F474-1F3FE","non_qualified":null,"image":"1f474-1f3fe.png","sheet_x":22,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F474-1F3FF","non_qualified":null,"image":"1f474-1f3ff.png","sheet_x":22,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Older Man","b":"1F474","j":["human","male","men","old","elder","senior"],"k":[21,50]},"flag-im":{"a":"Isle of Man Flag","b":"1F1EE-1F1F2","k":[2,36]},"flag-in":{"a":"India Flag","b":"1F1EE-1F1F3","k":[2,37]},"chart_with_upwards_trend":{"a":"Chart with Upwards Trend","b":"1F4C8","j":["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],"k":[25,51]},"part_alternation_mark":{"a":"Part Alternation Mark","b":"303D-FE0F","c":"303D","j":["graph","presentation","stats","business","economics","bad"],"k":[50,25],"o":3},"seat":{"a":"Seat","b":"1F4BA","j":["sit","airplane","transport","bus","flight","fly"],"k":[25,37]},"older_woman":{"skin_variations":{"1F3FB":{"unified":"1F475-1F3FB","non_qualified":null,"image":"1f475-1f3fb.png","sheet_x":22,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F475-1F3FC","non_qualified":null,"image":"1f475-1f3fc.png","sheet_x":22,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F475-1F3FD","non_qualified":null,"image":"1f475-1f3fd.png","sheet_x":22,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F475-1F3FE","non_qualified":null,"image":"1f475-1f3fe.png","sheet_x":22,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F475-1F3FF","non_qualified":null,"image":"1f475-1f3ff.png","sheet_x":22,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Older Woman","b":"1F475","j":["human","female","women","lady","old","elder","senior"],"k":[22,4]},"eight_spoked_asterisk":{"a":"Eight Spoked Asterisk","b":"2733-FE0F","c":"2733","j":["star","sparkle","green-square"],"k":[49,49],"o":1},"chart_with_downwards_trend":{"a":"Chart with Downwards Trend","b":"1F4C9","j":["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],"k":[26,0]},"flag-io":{"a":"British Indian Ocean Territory Flag","b":"1F1EE-1F1F4","k":[2,38]},"male-doctor":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2695-FE0F","non_qualified":"1F468-1F3FB-200D-2695","image":"1f468-1f3fb-200d-2695-fe0f.png","sheet_x":17,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-2695-FE0F","non_qualified":"1F468-1F3FC-200D-2695","image":"1f468-1f3fc-200d-2695-fe0f.png","sheet_x":17,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-2695-FE0F","non_qualified":"1F468-1F3FD-200D-2695","image":"1f468-1f3fd-200d-2695-fe0f.png","sheet_x":17,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-2695-FE0F","non_qualified":"1F468-1F3FE-200D-2695","image":"1f468-1f3fe-200d-2695-fe0f.png","sheet_x":17,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-2695-FE0F","non_qualified":"1F468-1F3FF-200D-2695","image":"1f468-1f3ff-200d-2695-fe0f.png","sheet_x":17,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Male Doctor","b":"1F468-200D-2695-FE0F","c":"1F468-200D-2695","k":[17,43]},"helicopter":{"a":"Helicopter","b":"1F681","j":["transportation","vehicle","fly"],"k":[34,9]},"female-doctor":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2695-FE0F","non_qualified":"1F469-1F3FB-200D-2695","image":"1f469-1f3fb-200d-2695-fe0f.png","sheet_x":20,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-2695-FE0F","non_qualified":"1F469-1F3FC-200D-2695","image":"1f469-1f3fc-200d-2695-fe0f.png","sheet_x":20,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-2695-FE0F","non_qualified":"1F469-1F3FD-200D-2695","image":"1f469-1f3fd-200d-2695-fe0f.png","sheet_x":20,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-2695-FE0F","non_qualified":"1F469-1F3FE-200D-2695","image":"1f469-1f3fe-200d-2695-fe0f.png","sheet_x":20,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-2695-FE0F","non_qualified":"1F469-1F3FF-200D-2695","image":"1f469-1f3ff-200d-2695-fe0f.png","sheet_x":20,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Doctor","b":"1F469-200D-2695-FE0F","c":"1F469-200D-2695","k":[20,1]},"suspension_railway":{"a":"Suspension Railway","b":"1F69F","j":["vehicle","transportation"],"k":[34,39]},"bar_chart":{"a":"Bar Chart","b":"1F4CA","j":["graph","presentation","stats"],"k":[26,1]},"flag-iq":{"a":"Iraq Flag","b":"1F1EE-1F1F6","k":[2,39]},"eight_pointed_black_star":{"a":"Eight Pointed Black Star","b":"2734-FE0F","c":"2734","j":["orange-square","shape","polygon"],"k":[49,50],"o":1},"mountain_cableway":{"a":"Mountain Cableway","b":"1F6A0","j":["transportation","vehicle","ski"],"k":[34,40]},"male-student":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F393","non_qualified":null,"image":"1f468-1f3fb-200d-1f393.png","sheet_x":16,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F393","non_qualified":null,"image":"1f468-1f3fc-200d-1f393.png","sheet_x":16,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F393","non_qualified":null,"image":"1f468-1f3fd-200d-1f393.png","sheet_x":16,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F393","non_qualified":null,"image":"1f468-1f3fe-200d-1f393.png","sheet_x":16,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F393","non_qualified":null,"image":"1f468-1f3ff-200d-1f393.png","sheet_x":16,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Student","b":"1F468-200D-1F393","k":[16,14]},"clipboard":{"a":"Clipboard","b":"1F4CB","j":["stationery","documents"],"k":[26,2]},"flag-ir":{"a":"Iran Flag","b":"1F1EE-1F1F7","k":[2,40]},"sparkle":{"a":"Sparkle","b":"2747-FE0F","c":"2747","j":["stars","green-square","awesome","good","fireworks"],"k":[50,0],"o":1},"female-student":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F393","non_qualified":null,"image":"1f469-1f3fb-200d-1f393.png","sheet_x":18,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F393","non_qualified":null,"image":"1f469-1f3fc-200d-1f393.png","sheet_x":18,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F393","non_qualified":null,"image":"1f469-1f3fd-200d-1f393.png","sheet_x":18,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F393","non_qualified":null,"image":"1f469-1f3fe-200d-1f393.png","sheet_x":18,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F393","non_qualified":null,"image":"1f469-1f3ff-200d-1f393.png","sheet_x":18,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Student","b":"1F469-200D-1F393","k":[18,29]},"pushpin":{"a":"Pushpin","b":"1F4CC","j":["stationery","mark","here"],"k":[26,3]},"aerial_tramway":{"a":"Aerial Tramway","b":"1F6A1","j":["transportation","vehicle","ski"],"k":[34,41]},"flag-is":{"a":"Iceland Flag","b":"1F1EE-1F1F8","k":[2,41]},"bangbang":{"a":"Double Exclamation Mark","b":"203C-FE0F","c":"203C","j":["exclamation","surprise"],"k":[46,29],"o":1},"interrobang":{"a":"Exclamation Question Mark","b":"2049-FE0F","c":"2049","j":["wat","punctuation","surprise"],"k":[46,30],"o":3},"satellite":{"a":"Satellite","b":"1F6F0-FE0F","c":"1F6F0","j":["communication","future","radio","space"],"k":[37,17],"o":7},"it":{"a":"Italy Flag","b":"1F1EE-1F1F9","j":["italy","flag","nation","country","banner"],"k":[2,42],"n":["flag-it"]},"male-teacher":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fb-200d-1f3eb.png","sheet_x":16,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fc-200d-1f3eb.png","sheet_x":16,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fd-200d-1f3eb.png","sheet_x":16,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fe-200d-1f3eb.png","sheet_x":16,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f468-1f3ff-200d-1f3eb.png","sheet_x":16,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Teacher","b":"1F468-200D-1F3EB","k":[16,32]},"round_pushpin":{"a":"Round Pushpin","b":"1F4CD","j":["stationery","location","map","here"],"k":[26,4]},"flag-je":{"a":"Jersey Flag","b":"1F1EF-1F1EA","k":[2,43]},"question":{"a":"Black Question Mark Ornament","b":"2753","j":["doubt","confused"],"k":[50,3]},"rocket":{"a":"Rocket","b":"1F680","j":["launch","ship","staffmode","NASA","outer space","outer_space","fly"],"k":[34,8]},"female-teacher":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fb-200d-1f3eb.png","sheet_x":18,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fc-200d-1f3eb.png","sheet_x":18,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fd-200d-1f3eb.png","sheet_x":18,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fe-200d-1f3eb.png","sheet_x":18,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f469-1f3ff-200d-1f3eb.png","sheet_x":19,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Teacher","b":"1F469-200D-1F3EB","k":[18,47]},"paperclip":{"a":"Paperclip","b":"1F4CE","j":["documents","stationery"],"k":[26,5]},"linked_paperclips":{"a":"Linked Paperclips","b":"1F587-FE0F","c":"1F587","k":[29,27],"o":7},"flying_saucer":{"a":"Flying Saucer","b":"1F6F8","k":[37,23],"o":10},"male-judge":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2696-FE0F","non_qualified":"1F468-1F3FB-200D-2696","image":"1f468-1f3fb-200d-2696-fe0f.png","sheet_x":17,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-2696-FE0F","non_qualified":"1F468-1F3FC-200D-2696","image":"1f468-1f3fc-200d-2696-fe0f.png","sheet_x":17,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-2696-FE0F","non_qualified":"1F468-1F3FD-200D-2696","image":"1f468-1f3fd-200d-2696-fe0f.png","sheet_x":18,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-2696-FE0F","non_qualified":"1F468-1F3FE-200D-2696","image":"1f468-1f3fe-200d-2696-fe0f.png","sheet_x":18,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-2696-FE0F","non_qualified":"1F468-1F3FF-200D-2696","image":"1f468-1f3ff-200d-2696-fe0f.png","sheet_x":18,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Male Judge","b":"1F468-200D-2696-FE0F","c":"1F468-200D-2696","k":[17,49]},"grey_question":{"a":"White Question Mark Ornament","b":"2754","j":["doubts","gray","huh","confused"],"k":[50,4]},"flag-jm":{"a":"Jamaica Flag","b":"1F1EF-1F1F2","k":[2,44]},"bellhop_bell":{"a":"Bellhop Bell","b":"1F6CE-FE0F","c":"1F6CE","j":["service"],"k":[37,3],"o":7},"straight_ruler":{"a":"Straight Ruler","b":"1F4CF","j":["stationery","calculate","length","math","school","drawing","architect","sketch"],"k":[26,6]},"flag-jo":{"a":"Jordan Flag","b":"1F1EF-1F1F4","k":[2,45]},"female-judge":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2696-FE0F","non_qualified":"1F469-1F3FB-200D-2696","image":"1f469-1f3fb-200d-2696-fe0f.png","sheet_x":20,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-2696-FE0F","non_qualified":"1F469-1F3FC-200D-2696","image":"1f469-1f3fc-200d-2696-fe0f.png","sheet_x":20,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-2696-FE0F","non_qualified":"1F469-1F3FD-200D-2696","image":"1f469-1f3fd-200d-2696-fe0f.png","sheet_x":20,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-2696-FE0F","non_qualified":"1F469-1F3FE-200D-2696","image":"1f469-1f3fe-200d-2696-fe0f.png","sheet_x":20,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-2696-FE0F","non_qualified":"1F469-1F3FF-200D-2696","image":"1f469-1f3ff-200d-2696-fe0f.png","sheet_x":20,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Judge","b":"1F469-200D-2696-FE0F","c":"1F469-200D-2696","k":[20,7]},"grey_exclamation":{"a":"White Exclamation Mark Ornament","b":"2755","j":["surprise","punctuation","gray","wow","warning"],"k":[50,5]},"door":{"a":"Door","b":"1F6AA","j":["house","entry","exit"],"k":[35,15]},"male-farmer":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F33E","non_qualified":null,"image":"1f468-1f3fb-200d-1f33e.png","sheet_x":16,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F33E","non_qualified":null,"image":"1f468-1f3fc-200d-1f33e.png","sheet_x":16,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F33E","non_qualified":null,"image":"1f468-1f3fd-200d-1f33e.png","sheet_x":16,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F33E","non_qualified":null,"image":"1f468-1f3fe-200d-1f33e.png","sheet_x":16,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F33E","non_qualified":null,"image":"1f468-1f3ff-200d-1f33e.png","sheet_x":16,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Farmer","b":"1F468-200D-1F33E","k":[16,2]},"jp":{"a":"Japan Flag","b":"1F1EF-1F1F5","j":["japanese","nation","flag","country","banner"],"k":[2,46],"n":["flag-jp"]},"triangular_ruler":{"a":"Triangular Ruler","b":"1F4D0","j":["stationery","math","architect","sketch"],"k":[26,7]},"exclamation":{"a":"Heavy Exclamation Mark Symbol","b":"2757","j":["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],"k":[50,6],"n":["heavy_exclamation_mark"],"o":5},"bed":{"a":"Bed","b":"1F6CF-FE0F","c":"1F6CF","j":["sleep","rest"],"k":[37,4],"o":7},"female-farmer":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F33E","non_qualified":null,"image":"1f469-1f3fb-200d-1f33e.png","sheet_x":18,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F33E","non_qualified":null,"image":"1f469-1f3fc-200d-1f33e.png","sheet_x":18,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F33E","non_qualified":null,"image":"1f469-1f3fd-200d-1f33e.png","sheet_x":18,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F33E","non_qualified":null,"image":"1f469-1f3fe-200d-1f33e.png","sheet_x":18,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F33E","non_qualified":null,"image":"1f469-1f3ff-200d-1f33e.png","sheet_x":18,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Farmer","b":"1F469-200D-1F33E","k":[18,17]},"scissors":{"a":"Black Scissors","b":"2702-FE0F","c":"2702","j":["stationery","cut"],"k":[49,14],"o":1},"wavy_dash":{"a":"Wavy Dash","b":"3030-FE0F","c":"3030","j":["draw","line","moustache","mustache","squiggle","scribble"],"k":[50,24],"o":1},"flag-ke":{"a":"Kenya Flag","b":"1F1F0-1F1EA","k":[2,47]},"flag-kg":{"a":"Kyrgyzstan Flag","b":"1F1F0-1F1EC","k":[2,48]},"couch_and_lamp":{"a":"Couch and Lamp","b":"1F6CB-FE0F","c":"1F6CB","j":["read","chill"],"k":[36,47],"o":7},"male-cook":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F373","non_qualified":null,"image":"1f468-1f3fb-200d-1f373.png","sheet_x":16,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F373","non_qualified":null,"image":"1f468-1f3fc-200d-1f373.png","sheet_x":16,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F373","non_qualified":null,"image":"1f468-1f3fd-200d-1f373.png","sheet_x":16,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F373","non_qualified":null,"image":"1f468-1f3fe-200d-1f373.png","sheet_x":16,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F373","non_qualified":null,"image":"1f468-1f3ff-200d-1f373.png","sheet_x":16,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Cook","b":"1F468-200D-1F373","k":[16,8]},"card_file_box":{"a":"Card File Box","b":"1F5C3-FE0F","c":"1F5C3","j":["business","stationery"],"k":[30,5],"o":7},"copyright":{"a":"Copyright Sign","b":"00A9-FE0F","c":"00A9","j":["ip","license","circle","law","legal"],"k":[0,12],"o":1},"file_cabinet":{"a":"File Cabinet","b":"1F5C4-FE0F","c":"1F5C4","j":["filing","organizing"],"k":[30,6],"o":7},"registered":{"a":"Registered Sign","b":"00AE-FE0F","c":"00AE","j":["alphabet","circle"],"k":[0,13],"o":1},"flag-kh":{"a":"Cambodia Flag","b":"1F1F0-1F1ED","k":[2,49]},"female-cook":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F373","non_qualified":null,"image":"1f469-1f3fb-200d-1f373.png","sheet_x":18,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F373","non_qualified":null,"image":"1f469-1f3fc-200d-1f373.png","sheet_x":18,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F373","non_qualified":null,"image":"1f469-1f3fd-200d-1f373.png","sheet_x":18,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F373","non_qualified":null,"image":"1f469-1f3fe-200d-1f373.png","sheet_x":18,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F373","non_qualified":null,"image":"1f469-1f3ff-200d-1f373.png","sheet_x":18,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Cook","b":"1F469-200D-1F373","k":[18,23]},"toilet":{"a":"Toilet","b":"1F6BD","j":["restroom","wc","washroom","bathroom","potty"],"k":[36,33]},"wastebasket":{"a":"Wastebasket","b":"1F5D1-FE0F","c":"1F5D1","j":["bin","trash","rubbish","garbage","toss"],"k":[30,7],"o":7},"flag-ki":{"a":"Kiribati Flag","b":"1F1F0-1F1EE","k":[2,50]},"shower":{"a":"Shower","b":"1F6BF","j":["clean","water","bathroom"],"k":[36,35]},"male-mechanic":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F527","non_qualified":null,"image":"1f468-1f3fb-200d-1f527.png","sheet_x":17,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F527","non_qualified":null,"image":"1f468-1f3fc-200d-1f527.png","sheet_x":17,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F527","non_qualified":null,"image":"1f468-1f3fd-200d-1f527.png","sheet_x":17,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F527","non_qualified":null,"image":"1f468-1f3fe-200d-1f527.png","sheet_x":17,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F527","non_qualified":null,"image":"1f468-1f3ff-200d-1f527.png","sheet_x":17,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Mechanic","b":"1F468-200D-1F527","k":[17,19]},"tm":{"a":"Trade Mark Sign","b":"2122-FE0F","c":"2122","j":["trademark","brand","law","legal"],"k":[46,31],"o":1},"hash":{"a":"Hash Key","b":"0023-FE0F-20E3","c":"0023-20E3","j":["symbol","blue-square","twitter"],"k":[0,0],"o":3},"flag-km":{"a":"Comoros Flag","b":"1F1F0-1F1F2","k":[2,51]},"bathtub":{"a":"Bathtub","b":"1F6C1","j":["clean","shower","bathroom"],"k":[36,42]},"female-mechanic":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F527","non_qualified":null,"image":"1f469-1f3fb-200d-1f527.png","sheet_x":19,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F527","non_qualified":null,"image":"1f469-1f3fc-200d-1f527.png","sheet_x":19,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F527","non_qualified":null,"image":"1f469-1f3fd-200d-1f527.png","sheet_x":19,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F527","non_qualified":null,"image":"1f469-1f3fe-200d-1f527.png","sheet_x":19,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F527","non_qualified":null,"image":"1f469-1f3ff-200d-1f527.png","sheet_x":19,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Mechanic","b":"1F469-200D-1F527","k":[19,29]},"lock":{"a":"Lock","b":"1F512","j":["security","password","padlock"],"k":[27,20]},"male-factory-worker":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fb-200d-1f3ed.png","sheet_x":16,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fc-200d-1f3ed.png","sheet_x":16,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fd-200d-1f3ed.png","sheet_x":16,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fe-200d-1f3ed.png","sheet_x":16,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f468-1f3ff-200d-1f3ed.png","sheet_x":16,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Factory Worker","b":"1F468-200D-1F3ED","k":[16,38]},"flag-kn":{"a":"St. Kitts & Nevis Flag","b":"1F1F0-1F1F3","k":[3,0]},"hourglass":{"a":"Hourglass","b":"231B","j":["time","clock","oldschool","limit","exam","quiz","test"],"k":[46,42],"o":1},"keycap_star":{"a":"Keycap Star","b":"002A-FE0F-20E3","c":"002A-20E3","k":[0,1],"o":3},"unlock":{"a":"Open Lock","b":"1F513","j":["privacy","security"],"k":[27,21]},"flag-kp":{"a":"North Korea Flag","b":"1F1F0-1F1F5","k":[3,1]},"female-factory-worker":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fb-200d-1f3ed.png","sheet_x":19,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fc-200d-1f3ed.png","sheet_x":19,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fd-200d-1f3ed.png","sheet_x":19,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fe-200d-1f3ed.png","sheet_x":19,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f469-1f3ff-200d-1f3ed.png","sheet_x":19,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Factory Worker","b":"1F469-200D-1F3ED","k":[19,1]},"zero":{"a":"Keycap 0","b":"0030-FE0F-20E3","c":"0030-20E3","j":["0","numbers","blue-square","null"],"k":[0,2],"o":3},"lock_with_ink_pen":{"a":"Lock with Ink Pen","b":"1F50F","j":["security","secret"],"k":[27,17]},"hourglass_flowing_sand":{"a":"Hourglass with Flowing Sand","b":"23F3","j":["oldschool","time","countdown"],"k":[47,3]},"one":{"a":"Keycap 1","b":"0031-FE0F-20E3","c":"0031-20E3","j":["blue-square","numbers","1"],"k":[0,3],"o":3},"kr":{"a":"South Korea Flag","b":"1F1F0-1F1F7","j":["south","korea","nation","flag","country","banner"],"k":[3,2],"n":["flag-kr"]},"watch":{"a":"Watch","b":"231A","j":["time","accessories"],"k":[46,41],"o":1},"male-office-worker":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fb-200d-1f4bc.png","sheet_x":17,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fc-200d-1f4bc.png","sheet_x":17,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fd-200d-1f4bc.png","sheet_x":17,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fe-200d-1f4bc.png","sheet_x":17,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f468-1f3ff-200d-1f4bc.png","sheet_x":17,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Office Worker","b":"1F468-200D-1F4BC","k":[17,13]},"closed_lock_with_key":{"a":"Closed Lock with Key","b":"1F510","j":["security","privacy"],"k":[27,18]},"female-office-worker":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fb-200d-1f4bc.png","sheet_x":19,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fc-200d-1f4bc.png","sheet_x":19,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fd-200d-1f4bc.png","sheet_x":19,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fe-200d-1f4bc.png","sheet_x":19,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f469-1f3ff-200d-1f4bc.png","sheet_x":19,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Office Worker","b":"1F469-200D-1F4BC","k":[19,23]},"two":{"a":"Keycap 2","b":"0032-FE0F-20E3","c":"0032-20E3","j":["numbers","2","prime","blue-square"],"k":[0,4],"o":3},"alarm_clock":{"a":"Alarm Clock","b":"23F0","j":["time","wake"],"k":[47,0]},"key":{"a":"Key","b":"1F511","j":["lock","door","password"],"k":[27,19]},"flag-kw":{"a":"Kuwait Flag","b":"1F1F0-1F1FC","k":[3,3]},"stopwatch":{"a":"Stopwatch","b":"23F1-FE0F","c":"23F1","j":["time","deadline"],"k":[47,1]},"male-scientist":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F52C","non_qualified":null,"image":"1f468-1f3fb-200d-1f52c.png","sheet_x":17,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F52C","non_qualified":null,"image":"1f468-1f3fc-200d-1f52c.png","sheet_x":17,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F52C","non_qualified":null,"image":"1f468-1f3fd-200d-1f52c.png","sheet_x":17,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F52C","non_qualified":null,"image":"1f468-1f3fe-200d-1f52c.png","sheet_x":17,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F52C","non_qualified":null,"image":"1f468-1f3ff-200d-1f52c.png","sheet_x":17,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Scientist","b":"1F468-200D-1F52C","k":[17,25]},"three":{"a":"Keycap 3","b":"0033-FE0F-20E3","c":"0033-20E3","j":["3","numbers","prime","blue-square"],"k":[0,5],"o":3},"flag-ky":{"a":"Cayman Islands Flag","b":"1F1F0-1F1FE","k":[3,4]},"old_key":{"a":"Old Key","b":"1F5DD-FE0F","c":"1F5DD","j":["lock","door","password"],"k":[30,11],"o":7},"flag-kz":{"a":"Kazakhstan Flag","b":"1F1F0-1F1FF","k":[3,5]},"hammer":{"a":"Hammer","b":"1F528","j":["tools","build","create"],"k":[27,42]},"female-scientist":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F52C","non_qualified":null,"image":"1f469-1f3fb-200d-1f52c.png","sheet_x":19,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F52C","non_qualified":null,"image":"1f469-1f3fc-200d-1f52c.png","sheet_x":19,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F52C","non_qualified":null,"image":"1f469-1f3fd-200d-1f52c.png","sheet_x":19,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F52C","non_qualified":null,"image":"1f469-1f3fe-200d-1f52c.png","sheet_x":19,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F52C","non_qualified":null,"image":"1f469-1f3ff-200d-1f52c.png","sheet_x":19,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Scientist","b":"1F469-200D-1F52C","k":[19,35]},"timer_clock":{"a":"Timer Clock","b":"23F2-FE0F","c":"23F2","j":["alarm"],"k":[47,2]},"four":{"a":"Keycap 4","b":"0034-FE0F-20E3","c":"0034-20E3","j":["4","numbers","blue-square"],"k":[0,6],"o":3},"male-technologist":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fb-200d-1f4bb.png","sheet_x":17,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fc-200d-1f4bb.png","sheet_x":17,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fd-200d-1f4bb.png","sheet_x":17,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fe-200d-1f4bb.png","sheet_x":17,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f468-1f3ff-200d-1f4bb.png","sheet_x":17,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Technologist","b":"1F468-200D-1F4BB","k":[17,7]},"mantelpiece_clock":{"a":"Mantelpiece Clock","b":"1F570-FE0F","c":"1F570","j":["time"],"k":[28,43],"o":7},"five":{"a":"Keycap 5","b":"0035-FE0F-20E3","c":"0035-20E3","j":["5","numbers","blue-square","prime"],"k":[0,7],"o":3},"flag-la":{"a":"Laos Flag","b":"1F1F1-1F1E6","k":[3,6]},"pick":{"a":"Pick","b":"26CF-FE0F","c":"26CF","j":["tools","dig"],"k":[48,32],"o":5},"flag-lb":{"a":"Lebanon Flag","b":"1F1F1-1F1E7","k":[3,7]},"clock12":{"a":"Clock Face Twelve Oclock","b":"1F55B","j":["time","noon","midnight","midday","late","early","schedule"],"k":[28,29]},"hammer_and_pick":{"a":"Hammer and Pick","b":"2692-FE0F","c":"2692","j":["tools","build","create"],"k":[48,11],"o":4},"six":{"a":"Keycap 6","b":"0036-FE0F-20E3","c":"0036-20E3","j":["6","numbers","blue-square"],"k":[0,8],"o":3},"female-technologist":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fb-200d-1f4bb.png","sheet_x":19,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fc-200d-1f4bb.png","sheet_x":19,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fd-200d-1f4bb.png","sheet_x":19,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fe-200d-1f4bb.png","sheet_x":19,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f469-1f3ff-200d-1f4bb.png","sheet_x":19,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Technologist","b":"1F469-200D-1F4BB","k":[19,17]},"hammer_and_wrench":{"a":"Hammer and Wrench","b":"1F6E0-FE0F","c":"1F6E0","j":["tools","build","create"],"k":[37,8],"o":7},"flag-lc":{"a":"St. Lucia Flag","b":"1F1F1-1F1E8","k":[3,8]},"clock1230":{"a":"Clock Face Twelve-Thirty","b":"1F567","j":["time","late","early","schedule"],"k":[28,41]},"seven":{"a":"Keycap 7","b":"0037-FE0F-20E3","c":"0037-20E3","j":["7","numbers","blue-square","prime"],"k":[0,9],"o":3},"male-singer":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fb-200d-1f3a4.png","sheet_x":16,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fc-200d-1f3a4.png","sheet_x":16,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fd-200d-1f3a4.png","sheet_x":16,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fe-200d-1f3a4.png","sheet_x":16,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f468-1f3ff-200d-1f3a4.png","sheet_x":16,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Singer","b":"1F468-200D-1F3A4","k":[16,20]},"eight":{"a":"Keycap 8","b":"0038-FE0F-20E3","c":"0038-20E3","j":["8","blue-square","numbers"],"k":[0,10],"o":3},"flag-li":{"a":"Liechtenstein Flag","b":"1F1F1-1F1EE","k":[3,9]},"dagger_knife":{"a":"Dagger Knife","b":"1F5E1-FE0F","c":"1F5E1","k":[30,13],"o":7},"clock1":{"a":"Clock Face One Oclock","b":"1F550","j":["time","late","early","schedule"],"k":[28,18]},"female-singer":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fb-200d-1f3a4.png","sheet_x":18,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fc-200d-1f3a4.png","sheet_x":18,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fd-200d-1f3a4.png","sheet_x":18,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fe-200d-1f3a4.png","sheet_x":18,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f469-1f3ff-200d-1f3a4.png","sheet_x":18,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Singer","b":"1F469-200D-1F3A4","k":[18,35]},"male-artist":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fb-200d-1f3a8.png","sheet_x":16,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fc-200d-1f3a8.png","sheet_x":16,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fd-200d-1f3a8.png","sheet_x":16,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fe-200d-1f3a8.png","sheet_x":16,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f468-1f3ff-200d-1f3a8.png","sheet_x":16,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Artist","b":"1F468-200D-1F3A8","k":[16,26]},"crossed_swords":{"a":"Crossed Swords","b":"2694-FE0F","c":"2694","j":["weapon"],"k":[48,13],"o":4},"nine":{"a":"Keycap 9","b":"0039-FE0F-20E3","c":"0039-20E3","j":["blue-square","numbers","9"],"k":[0,11],"o":3},"flag-lk":{"a":"Sri Lanka Flag","b":"1F1F1-1F1F0","k":[3,10]},"clock130":{"a":"Clock Face One-Thirty","b":"1F55C","j":["time","late","early","schedule"],"k":[28,30]},"clock2":{"a":"Clock Face Two Oclock","b":"1F551","j":["time","late","early","schedule"],"k":[28,19]},"gun":{"a":"Pistol","b":"1F52B","j":["violence","weapon","pistol","revolver"],"k":[27,45]},"keycap_ten":{"a":"Keycap Ten","b":"1F51F","j":["numbers","10","blue-square"],"k":[27,33]},"female-artist":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fb-200d-1f3a8.png","sheet_x":18,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fc-200d-1f3a8.png","sheet_x":18,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fd-200d-1f3a8.png","sheet_x":18,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fe-200d-1f3a8.png","sheet_x":18,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f469-1f3ff-200d-1f3a8.png","sheet_x":18,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Artist","b":"1F469-200D-1F3A8","k":[18,41]},"flag-lr":{"a":"Liberia Flag","b":"1F1F1-1F1F7","k":[3,11]},"clock230":{"a":"Clock Face Two-Thirty","b":"1F55D","j":["time","late","early","schedule"],"k":[28,31]},"bow_and_arrow":{"a":"Bow and Arrow","b":"1F3F9","j":["sports"],"k":[12,23],"o":8},"male-pilot":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2708-FE0F","non_qualified":"1F468-1F3FB-200D-2708","image":"1f468-1f3fb-200d-2708-fe0f.png","sheet_x":18,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-2708-FE0F","non_qualified":"1F468-1F3FC-200D-2708","image":"1f468-1f3fc-200d-2708-fe0f.png","sheet_x":18,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-2708-FE0F","non_qualified":"1F468-1F3FD-200D-2708","image":"1f468-1f3fd-200d-2708-fe0f.png","sheet_x":18,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-2708-FE0F","non_qualified":"1F468-1F3FE-200D-2708","image":"1f468-1f3fe-200d-2708-fe0f.png","sheet_x":18,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-2708-FE0F","non_qualified":"1F468-1F3FF-200D-2708","image":"1f468-1f3ff-200d-2708-fe0f.png","sheet_x":18,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Male Pilot","b":"1F468-200D-2708-FE0F","c":"1F468-200D-2708","k":[18,3]},"flag-ls":{"a":"Lesotho Flag","b":"1F1F1-1F1F8","k":[3,12]},"flag-lt":{"a":"Lithuania Flag","b":"1F1F1-1F1F9","k":[3,13]},"capital_abcd":{"a":"Input Symbol for Latin Capital Letters","b":"1F520","j":["alphabet","words","blue-square"],"k":[27,34]},"female-pilot":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2708-FE0F","non_qualified":"1F469-1F3FB-200D-2708","image":"1f469-1f3fb-200d-2708-fe0f.png","sheet_x":20,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-2708-FE0F","non_qualified":"1F469-1F3FC-200D-2708","image":"1f469-1f3fc-200d-2708-fe0f.png","sheet_x":20,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-2708-FE0F","non_qualified":"1F469-1F3FD-200D-2708","image":"1f469-1f3fd-200d-2708-fe0f.png","sheet_x":20,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-2708-FE0F","non_qualified":"1F469-1F3FE-200D-2708","image":"1f469-1f3fe-200d-2708-fe0f.png","sheet_x":20,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-2708-FE0F","non_qualified":"1F469-1F3FF-200D-2708","image":"1f469-1f3ff-200d-2708-fe0f.png","sheet_x":20,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Pilot","b":"1F469-200D-2708-FE0F","c":"1F469-200D-2708","k":[20,13]},"clock3":{"a":"Clock Face Three Oclock","b":"1F552","j":["time","late","early","schedule"],"k":[28,20]},"shield":{"a":"Shield","b":"1F6E1-FE0F","c":"1F6E1","j":["protection","security"],"k":[37,9],"o":7},"male-astronaut":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F680","non_qualified":null,"image":"1f468-1f3fb-200d-1f680.png","sheet_x":17,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F680","non_qualified":null,"image":"1f468-1f3fc-200d-1f680.png","sheet_x":17,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F680","non_qualified":null,"image":"1f468-1f3fd-200d-1f680.png","sheet_x":17,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F680","non_qualified":null,"image":"1f468-1f3fe-200d-1f680.png","sheet_x":17,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F680","non_qualified":null,"image":"1f468-1f3ff-200d-1f680.png","sheet_x":17,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Astronaut","b":"1F468-200D-1F680","k":[17,31]},"abcd":{"a":"Input Symbol for Latin Small Letters","b":"1F521","j":["blue-square","alphabet"],"k":[27,35]},"clock330":{"a":"Clock Face Three-Thirty","b":"1F55E","j":["time","late","early","schedule"],"k":[28,32]},"flag-lu":{"a":"Luxembourg Flag","b":"1F1F1-1F1FA","k":[3,14]},"wrench":{"a":"Wrench","b":"1F527","j":["tools","diy","ikea","fix","maintainer"],"k":[27,41]},"nut_and_bolt":{"a":"Nut and Bolt","b":"1F529","j":["handy","tools","fix"],"k":[27,43]},"clock4":{"a":"Clock Face Four Oclock","b":"1F553","j":["time","late","early","schedule"],"k":[28,21]},"female-astronaut":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F680","non_qualified":null,"image":"1f469-1f3fb-200d-1f680.png","sheet_x":19,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F680","non_qualified":null,"image":"1f469-1f3fc-200d-1f680.png","sheet_x":19,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F680","non_qualified":null,"image":"1f469-1f3fd-200d-1f680.png","sheet_x":19,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F680","non_qualified":null,"image":"1f469-1f3fe-200d-1f680.png","sheet_x":19,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F680","non_qualified":null,"image":"1f469-1f3ff-200d-1f680.png","sheet_x":19,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Astronaut","b":"1F469-200D-1F680","k":[19,41]},"flag-lv":{"a":"Latvia Flag","b":"1F1F1-1F1FB","k":[3,15]},"gear":{"a":"Gear","b":"2699-FE0F","c":"2699","j":["cog"],"k":[48,17],"o":4},"male-firefighter":{"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F692","non_qualified":null,"image":"1f468-1f3fb-200d-1f692.png","sheet_x":17,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F692","non_qualified":null,"image":"1f468-1f3fc-200d-1f692.png","sheet_x":17,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F692","non_qualified":null,"image":"1f468-1f3fd-200d-1f692.png","sheet_x":17,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F692","non_qualified":null,"image":"1f468-1f3fe-200d-1f692.png","sheet_x":17,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F692","non_qualified":null,"image":"1f468-1f3ff-200d-1f692.png","sheet_x":17,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Male Firefighter","b":"1F468-200D-1F692","k":[17,37]},"flag-ly":{"a":"Libya Flag","b":"1F1F1-1F1FE","k":[3,16]},"symbols":{"a":"Input Symbol for Symbols","b":"1F523","j":["blue-square","music","note","ampersand","percent","glyphs","characters"],"k":[27,37]},"clock430":{"a":"Clock Face Four-Thirty","b":"1F55F","j":["time","late","early","schedule"],"k":[28,33]},"flag-ma":{"a":"Morocco Flag","b":"1F1F2-1F1E6","k":[3,17]},"compression":{"a":"Compression","b":"1F5DC-FE0F","c":"1F5DC","k":[30,10],"o":7},"female-firefighter":{"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F692","non_qualified":null,"image":"1f469-1f3fb-200d-1f692.png","sheet_x":19,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F692","non_qualified":null,"image":"1f469-1f3fc-200d-1f692.png","sheet_x":19,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F692","non_qualified":null,"image":"1f469-1f3fd-200d-1f692.png","sheet_x":19,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F692","non_qualified":null,"image":"1f469-1f3fe-200d-1f692.png","sheet_x":19,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F692","non_qualified":null,"image":"1f469-1f3ff-200d-1f692.png","sheet_x":20,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Female Firefighter","b":"1F469-200D-1F692","k":[19,47]},"abc":{"a":"Input Symbol for Latin Letters","b":"1F524","j":["blue-square","alphabet"],"k":[27,38]},"clock5":{"a":"Clock Face Five Oclock","b":"1F554","j":["time","late","early","schedule"],"k":[28,22]},"clock530":{"a":"Clock Face Five-Thirty","b":"1F560","j":["time","late","early","schedule"],"k":[28,34]},"a":{"a":"Negative Squared Latin Capital Letter a","b":"1F170-FE0F","c":"1F170","j":["red-square","alphabet","letter"],"k":[0,16]},"alembic":{"a":"Alembic","b":"2697-FE0F","c":"2697","j":["distilling","science","experiment","chemistry"],"k":[48,16],"o":4},"flag-mc":{"a":"Monaco Flag","b":"1F1F2-1F1E8","k":[3,18]},"cop":{"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB","non_qualified":null,"image":"1f46e-1f3fb.png","sheet_x":20,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F46E-1F3FC","non_qualified":null,"image":"1f46e-1f3fc.png","sheet_x":20,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F46E-1F3FD","non_qualified":null,"image":"1f46e-1f3fd.png","sheet_x":20,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F46E-1F3FE","non_qualified":null,"image":"1f46e-1f3fe.png","sheet_x":20,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F46E-1F3FF","non_qualified":null,"image":"1f46e-1f3ff.png","sheet_x":20,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F46E-200D-2642-FE0F","a":"Police Officer","b":"1F46E","k":[20,45]},"scales":{"a":"Scales","b":"2696-FE0F","c":"2696","k":[48,15],"o":4},"clock6":{"a":"Clock Face Six Oclock","b":"1F555","j":["time","late","early","schedule","dawn","dusk"],"k":[28,23]},"flag-md":{"a":"Moldova Flag","b":"1F1F2-1F1E9","k":[3,19]},"ab":{"a":"Negative Squared Ab","b":"1F18E","j":["red-square","alphabet"],"k":[0,20]},"male-police-officer":{"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2642-FE0F","non_qualified":"1F46E-1F3FB-200D-2642","image":"1f46e-1f3fb-200d-2642-fe0f.png","sheet_x":20,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F46E-1F3FC-200D-2642-FE0F","non_qualified":"1F46E-1F3FC-200D-2642","image":"1f46e-1f3fc-200d-2642-fe0f.png","sheet_x":20,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F46E-1F3FD-200D-2642-FE0F","non_qualified":"1F46E-1F3FD-200D-2642","image":"1f46e-1f3fd-200d-2642-fe0f.png","sheet_x":20,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F46E-1F3FE-200D-2642-FE0F","non_qualified":"1F46E-1F3FE-200D-2642","image":"1f46e-1f3fe-200d-2642-fe0f.png","sheet_x":20,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F46E-1F3FF-200D-2642-FE0F","non_qualified":"1F46E-1F3FF-200D-2642","image":"1f46e-1f3ff-200d-2642-fe0f.png","sheet_x":20,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F46E","a":"Male Police Officer","b":"1F46E-200D-2642-FE0F","c":"1F46E-200D-2642","k":[20,39]},"link":{"a":"Link Symbol","b":"1F517","j":["rings","url"],"k":[27,25]},"flag-me":{"a":"Montenegro Flag","b":"1F1F2-1F1EA","k":[3,20]},"clock630":{"a":"Clock Face Six-Thirty","b":"1F561","j":["time","late","early","schedule"],"k":[28,35]},"b":{"a":"Negative Squared Latin Capital Letter B","b":"1F171-FE0F","c":"1F171","j":["red-square","alphabet","letter"],"k":[0,17]},"female-police-officer":{"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2640-FE0F","non_qualified":"1F46E-1F3FB-200D-2640","image":"1f46e-1f3fb-200d-2640-fe0f.png","sheet_x":20,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F46E-1F3FC-200D-2640-FE0F","non_qualified":"1F46E-1F3FC-200D-2640","image":"1f46e-1f3fc-200d-2640-fe0f.png","sheet_x":20,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F46E-1F3FD-200D-2640-FE0F","non_qualified":"1F46E-1F3FD-200D-2640","image":"1f46e-1f3fd-200d-2640-fe0f.png","sheet_x":20,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F46E-1F3FE-200D-2640-FE0F","non_qualified":"1F46E-1F3FE-200D-2640","image":"1f46e-1f3fe-200d-2640-fe0f.png","sheet_x":20,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F46E-1F3FF-200D-2640-FE0F","non_qualified":"1F46E-1F3FF-200D-2640","image":"1f46e-1f3ff-200d-2640-fe0f.png","sheet_x":20,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Police Officer","b":"1F46E-200D-2640-FE0F","c":"1F46E-200D-2640","k":[20,33]},"clock7":{"a":"Clock Face Seven Oclock","b":"1F556","j":["time","late","early","schedule"],"k":[28,24]},"cl":{"a":"Squared Cl","b":"1F191","j":["alphabet","words","red-square"],"k":[0,21]},"sleuth_or_spy":{"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB","non_qualified":null,"image":"1f575-1f3fb.png","sheet_x":29,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F575-1F3FC","non_qualified":null,"image":"1f575-1f3fc.png","sheet_x":29,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F575-1F3FD","non_qualified":null,"image":"1f575-1f3fd.png","sheet_x":29,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F575-1F3FE","non_qualified":null,"image":"1f575-1f3fe.png","sheet_x":29,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F575-1F3FF","non_qualified":null,"image":"1f575-1f3ff.png","sheet_x":29,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoleted_by":"1F575-FE0F-200D-2642-FE0F","a":"Sleuth or Spy","b":"1F575-FE0F","c":"1F575","k":[29,11],"o":7},"chains":{"a":"Chains","b":"26D3-FE0F","c":"26D3","j":["lock","arrest"],"k":[48,34],"o":5},"syringe":{"a":"Syringe","b":"1F489","j":["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],"k":[24,35]},"male-detective":{"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2642-FE0F","non_qualified":"1F575-1F3FB-200D-2642","image":"1f575-1f3fb-200d-2642-fe0f.png","sheet_x":29,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F575-1F3FC-200D-2642-FE0F","non_qualified":"1F575-1F3FC-200D-2642","image":"1f575-1f3fc-200d-2642-fe0f.png","sheet_x":29,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F575-1F3FD-200D-2642-FE0F","non_qualified":"1F575-1F3FD-200D-2642","image":"1f575-1f3fd-200d-2642-fe0f.png","sheet_x":29,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F575-1F3FE-200D-2642-FE0F","non_qualified":"1F575-1F3FE-200D-2642","image":"1f575-1f3fe-200d-2642-fe0f.png","sheet_x":29,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F575-1F3FF-200D-2642-FE0F","non_qualified":"1F575-1F3FF-200D-2642","image":"1f575-1f3ff-200d-2642-fe0f.png","sheet_x":29,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F575-FE0F","a":"Male Detective","b":"1F575-FE0F-200D-2642-FE0F","k":[29,5],"o":7},"cool":{"a":"Squared Cool","b":"1F192","j":["words","blue-square"],"k":[0,22]},"clock730":{"a":"Clock Face Seven-Thirty","b":"1F562","j":["time","late","early","schedule"],"k":[28,36]},"flag-mg":{"a":"Madagascar Flag","b":"1F1F2-1F1EC","k":[3,22]},"free":{"a":"Squared Free","b":"1F193","j":["blue-square","words"],"k":[0,23]},"flag-mh":{"a":"Marshall Islands Flag","b":"1F1F2-1F1ED","k":[3,23]},"clock8":{"a":"Clock Face Eight Oclock","b":"1F557","j":["time","late","early","schedule"],"k":[28,25]},"pill":{"a":"Pill","b":"1F48A","j":["health","medicine","doctor","pharmacy","drug"],"k":[24,36]},"female-detective":{"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2640-FE0F","non_qualified":"1F575-1F3FB-200D-2640","image":"1f575-1f3fb-200d-2640-fe0f.png","sheet_x":29,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F575-1F3FC-200D-2640-FE0F","non_qualified":"1F575-1F3FC-200D-2640","image":"1f575-1f3fc-200d-2640-fe0f.png","sheet_x":29,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F575-1F3FD-200D-2640-FE0F","non_qualified":"1F575-1F3FD-200D-2640","image":"1f575-1f3fd-200d-2640-fe0f.png","sheet_x":29,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F575-1F3FE-200D-2640-FE0F","non_qualified":"1F575-1F3FE-200D-2640","image":"1f575-1f3fe-200d-2640-fe0f.png","sheet_x":29,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F575-1F3FF-200D-2640-FE0F","non_qualified":"1F575-1F3FF-200D-2640","image":"1f575-1f3ff-200d-2640-fe0f.png","sheet_x":29,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Detective","b":"1F575-FE0F-200D-2640-FE0F","k":[28,51],"o":7},"clock830":{"a":"Clock Face Eight-Thirty","b":"1F563","j":["time","late","early","schedule"],"k":[28,37]},"guardsman":{"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB","non_qualified":null,"image":"1f482-1f3fb.png","sheet_x":23,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F482-1F3FC","non_qualified":null,"image":"1f482-1f3fc.png","sheet_x":23,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F482-1F3FD","non_qualified":null,"image":"1f482-1f3fd.png","sheet_x":23,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F482-1F3FE","non_qualified":null,"image":"1f482-1f3fe.png","sheet_x":23,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F482-1F3FF","non_qualified":null,"image":"1f482-1f3ff.png","sheet_x":23,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F482-200D-2642-FE0F","a":"Guardsman","b":"1F482","j":["uk","gb","british","male","guy","royal"],"k":[23,31]},"information_source":{"a":"Information Source","b":"2139-FE0F","c":"2139","j":["blue-square","alphabet","letter"],"k":[46,32],"o":3},"flag-mk":{"a":"Macedonia Flag","b":"1F1F2-1F1F0","k":[3,24]},"smoking":{"a":"Smoking Symbol","b":"1F6AC","j":["kills","tobacco","cigarette","joint","smoke"],"k":[35,17]},"id":{"a":"Squared Id","b":"1F194","j":["purple-square","words"],"k":[0,24]},"clock9":{"a":"Clock Face Nine Oclock","b":"1F558","j":["time","late","early","schedule"],"k":[28,26]},"flag-ml":{"a":"Mali Flag","b":"1F1F2-1F1F1","k":[3,25]},"coffin":{"a":"Coffin","b":"26B0-FE0F","c":"26B0","j":["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],"k":[48,24],"o":4},"male-guard":{"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2642-FE0F","non_qualified":"1F482-1F3FB-200D-2642","image":"1f482-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F482-1F3FC-200D-2642-FE0F","non_qualified":"1F482-1F3FC-200D-2642","image":"1f482-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F482-1F3FD-200D-2642-FE0F","non_qualified":"1F482-1F3FD-200D-2642","image":"1f482-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F482-1F3FE-200D-2642-FE0F","non_qualified":"1F482-1F3FE-200D-2642","image":"1f482-1f3fe-200d-2642-fe0f.png","sheet_x":23,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F482-1F3FF-200D-2642-FE0F","non_qualified":"1F482-1F3FF-200D-2642","image":"1f482-1f3ff-200d-2642-fe0f.png","sheet_x":23,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F482","a":"Male Guard","b":"1F482-200D-2642-FE0F","c":"1F482-200D-2642","k":[23,25]},"m":{"a":"Circled Latin Capital Letter M","b":"24C2-FE0F","c":"24C2","j":["alphabet","blue-circle","letter"],"k":[47,7],"o":1},"funeral_urn":{"a":"Funeral Urn","b":"26B1-FE0F","c":"26B1","j":["dead","die","death","rip","ashes"],"k":[48,25],"o":4},"female-guard":{"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2640-FE0F","non_qualified":"1F482-1F3FB-200D-2640","image":"1f482-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F482-1F3FC-200D-2640-FE0F","non_qualified":"1F482-1F3FC-200D-2640","image":"1f482-1f3fc-200d-2640-fe0f.png","sheet_x":23,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F482-1F3FD-200D-2640-FE0F","non_qualified":"1F482-1F3FD-200D-2640","image":"1f482-1f3fd-200d-2640-fe0f.png","sheet_x":23,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F482-1F3FE-200D-2640-FE0F","non_qualified":"1F482-1F3FE-200D-2640","image":"1f482-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F482-1F3FF-200D-2640-FE0F","non_qualified":"1F482-1F3FF-200D-2640","image":"1f482-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Guard","b":"1F482-200D-2640-FE0F","c":"1F482-200D-2640","k":[23,19]},"flag-mm":{"a":"Myanmar (burma) Flag","b":"1F1F2-1F1F2","k":[3,26]},"clock930":{"a":"Clock Face Nine-Thirty","b":"1F564","j":["time","late","early","schedule"],"k":[28,38]},"moyai":{"a":"Moyai","b":"1F5FF","j":["rock","easter island","moai"],"k":[30,23]},"new":{"a":"Squared New","b":"1F195","j":["blue-square","words","start"],"k":[0,25]},"flag-mn":{"a":"Mongolia Flag","b":"1F1F2-1F1F3","k":[3,27]},"construction_worker":{"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB","non_qualified":null,"image":"1f477-1f3fb.png","sheet_x":22,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F477-1F3FC","non_qualified":null,"image":"1f477-1f3fc.png","sheet_x":22,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F477-1F3FD","non_qualified":null,"image":"1f477-1f3fd.png","sheet_x":22,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F477-1F3FE","non_qualified":null,"image":"1f477-1f3fe.png","sheet_x":22,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F477-1F3FF","non_qualified":null,"image":"1f477-1f3ff.png","sheet_x":22,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F477-200D-2642-FE0F","a":"Construction Worker","b":"1F477","k":[22,28]},"clock10":{"a":"Clock Face Ten Oclock","b":"1F559","j":["time","late","early","schedule"],"k":[28,27]},"clock1030":{"a":"Clock Face Ten-Thirty","b":"1F565","j":["time","late","early","schedule"],"k":[28,39]},"ng":{"a":"Squared Ng","b":"1F196","j":["blue-square","words","shape","icon"],"k":[0,26]},"male-construction-worker":{"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2642-FE0F","non_qualified":"1F477-1F3FB-200D-2642","image":"1f477-1f3fb-200d-2642-fe0f.png","sheet_x":22,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F477-1F3FC-200D-2642-FE0F","non_qualified":"1F477-1F3FC-200D-2642","image":"1f477-1f3fc-200d-2642-fe0f.png","sheet_x":22,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F477-1F3FD-200D-2642-FE0F","non_qualified":"1F477-1F3FD-200D-2642","image":"1f477-1f3fd-200d-2642-fe0f.png","sheet_x":22,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F477-1F3FE-200D-2642-FE0F","non_qualified":"1F477-1F3FE-200D-2642","image":"1f477-1f3fe-200d-2642-fe0f.png","sheet_x":22,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F477-1F3FF-200D-2642-FE0F","non_qualified":"1F477-1F3FF-200D-2642","image":"1f477-1f3ff-200d-2642-fe0f.png","sheet_x":22,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F477","a":"Male Construction Worker","b":"1F477-200D-2642-FE0F","c":"1F477-200D-2642","k":[22,22]},"flag-mo":{"a":"Macau Sar China Flag","b":"1F1F2-1F1F4","k":[3,28]},"oil_drum":{"a":"Oil Drum","b":"1F6E2-FE0F","c":"1F6E2","j":["barrell"],"k":[37,10],"o":7},"o2":{"a":"Negative Squared Latin Capital Letter O","b":"1F17E-FE0F","c":"1F17E","j":["alphabet","red-square","letter"],"k":[0,18]},"female-construction-worker":{"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2640-FE0F","non_qualified":"1F477-1F3FB-200D-2640","image":"1f477-1f3fb-200d-2640-fe0f.png","sheet_x":22,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F477-1F3FC-200D-2640-FE0F","non_qualified":"1F477-1F3FC-200D-2640","image":"1f477-1f3fc-200d-2640-fe0f.png","sheet_x":22,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F477-1F3FD-200D-2640-FE0F","non_qualified":"1F477-1F3FD-200D-2640","image":"1f477-1f3fd-200d-2640-fe0f.png","sheet_x":22,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F477-1F3FE-200D-2640-FE0F","non_qualified":"1F477-1F3FE-200D-2640","image":"1f477-1f3fe-200d-2640-fe0f.png","sheet_x":22,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F477-1F3FF-200D-2640-FE0F","non_qualified":"1F477-1F3FF-200D-2640","image":"1f477-1f3ff-200d-2640-fe0f.png","sheet_x":22,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Construction Worker","b":"1F477-200D-2640-FE0F","c":"1F477-200D-2640","k":[22,16]},"clock11":{"a":"Clock Face Eleven Oclock","b":"1F55A","j":["time","late","early","schedule"],"k":[28,28]},"crystal_ball":{"a":"Crystal Ball","b":"1F52E","j":["disco","party","magic","circus","fortune_teller"],"k":[27,48]},"flag-mp":{"a":"Northern Mariana Islands Flag","b":"1F1F2-1F1F5","k":[3,29]},"prince":{"skin_variations":{"1F3FB":{"unified":"1F934-1F3FB","non_qualified":null,"image":"1f934-1f3fb.png","sheet_x":39,"sheet_y":29,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F934-1F3FC","non_qualified":null,"image":"1f934-1f3fc.png","sheet_x":39,"sheet_y":30,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F934-1F3FD","non_qualified":null,"image":"1f934-1f3fd.png","sheet_x":39,"sheet_y":31,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F934-1F3FE","non_qualified":null,"image":"1f934-1f3fe.png","sheet_x":39,"sheet_y":32,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F934-1F3FF","non_qualified":null,"image":"1f934-1f3ff.png","sheet_x":39,"sheet_y":33,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Prince","b":"1F934","j":["boy","man","male","crown","royal","king"],"k":[39,28],"o":9},"ok":{"a":"Squared Ok","b":"1F197","j":["good","agree","yes","blue-square"],"k":[0,27]},"clock1130":{"a":"Clock Face Eleven-Thirty","b":"1F566","j":["time","late","early","schedule"],"k":[28,40]},"shopping_trolley":{"a":"Shopping Trolley","b":"1F6D2","k":[37,7],"o":9},"flag-mr":{"a":"Mauritania Flag","b":"1F1F2-1F1F7","k":[3,31]},"princess":{"skin_variations":{"1F3FB":{"unified":"1F478-1F3FB","non_qualified":null,"image":"1f478-1f3fb.png","sheet_x":22,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F478-1F3FC","non_qualified":null,"image":"1f478-1f3fc.png","sheet_x":22,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F478-1F3FD","non_qualified":null,"image":"1f478-1f3fd.png","sheet_x":22,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F478-1F3FE","non_qualified":null,"image":"1f478-1f3fe.png","sheet_x":22,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F478-1F3FF","non_qualified":null,"image":"1f478-1f3ff.png","sheet_x":22,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Princess","b":"1F478","j":["girl","woman","female","blond","crown","royal","queen"],"k":[22,34]},"new_moon":{"a":"New Moon Symbol","b":"1F311","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,9]},"parking":{"a":"Negative Squared Latin Capital Letter P","b":"1F17F-FE0F","c":"1F17F","j":["cars","blue-square","alphabet","letter"],"k":[0,19],"o":5},"sos":{"a":"Squared Sos","b":"1F198","j":["help","red-square","words","emergency","911"],"k":[0,28]},"man_with_turban":{"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB","non_qualified":null,"image":"1f473-1f3fb.png","sheet_x":21,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F473-1F3FC","non_qualified":null,"image":"1f473-1f3fc.png","sheet_x":21,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F473-1F3FD","non_qualified":null,"image":"1f473-1f3fd.png","sheet_x":21,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F473-1F3FE","non_qualified":null,"image":"1f473-1f3fe.png","sheet_x":21,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F473-1F3FF","non_qualified":null,"image":"1f473-1f3ff.png","sheet_x":21,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F473-200D-2642-FE0F","a":"Man with Turban","b":"1F473","j":["male","indian","hinduism","arabs"],"k":[21,44]},"flag-ms":{"a":"Montserrat Flag","b":"1F1F2-1F1F8","k":[3,32]},"waxing_crescent_moon":{"a":"Waxing Crescent Moon Symbol","b":"1F312","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,10]},"up":{"a":"Squared Up with Exclamation Mark","b":"1F199","j":["blue-square","above","high"],"k":[0,29]},"first_quarter_moon":{"a":"First Quarter Moon Symbol","b":"1F313","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,11]},"flag-mt":{"a":"Malta Flag","b":"1F1F2-1F1F9","k":[3,33]},"man-wearing-turban":{"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2642-FE0F","non_qualified":"1F473-1F3FB-200D-2642","image":"1f473-1f3fb-200d-2642-fe0f.png","sheet_x":21,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F473-1F3FC-200D-2642-FE0F","non_qualified":"1F473-1F3FC-200D-2642","image":"1f473-1f3fc-200d-2642-fe0f.png","sheet_x":21,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F473-1F3FD-200D-2642-FE0F","non_qualified":"1F473-1F3FD-200D-2642","image":"1f473-1f3fd-200d-2642-fe0f.png","sheet_x":21,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F473-1F3FE-200D-2642-FE0F","non_qualified":"1F473-1F3FE-200D-2642","image":"1f473-1f3fe-200d-2642-fe0f.png","sheet_x":21,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F473-1F3FF-200D-2642-FE0F","non_qualified":"1F473-1F3FF-200D-2642","image":"1f473-1f3ff-200d-2642-fe0f.png","sheet_x":21,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F473","a":"Man Wearing Turban","b":"1F473-200D-2642-FE0F","c":"1F473-200D-2642","k":[21,38]},"moon":{"a":"Waxing Gibbous Moon Symbol","b":"1F314","k":[6,12],"n":["waxing_gibbous_moon"]},"woman-wearing-turban":{"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2640-FE0F","non_qualified":"1F473-1F3FB-200D-2640","image":"1f473-1f3fb-200d-2640-fe0f.png","sheet_x":21,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F473-1F3FC-200D-2640-FE0F","non_qualified":"1F473-1F3FC-200D-2640","image":"1f473-1f3fc-200d-2640-fe0f.png","sheet_x":21,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F473-1F3FD-200D-2640-FE0F","non_qualified":"1F473-1F3FD-200D-2640","image":"1f473-1f3fd-200d-2640-fe0f.png","sheet_x":21,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F473-1F3FE-200D-2640-FE0F","non_qualified":"1F473-1F3FE-200D-2640","image":"1f473-1f3fe-200d-2640-fe0f.png","sheet_x":21,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F473-1F3FF-200D-2640-FE0F","non_qualified":"1F473-1F3FF-200D-2640","image":"1f473-1f3ff-200d-2640-fe0f.png","sheet_x":21,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Wearing Turban","b":"1F473-200D-2640-FE0F","c":"1F473-200D-2640","k":[21,32]},"vs":{"a":"Squared Vs","b":"1F19A","j":["words","orange-square"],"k":[0,30]},"flag-mu":{"a":"Mauritius Flag","b":"1F1F2-1F1FA","k":[3,34]},"man_with_gua_pi_mao":{"skin_variations":{"1F3FB":{"unified":"1F472-1F3FB","non_qualified":null,"image":"1f472-1f3fb.png","sheet_x":21,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F472-1F3FC","non_qualified":null,"image":"1f472-1f3fc.png","sheet_x":21,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F472-1F3FD","non_qualified":null,"image":"1f472-1f3fd.png","sheet_x":21,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F472-1F3FE","non_qualified":null,"image":"1f472-1f3fe.png","sheet_x":21,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F472-1F3FF","non_qualified":null,"image":"1f472-1f3ff.png","sheet_x":21,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Man with Gua Pi Mao","b":"1F472","j":["male","boy","chinese"],"k":[21,26]},"koko":{"a":"Squared Katakana Koko","b":"1F201","j":["blue-square","here","katakana","japanese","destination"],"k":[5,29]},"full_moon":{"a":"Full Moon Symbol","b":"1F315","j":["nature","yellow","twilight","planet","space","night","evening","sleep"],"k":[6,13]},"flag-mv":{"a":"Maldives Flag","b":"1F1F2-1F1FB","k":[3,35]},"person_with_headscarf":{"skin_variations":{"1F3FB":{"unified":"1F9D5-1F3FB","non_qualified":null,"image":"1f9d5-1f3fb.png","sheet_x":43,"sheet_y":23,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F9D5-1F3FC","non_qualified":null,"image":"1f9d5-1f3fc.png","sheet_x":43,"sheet_y":24,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F9D5-1F3FD","non_qualified":null,"image":"1f9d5-1f3fd.png","sheet_x":43,"sheet_y":25,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F9D5-1F3FE","non_qualified":null,"image":"1f9d5-1f3fe.png","sheet_x":43,"sheet_y":26,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F9D5-1F3FF","non_qualified":null,"image":"1f9d5-1f3ff.png","sheet_x":43,"sheet_y":27,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Person with Headscarf","b":"1F9D5","k":[43,22],"o":10},"waning_gibbous_moon":{"a":"Waning Gibbous Moon Symbol","b":"1F316","j":["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],"k":[6,14]},"sa":{"a":"Squared Katakana Sa","b":"1F202-FE0F","c":"1F202","j":["japanese","blue-square","katakana"],"k":[5,30]},"flag-mw":{"a":"Malawi Flag","b":"1F1F2-1F1FC","k":[3,36]},"last_quarter_moon":{"a":"Last Quarter Moon Symbol","b":"1F317","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,15]},"u6708":{"a":"Squared Cjk Unified Ideograph-6708","b":"1F237-FE0F","c":"1F237","j":["chinese","month","moon","japanese","orange-square","kanji"],"k":[5,38]},"bearded_person":{"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB","non_qualified":null,"image":"1f9d4-1f3fb.png","sheet_x":43,"sheet_y":17,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F9D4-1F3FC","non_qualified":null,"image":"1f9d4-1f3fc.png","sheet_x":43,"sheet_y":18,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F9D4-1F3FD","non_qualified":null,"image":"1f9d4-1f3fd.png","sheet_x":43,"sheet_y":19,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F9D4-1F3FE","non_qualified":null,"image":"1f9d4-1f3fe.png","sheet_x":43,"sheet_y":20,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F9D4-1F3FF","non_qualified":null,"image":"1f9d4-1f3ff.png","sheet_x":43,"sheet_y":21,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Bearded Person","b":"1F9D4","k":[43,16],"o":10},"flag-mx":{"a":"Mexico Flag","b":"1F1F2-1F1FD","k":[3,37]},"u6709":{"a":"Squared Cjk Unified Ideograph-6709","b":"1F236","j":["orange-square","chinese","have","kanji"],"k":[5,37]},"person_with_blond_hair":{"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB","non_qualified":null,"image":"1f471-1f3fb.png","sheet_x":21,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F471-1F3FC","non_qualified":null,"image":"1f471-1f3fc.png","sheet_x":21,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F471-1F3FD","non_qualified":null,"image":"1f471-1f3fd.png","sheet_x":21,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F471-1F3FE","non_qualified":null,"image":"1f471-1f3fe.png","sheet_x":21,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F471-1F3FF","non_qualified":null,"image":"1f471-1f3ff.png","sheet_x":21,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F471-200D-2642-FE0F","a":"Person with Blond Hair","b":"1F471","k":[21,20]},"waning_crescent_moon":{"a":"Waning Crescent Moon Symbol","b":"1F318","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,16]},"flag-my":{"a":"Malaysia Flag","b":"1F1F2-1F1FE","k":[3,38]},"u6307":{"a":"Squared Cjk Unified Ideograph-6307","b":"1F22F","j":["chinese","point","green-square","kanji"],"k":[5,32],"o":5},"blond-haired-man":{"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2642-FE0F","non_qualified":"1F471-1F3FB-200D-2642","image":"1f471-1f3fb-200d-2642-fe0f.png","sheet_x":21,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F471-1F3FC-200D-2642-FE0F","non_qualified":"1F471-1F3FC-200D-2642","image":"1f471-1f3fc-200d-2642-fe0f.png","sheet_x":21,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F471-1F3FD-200D-2642-FE0F","non_qualified":"1F471-1F3FD-200D-2642","image":"1f471-1f3fd-200d-2642-fe0f.png","sheet_x":21,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F471-1F3FE-200D-2642-FE0F","non_qualified":"1F471-1F3FE-200D-2642","image":"1f471-1f3fe-200d-2642-fe0f.png","sheet_x":21,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F471-1F3FF-200D-2642-FE0F","non_qualified":"1F471-1F3FF-200D-2642","image":"1f471-1f3ff-200d-2642-fe0f.png","sheet_x":21,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F471","a":"Blond Haired Man","b":"1F471-200D-2642-FE0F","c":"1F471-200D-2642","k":[21,14]},"crescent_moon":{"a":"Crescent Moon","b":"1F319","j":["night","sleep","sky","evening","magic"],"k":[6,17]},"flag-mz":{"a":"Mozambique Flag","b":"1F1F2-1F1FF","k":[3,39]},"new_moon_with_face":{"a":"New Moon with Face","b":"1F31A","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,18]},"flag-na":{"a":"Namibia Flag","b":"1F1F3-1F1E6","k":[3,40]},"blond-haired-woman":{"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2640-FE0F","non_qualified":"1F471-1F3FB-200D-2640","image":"1f471-1f3fb-200d-2640-fe0f.png","sheet_x":21,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F471-1F3FC-200D-2640-FE0F","non_qualified":"1F471-1F3FC-200D-2640","image":"1f471-1f3fc-200d-2640-fe0f.png","sheet_x":21,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F471-1F3FD-200D-2640-FE0F","non_qualified":"1F471-1F3FD-200D-2640","image":"1f471-1f3fd-200d-2640-fe0f.png","sheet_x":21,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F471-1F3FE-200D-2640-FE0F","non_qualified":"1F471-1F3FE-200D-2640","image":"1f471-1f3fe-200d-2640-fe0f.png","sheet_x":21,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F471-1F3FF-200D-2640-FE0F","non_qualified":"1F471-1F3FF-200D-2640","image":"1f471-1f3ff-200d-2640-fe0f.png","sheet_x":21,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Blond Haired Woman","b":"1F471-200D-2640-FE0F","c":"1F471-200D-2640","k":[21,8]},"ideograph_advantage":{"a":"Circled Ideograph Advantage","b":"1F250","j":["chinese","kanji","obtain","get","circle"],"k":[5,42]},"first_quarter_moon_with_face":{"a":"First Quarter Moon with Face","b":"1F31B","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,19]},"man_in_tuxedo":{"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB","non_qualified":null,"image":"1f935-1f3fb.png","sheet_x":39,"sheet_y":35,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F935-1F3FC","non_qualified":null,"image":"1f935-1f3fc.png","sheet_x":39,"sheet_y":36,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F935-1F3FD","non_qualified":null,"image":"1f935-1f3fd.png","sheet_x":39,"sheet_y":37,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F935-1F3FE","non_qualified":null,"image":"1f935-1f3fe.png","sheet_x":39,"sheet_y":38,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F935-1F3FF","non_qualified":null,"image":"1f935-1f3ff.png","sheet_x":39,"sheet_y":39,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Man in Tuxedo","b":"1F935","j":["couple","marriage","wedding","groom"],"k":[39,34],"o":9},"u5272":{"a":"Squared Cjk Unified Ideograph-5272","b":"1F239","j":["cut","divide","chinese","kanji","pink-square"],"k":[5,40]},"flag-ne":{"a":"Niger Flag","b":"1F1F3-1F1EA","k":[3,42]},"last_quarter_moon_with_face":{"a":"Last Quarter Moon with Face","b":"1F31C","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,20]},"u7121":{"a":"Squared Cjk Unified Ideograph-7121","b":"1F21A","j":["nothing","chinese","kanji","japanese","orange-square"],"k":[5,31],"o":5},"bride_with_veil":{"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB","non_qualified":null,"image":"1f470-1f3fb.png","sheet_x":21,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F470-1F3FC","non_qualified":null,"image":"1f470-1f3fc.png","sheet_x":21,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F470-1F3FD","non_qualified":null,"image":"1f470-1f3fd.png","sheet_x":21,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F470-1F3FE","non_qualified":null,"image":"1f470-1f3fe.png","sheet_x":21,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F470-1F3FF","non_qualified":null,"image":"1f470-1f3ff.png","sheet_x":21,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Bride with Veil","b":"1F470","j":["couple","marriage","wedding","woman","bride"],"k":[21,2]},"u7981":{"a":"Squared Cjk Unified Ideograph-7981","b":"1F232","j":["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],"k":[5,33]},"pregnant_woman":{"skin_variations":{"1F3FB":{"unified":"1F930-1F3FB","non_qualified":null,"image":"1f930-1f3fb.png","sheet_x":39,"sheet_y":5,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F930-1F3FC","non_qualified":null,"image":"1f930-1f3fc.png","sheet_x":39,"sheet_y":6,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F930-1F3FD","non_qualified":null,"image":"1f930-1f3fd.png","sheet_x":39,"sheet_y":7,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F930-1F3FE","non_qualified":null,"image":"1f930-1f3fe.png","sheet_x":39,"sheet_y":8,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F930-1F3FF","non_qualified":null,"image":"1f930-1f3ff.png","sheet_x":39,"sheet_y":9,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Pregnant Woman","b":"1F930","j":["baby"],"k":[39,4],"o":9},"thermometer":{"a":"Thermometer","b":"1F321-FE0F","c":"1F321","j":["weather","temperature","hot","cold"],"k":[6,25],"o":7},"flag-nf":{"a":"Norfolk Island Flag","b":"1F1F3-1F1EB","k":[3,43]},"sunny":{"a":"Black Sun with Rays","b":"2600-FE0F","c":"2600","j":["weather","nature","brightness","summer","beach","spring"],"k":[47,16],"o":1},"accept":{"a":"Circled Ideograph Accept","b":"1F251","j":["ok","good","chinese","kanji","agree","yes","orange-circle"],"k":[5,43]},"flag-ng":{"a":"Nigeria Flag","b":"1F1F3-1F1EC","k":[3,44]},"breast-feeding":{"skin_variations":{"1F3FB":{"unified":"1F931-1F3FB","non_qualified":null,"image":"1f931-1f3fb.png","sheet_x":39,"sheet_y":11,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F931-1F3FC","non_qualified":null,"image":"1f931-1f3fc.png","sheet_x":39,"sheet_y":12,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F931-1F3FD","non_qualified":null,"image":"1f931-1f3fd.png","sheet_x":39,"sheet_y":13,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F931-1F3FE","non_qualified":null,"image":"1f931-1f3fe.png","sheet_x":39,"sheet_y":14,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F931-1F3FF","non_qualified":null,"image":"1f931-1f3ff.png","sheet_x":39,"sheet_y":15,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Breast-Feeding","b":"1F931","k":[39,10],"o":10},"full_moon_with_face":{"a":"Full Moon with Face","b":"1F31D","j":["nature","twilight","planet","space","night","evening","sleep"],"k":[6,21]},"flag-ni":{"a":"Nicaragua Flag","b":"1F1F3-1F1EE","k":[3,45]},"u7533":{"a":"Squared Cjk Unified Ideograph-7533","b":"1F238","j":["chinese","japanese","kanji","orange-square"],"k":[5,39]},"angel":{"skin_variations":{"1F3FB":{"unified":"1F47C-1F3FB","non_qualified":null,"image":"1f47c-1f3fb.png","sheet_x":22,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F47C-1F3FC","non_qualified":null,"image":"1f47c-1f3fc.png","sheet_x":22,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F47C-1F3FD","non_qualified":null,"image":"1f47c-1f3fd.png","sheet_x":22,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F47C-1F3FE","non_qualified":null,"image":"1f47c-1f3fe.png","sheet_x":22,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F47C-1F3FF","non_qualified":null,"image":"1f47c-1f3ff.png","sheet_x":22,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Baby Angel","b":"1F47C","j":["heaven","wings","halo"],"k":[22,43]},"sun_with_face":{"a":"Sun with Face","b":"1F31E","j":["nature","morning","sky"],"k":[6,22]},"santa":{"skin_variations":{"1F3FB":{"unified":"1F385-1F3FB","non_qualified":null,"image":"1f385-1f3fb.png","sheet_x":8,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F385-1F3FC","non_qualified":null,"image":"1f385-1f3fc.png","sheet_x":8,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F385-1F3FD","non_qualified":null,"image":"1f385-1f3fd.png","sheet_x":8,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F385-1F3FE","non_qualified":null,"image":"1f385-1f3fe.png","sheet_x":8,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F385-1F3FF","non_qualified":null,"image":"1f385-1f3ff.png","sheet_x":8,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Father Christmas","b":"1F385","j":["festival","man","male","xmas","father christmas"],"k":[8,19]},"u5408":{"a":"Squared Cjk Unified Ideograph-5408","b":"1F234","j":["japanese","chinese","join","kanji","red-square"],"k":[5,35]},"flag-nl":{"a":"Netherlands Flag","b":"1F1F3-1F1F1","k":[3,46]},"mrs_claus":{"skin_variations":{"1F3FB":{"unified":"1F936-1F3FB","non_qualified":null,"image":"1f936-1f3fb.png","sheet_x":39,"sheet_y":41,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F936-1F3FC","non_qualified":null,"image":"1f936-1f3fc.png","sheet_x":39,"sheet_y":42,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F936-1F3FD","non_qualified":null,"image":"1f936-1f3fd.png","sheet_x":39,"sheet_y":43,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F936-1F3FE","non_qualified":null,"image":"1f936-1f3fe.png","sheet_x":39,"sheet_y":44,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F936-1F3FF","non_qualified":null,"image":"1f936-1f3ff.png","sheet_x":39,"sheet_y":45,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Mother Christmas","b":"1F936","j":["woman","female","xmas","mother christmas"],"k":[39,40],"n":["mother_christmas"],"o":9},"u7a7a":{"a":"Squared Cjk Unified Ideograph-7a7a","b":"1F233","j":["kanji","japanese","chinese","empty","sky","blue-square"],"k":[5,34]},"star":{"a":"White Medium Star","b":"2B50","j":["night","yellow"],"k":[50,22],"o":5},"flag-no":{"a":"Norway Flag","b":"1F1F3-1F1F4","k":[3,47]},"mage":{"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB","non_qualified":null,"image":"1f9d9-1f3fb.png","sheet_x":44,"sheet_y":43,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D9-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D9-1F3FC","non_qualified":null,"image":"1f9d9-1f3fc.png","sheet_x":44,"sheet_y":44,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D9-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D9-1F3FD","non_qualified":null,"image":"1f9d9-1f3fd.png","sheet_x":44,"sheet_y":45,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D9-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D9-1F3FE","non_qualified":null,"image":"1f9d9-1f3fe.png","sheet_x":44,"sheet_y":46,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D9-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D9-1F3FF","non_qualified":null,"image":"1f9d9-1f3ff.png","sheet_x":44,"sheet_y":47,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D9-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D9-200D-2640-FE0F","a":"Mage","b":"1F9D9","k":[44,42],"o":10},"star2":{"a":"Glowing Star","b":"1F31F","j":["night","sparkle","awesome","good","magic"],"k":[6,23]},"flag-np":{"a":"Nepal Flag","b":"1F1F3-1F1F5","k":[3,48]},"congratulations":{"a":"Circled Ideograph Congratulation","b":"3297-FE0F","c":"3297","j":["chinese","kanji","japanese","red-circle"],"k":[50,26],"o":1},"flag-nr":{"a":"Nauru Flag","b":"1F1F3-1F1F7","k":[3,49]},"stars":{"a":"Shooting Star","b":"1F320","j":["night","photo"],"k":[6,24]},"female_mage":{"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2640-FE0F","non_qualified":"1F9D9-1F3FB-200D-2640","image":"1f9d9-1f3fb-200d-2640-fe0f.png","sheet_x":44,"sheet_y":31,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D9-1F3FB"},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2640-FE0F","non_qualified":"1F9D9-1F3FC-200D-2640","image":"1f9d9-1f3fc-200d-2640-fe0f.png","sheet_x":44,"sheet_y":32,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D9-1F3FC"},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2640-FE0F","non_qualified":"1F9D9-1F3FD-200D-2640","image":"1f9d9-1f3fd-200d-2640-fe0f.png","sheet_x":44,"sheet_y":33,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D9-1F3FD"},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2640-FE0F","non_qualified":"1F9D9-1F3FE-200D-2640","image":"1f9d9-1f3fe-200d-2640-fe0f.png","sheet_x":44,"sheet_y":34,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D9-1F3FE"},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2640-FE0F","non_qualified":"1F9D9-1F3FF-200D-2640","image":"1f9d9-1f3ff-200d-2640-fe0f.png","sheet_x":44,"sheet_y":35,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D9-1F3FF"}},"obsoletes":"1F9D9","a":"Female Mage","b":"1F9D9-200D-2640-FE0F","c":"1F9D9-200D-2640","k":[44,30],"o":10},"secret":{"a":"Circled Ideograph Secret","b":"3299-FE0F","c":"3299","j":["privacy","chinese","sshh","kanji","red-circle"],"k":[50,27],"o":1},"flag-nu":{"a":"Niue Flag","b":"1F1F3-1F1FA","k":[3,50]},"u55b6":{"a":"Squared Cjk Unified Ideograph-55b6","b":"1F23A","j":["japanese","opening hours","orange-square"],"k":[5,41]},"male_mage":{"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2642-FE0F","non_qualified":"1F9D9-1F3FB-200D-2642","image":"1f9d9-1f3fb-200d-2642-fe0f.png","sheet_x":44,"sheet_y":37,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2642-FE0F","non_qualified":"1F9D9-1F3FC-200D-2642","image":"1f9d9-1f3fc-200d-2642-fe0f.png","sheet_x":44,"sheet_y":38,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2642-FE0F","non_qualified":"1F9D9-1F3FD-200D-2642","image":"1f9d9-1f3fd-200d-2642-fe0f.png","sheet_x":44,"sheet_y":39,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2642-FE0F","non_qualified":"1F9D9-1F3FE-200D-2642","image":"1f9d9-1f3fe-200d-2642-fe0f.png","sheet_x":44,"sheet_y":40,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2642-FE0F","non_qualified":"1F9D9-1F3FF-200D-2642","image":"1f9d9-1f3ff-200d-2642-fe0f.png","sheet_x":44,"sheet_y":41,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Male Mage","b":"1F9D9-200D-2642-FE0F","c":"1F9D9-200D-2642","k":[44,36],"o":10},"cloud":{"a":"Cloud","b":"2601-FE0F","c":"2601","j":["weather","sky"],"k":[47,17],"o":1},"flag-nz":{"a":"New Zealand Flag","b":"1F1F3-1F1FF","k":[3,51]},"partly_sunny":{"a":"Sun Behind Cloud","b":"26C5","j":["weather","nature","cloudy","morning","fall","spring"],"k":[48,29],"o":5},"fairy":{"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB","non_qualified":null,"image":"1f9da-1f3fb.png","sheet_x":45,"sheet_y":9,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DA-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9DA-1F3FC","non_qualified":null,"image":"1f9da-1f3fc.png","sheet_x":45,"sheet_y":10,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DA-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9DA-1F3FD","non_qualified":null,"image":"1f9da-1f3fd.png","sheet_x":45,"sheet_y":11,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DA-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9DA-1F3FE","non_qualified":null,"image":"1f9da-1f3fe.png","sheet_x":45,"sheet_y":12,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DA-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9DA-1F3FF","non_qualified":null,"image":"1f9da-1f3ff.png","sheet_x":45,"sheet_y":13,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DA-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9DA-200D-2640-FE0F","a":"Fairy","b":"1F9DA","k":[45,8],"o":10},"u6e80":{"a":"Squared Cjk Unified Ideograph-6e80","b":"1F235","j":["full","chinese","japanese","red-square","kanji"],"k":[5,36]},"black_small_square":{"a":"Black Small Square","b":"25AA-FE0F","c":"25AA","j":["shape","icon"],"k":[47,8],"o":1},"thunder_cloud_and_rain":{"a":"Thunder Cloud and Rain","b":"26C8-FE0F","c":"26C8","k":[48,30],"o":5},"female_fairy":{"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2640-FE0F","non_qualified":"1F9DA-1F3FB-200D-2640","image":"1f9da-1f3fb-200d-2640-fe0f.png","sheet_x":44,"sheet_y":49,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DA-1F3FB"},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2640-FE0F","non_qualified":"1F9DA-1F3FC-200D-2640","image":"1f9da-1f3fc-200d-2640-fe0f.png","sheet_x":44,"sheet_y":50,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DA-1F3FC"},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2640-FE0F","non_qualified":"1F9DA-1F3FD-200D-2640","image":"1f9da-1f3fd-200d-2640-fe0f.png","sheet_x":44,"sheet_y":51,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DA-1F3FD"},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2640-FE0F","non_qualified":"1F9DA-1F3FE-200D-2640","image":"1f9da-1f3fe-200d-2640-fe0f.png","sheet_x":45,"sheet_y":0,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DA-1F3FE"},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2640-FE0F","non_qualified":"1F9DA-1F3FF-200D-2640","image":"1f9da-1f3ff-200d-2640-fe0f.png","sheet_x":45,"sheet_y":1,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DA-1F3FF"}},"obsoletes":"1F9DA","a":"Female Fairy","b":"1F9DA-200D-2640-FE0F","c":"1F9DA-200D-2640","k":[44,48],"o":10},"flag-om":{"a":"Oman Flag","b":"1F1F4-1F1F2","k":[4,0]},"white_small_square":{"a":"White Small Square","b":"25AB-FE0F","c":"25AB","j":["shape","icon"],"k":[47,9],"o":1},"flag-pa":{"a":"Panama Flag","b":"1F1F5-1F1E6","k":[4,1]},"mostly_sunny":{"a":"Mostly Sunny","b":"1F324-FE0F","c":"1F324","k":[6,26],"n":["sun_small_cloud"],"o":7},"male_fairy":{"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2642-FE0F","non_qualified":"1F9DA-1F3FB-200D-2642","image":"1f9da-1f3fb-200d-2642-fe0f.png","sheet_x":45,"sheet_y":3,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2642-FE0F","non_qualified":"1F9DA-1F3FC-200D-2642","image":"1f9da-1f3fc-200d-2642-fe0f.png","sheet_x":45,"sheet_y":4,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2642-FE0F","non_qualified":"1F9DA-1F3FD-200D-2642","image":"1f9da-1f3fd-200d-2642-fe0f.png","sheet_x":45,"sheet_y":5,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2642-FE0F","non_qualified":"1F9DA-1F3FE-200D-2642","image":"1f9da-1f3fe-200d-2642-fe0f.png","sheet_x":45,"sheet_y":6,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2642-FE0F","non_qualified":"1F9DA-1F3FF-200D-2642","image":"1f9da-1f3ff-200d-2642-fe0f.png","sheet_x":45,"sheet_y":7,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Male Fairy","b":"1F9DA-200D-2642-FE0F","c":"1F9DA-200D-2642","k":[45,2],"o":10},"barely_sunny":{"a":"Barely Sunny","b":"1F325-FE0F","c":"1F325","k":[6,27],"n":["sun_behind_cloud"],"o":7},"white_medium_square":{"a":"White Medium Square","b":"25FB-FE0F","c":"25FB","j":["shape","stone","icon"],"k":[47,12],"o":3},"flag-pe":{"a":"Peru Flag","b":"1F1F5-1F1EA","k":[4,2]},"vampire":{"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB","non_qualified":null,"image":"1f9db-1f3fb.png","sheet_x":45,"sheet_y":27,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DB-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9DB-1F3FC","non_qualified":null,"image":"1f9db-1f3fc.png","sheet_x":45,"sheet_y":28,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DB-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9DB-1F3FD","non_qualified":null,"image":"1f9db-1f3fd.png","sheet_x":45,"sheet_y":29,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DB-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9DB-1F3FE","non_qualified":null,"image":"1f9db-1f3fe.png","sheet_x":45,"sheet_y":30,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DB-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9DB-1F3FF","non_qualified":null,"image":"1f9db-1f3ff.png","sheet_x":45,"sheet_y":31,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoleted_by":"1F9DB-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9DB-200D-2640-FE0F","a":"Vampire","b":"1F9DB","k":[45,26],"o":10},"female_vampire":{"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2640-FE0F","non_qualified":"1F9DB-1F3FB-200D-2640","image":"1f9db-1f3fb-200d-2640-fe0f.png","sheet_x":45,"sheet_y":15,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DB-1F3FB"},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2640-FE0F","non_qualified":"1F9DB-1F3FC-200D-2640","image":"1f9db-1f3fc-200d-2640-fe0f.png","sheet_x":45,"sheet_y":16,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DB-1F3FC"},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2640-FE0F","non_qualified":"1F9DB-1F3FD-200D-2640","image":"1f9db-1f3fd-200d-2640-fe0f.png","sheet_x":45,"sheet_y":17,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DB-1F3FD"},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2640-FE0F","non_qualified":"1F9DB-1F3FE-200D-2640","image":"1f9db-1f3fe-200d-2640-fe0f.png","sheet_x":45,"sheet_y":18,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DB-1F3FE"},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2640-FE0F","non_qualified":"1F9DB-1F3FF-200D-2640","image":"1f9db-1f3ff-200d-2640-fe0f.png","sheet_x":45,"sheet_y":19,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DB-1F3FF"}},"obsoletes":"1F9DB","a":"Female Vampire","b":"1F9DB-200D-2640-FE0F","c":"1F9DB-200D-2640","k":[45,14],"o":10},"partly_sunny_rain":{"a":"Partly Sunny Rain","b":"1F326-FE0F","c":"1F326","k":[6,28],"n":["sun_behind_rain_cloud"],"o":7},"flag-pf":{"a":"French Polynesia Flag","b":"1F1F5-1F1EB","k":[4,3]},"black_medium_square":{"a":"Black Medium Square","b":"25FC-FE0F","c":"25FC","j":["shape","button","icon"],"k":[47,13],"o":3},"white_medium_small_square":{"a":"White Medium Small Square","b":"25FD","j":["shape","stone","icon","button"],"k":[47,14],"o":3},"rain_cloud":{"a":"Rain Cloud","b":"1F327-FE0F","c":"1F327","k":[6,29],"o":7},"flag-pg":{"a":"Papua New Guinea Flag","b":"1F1F5-1F1EC","k":[4,4]},"male_vampire":{"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2642-FE0F","non_qualified":"1F9DB-1F3FB-200D-2642","image":"1f9db-1f3fb-200d-2642-fe0f.png","sheet_x":45,"sheet_y":21,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2642-FE0F","non_qualified":"1F9DB-1F3FC-200D-2642","image":"1f9db-1f3fc-200d-2642-fe0f.png","sheet_x":45,"sheet_y":22,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2642-FE0F","non_qualified":"1F9DB-1F3FD-200D-2642","image":"1f9db-1f3fd-200d-2642-fe0f.png","sheet_x":45,"sheet_y":23,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2642-FE0F","non_qualified":"1F9DB-1F3FE-200D-2642","image":"1f9db-1f3fe-200d-2642-fe0f.png","sheet_x":45,"sheet_y":24,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2642-FE0F","non_qualified":"1F9DB-1F3FF-200D-2642","image":"1f9db-1f3ff-200d-2642-fe0f.png","sheet_x":45,"sheet_y":25,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Male Vampire","b":"1F9DB-200D-2642-FE0F","c":"1F9DB-200D-2642","k":[45,20],"o":10},"flag-ph":{"a":"Philippines Flag","b":"1F1F5-1F1ED","k":[4,5]},"merperson":{"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB","non_qualified":null,"image":"1f9dc-1f3fb.png","sheet_x":45,"sheet_y":45,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DC-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9DC-1F3FC","non_qualified":null,"image":"1f9dc-1f3fc.png","sheet_x":45,"sheet_y":46,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DC-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9DC-1F3FD","non_qualified":null,"image":"1f9dc-1f3fd.png","sheet_x":45,"sheet_y":47,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DC-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9DC-1F3FE","non_qualified":null,"image":"1f9dc-1f3fe.png","sheet_x":45,"sheet_y":48,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DC-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9DC-1F3FF","non_qualified":null,"image":"1f9dc-1f3ff.png","sheet_x":45,"sheet_y":49,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DC-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9DC-200D-2642-FE0F","a":"Merperson","b":"1F9DC","k":[45,44],"o":10},"black_medium_small_square":{"a":"Black Medium Small Square","b":"25FE","j":["icon","shape","button"],"k":[47,15],"o":3},"snow_cloud":{"a":"Snow Cloud","b":"1F328-FE0F","c":"1F328","k":[6,30],"o":7},"lightning":{"a":"Lightning","b":"1F329-FE0F","c":"1F329","k":[6,31],"n":["lightning_cloud"],"o":7},"black_large_square":{"a":"Black Large Square","b":"2B1B","j":["shape","icon","button"],"k":[50,20],"o":5},"mermaid":{"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2640-FE0F","non_qualified":"1F9DC-1F3FB-200D-2640","image":"1f9dc-1f3fb-200d-2640-fe0f.png","sheet_x":45,"sheet_y":33,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2640-FE0F","non_qualified":"1F9DC-1F3FC-200D-2640","image":"1f9dc-1f3fc-200d-2640-fe0f.png","sheet_x":45,"sheet_y":34,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2640-FE0F","non_qualified":"1F9DC-1F3FD-200D-2640","image":"1f9dc-1f3fd-200d-2640-fe0f.png","sheet_x":45,"sheet_y":35,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2640-FE0F","non_qualified":"1F9DC-1F3FE-200D-2640","image":"1f9dc-1f3fe-200d-2640-fe0f.png","sheet_x":45,"sheet_y":36,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2640-FE0F","non_qualified":"1F9DC-1F3FF-200D-2640","image":"1f9dc-1f3ff-200d-2640-fe0f.png","sheet_x":45,"sheet_y":37,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Mermaid","b":"1F9DC-200D-2640-FE0F","c":"1F9DC-200D-2640","k":[45,32],"o":10},"flag-pk":{"a":"Pakistan Flag","b":"1F1F5-1F1F0","k":[4,6]},"merman":{"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2642-FE0F","non_qualified":"1F9DC-1F3FB-200D-2642","image":"1f9dc-1f3fb-200d-2642-fe0f.png","sheet_x":45,"sheet_y":39,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DC-1F3FB"},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2642-FE0F","non_qualified":"1F9DC-1F3FC-200D-2642","image":"1f9dc-1f3fc-200d-2642-fe0f.png","sheet_x":45,"sheet_y":40,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DC-1F3FC"},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2642-FE0F","non_qualified":"1F9DC-1F3FD-200D-2642","image":"1f9dc-1f3fd-200d-2642-fe0f.png","sheet_x":45,"sheet_y":41,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DC-1F3FD"},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2642-FE0F","non_qualified":"1F9DC-1F3FE-200D-2642","image":"1f9dc-1f3fe-200d-2642-fe0f.png","sheet_x":45,"sheet_y":42,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DC-1F3FE"},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2642-FE0F","non_qualified":"1F9DC-1F3FF-200D-2642","image":"1f9dc-1f3ff-200d-2642-fe0f.png","sheet_x":45,"sheet_y":43,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DC-1F3FF"}},"obsoletes":"1F9DC","a":"Merman","b":"1F9DC-200D-2642-FE0F","c":"1F9DC-200D-2642","k":[45,38],"o":10},"white_large_square":{"a":"White Large Square","b":"2B1C","j":["shape","icon","stone","button"],"k":[50,21],"o":5},"tornado":{"a":"Tornado","b":"1F32A-FE0F","c":"1F32A","j":["weather","cyclone","twister"],"k":[6,32],"n":["tornado_cloud"],"o":7},"flag-pl":{"a":"Poland Flag","b":"1F1F5-1F1F1","k":[4,7]},"elf":{"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB","non_qualified":null,"image":"1f9dd-1f3fb.png","sheet_x":46,"sheet_y":11,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DD-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9DD-1F3FC","non_qualified":null,"image":"1f9dd-1f3fc.png","sheet_x":46,"sheet_y":12,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DD-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9DD-1F3FD","non_qualified":null,"image":"1f9dd-1f3fd.png","sheet_x":46,"sheet_y":13,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DD-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9DD-1F3FE","non_qualified":null,"image":"1f9dd-1f3fe.png","sheet_x":46,"sheet_y":14,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DD-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9DD-1F3FF","non_qualified":null,"image":"1f9dd-1f3ff.png","sheet_x":46,"sheet_y":15,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9DD-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9DD-200D-2642-FE0F","a":"Elf","b":"1F9DD","k":[46,10],"o":10},"fog":{"a":"Fog","b":"1F32B-FE0F","c":"1F32B","j":["weather"],"k":[6,33],"o":7},"large_orange_diamond":{"a":"Large Orange Diamond","b":"1F536","j":["shape","jewel","gem"],"k":[28,4]},"flag-pn":{"a":"Pitcairn Islands Flag","b":"1F1F5-1F1F3","k":[4,9]},"wind_blowing_face":{"a":"Wind Blowing Face","b":"1F32C-FE0F","c":"1F32C","k":[6,34],"o":7},"female_elf":{"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2640-FE0F","non_qualified":"1F9DD-1F3FB-200D-2640","image":"1f9dd-1f3fb-200d-2640-fe0f.png","sheet_x":45,"sheet_y":51,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2640-FE0F","non_qualified":"1F9DD-1F3FC-200D-2640","image":"1f9dd-1f3fc-200d-2640-fe0f.png","sheet_x":46,"sheet_y":0,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2640-FE0F","non_qualified":"1F9DD-1F3FD-200D-2640","image":"1f9dd-1f3fd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":1,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2640-FE0F","non_qualified":"1F9DD-1F3FE-200D-2640","image":"1f9dd-1f3fe-200d-2640-fe0f.png","sheet_x":46,"sheet_y":2,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2640-FE0F","non_qualified":"1F9DD-1F3FF-200D-2640","image":"1f9dd-1f3ff-200d-2640-fe0f.png","sheet_x":46,"sheet_y":3,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Female Elf","b":"1F9DD-200D-2640-FE0F","c":"1F9DD-200D-2640","k":[45,50],"o":10},"large_blue_diamond":{"a":"Large Blue Diamond","b":"1F537","j":["shape","jewel","gem"],"k":[28,5]},"male_elf":{"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2642-FE0F","non_qualified":"1F9DD-1F3FB-200D-2642","image":"1f9dd-1f3fb-200d-2642-fe0f.png","sheet_x":46,"sheet_y":5,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DD-1F3FB"},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2642-FE0F","non_qualified":"1F9DD-1F3FC-200D-2642","image":"1f9dd-1f3fc-200d-2642-fe0f.png","sheet_x":46,"sheet_y":6,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DD-1F3FC"},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2642-FE0F","non_qualified":"1F9DD-1F3FD-200D-2642","image":"1f9dd-1f3fd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":7,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DD-1F3FD"},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2642-FE0F","non_qualified":"1F9DD-1F3FE-200D-2642","image":"1f9dd-1f3fe-200d-2642-fe0f.png","sheet_x":46,"sheet_y":8,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DD-1F3FE"},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2642-FE0F","non_qualified":"1F9DD-1F3FF-200D-2642","image":"1f9dd-1f3ff-200d-2642-fe0f.png","sheet_x":46,"sheet_y":9,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9DD-1F3FF"}},"obsoletes":"1F9DD","a":"Male Elf","b":"1F9DD-200D-2642-FE0F","c":"1F9DD-200D-2642","k":[46,4],"o":10},"small_orange_diamond":{"a":"Small Orange Diamond","b":"1F538","j":["shape","jewel","gem"],"k":[28,6]},"flag-pr":{"a":"Puerto Rico Flag","b":"1F1F5-1F1F7","k":[4,10]},"cyclone":{"a":"Cyclone","b":"1F300","j":["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],"k":[5,44]},"rainbow":{"a":"Rainbow","b":"1F308","j":["nature","happy","unicorn_face","photo","sky","spring"],"k":[6,0]},"small_blue_diamond":{"a":"Small Blue Diamond","b":"1F539","j":["shape","jewel","gem"],"k":[28,7]},"genie":{"obsoleted_by":"1F9DE-200D-2642-FE0F","a":"Genie","b":"1F9DE","k":[46,18],"o":10},"flag-ps":{"a":"Palestinian Territories Flag","b":"1F1F5-1F1F8","k":[4,11]},"small_red_triangle":{"a":"Up-Pointing Red Triangle","b":"1F53A","j":["shape","direction","up","top"],"k":[28,8]},"closed_umbrella":{"a":"Closed Umbrella","b":"1F302","j":["weather","rain","drizzle"],"k":[5,46]},"female_genie":{"a":"Female Genie","b":"1F9DE-200D-2640-FE0F","c":"1F9DE-200D-2640","k":[46,16],"o":10},"flag-pt":{"a":"Portugal Flag","b":"1F1F5-1F1F9","k":[4,12]},"flag-pw":{"a":"Palau Flag","b":"1F1F5-1F1FC","k":[4,13]},"small_red_triangle_down":{"a":"Down-Pointing Red Triangle","b":"1F53B","j":["shape","direction","bottom"],"k":[28,9]},"umbrella":{"a":"Umbrella","b":"2602-FE0F","c":"2602","j":["rainy","weather","spring"],"k":[47,18],"o":1},"male_genie":{"obsoletes":"1F9DE","a":"Male Genie","b":"1F9DE-200D-2642-FE0F","c":"1F9DE-200D-2642","k":[46,17],"o":10},"zombie":{"obsoleted_by":"1F9DF-200D-2642-FE0F","a":"Zombie","b":"1F9DF","k":[46,21],"o":10},"flag-py":{"a":"Paraguay Flag","b":"1F1F5-1F1FE","k":[4,14]},"diamond_shape_with_a_dot_inside":{"a":"Diamond Shape with a Dot Inside","b":"1F4A0","j":["jewel","blue","gem","crystal","fancy"],"k":[25,6]},"umbrella_with_rain_drops":{"a":"Umbrella with Rain Drops","b":"2614","k":[47,23],"o":4},"radio_button":{"a":"Radio Button","b":"1F518","j":["input","old","music","circle"],"k":[27,26]},"female_zombie":{"a":"Female Zombie","b":"1F9DF-200D-2640-FE0F","c":"1F9DF-200D-2640","k":[46,19],"o":10},"flag-qa":{"a":"Qatar Flag","b":"1F1F6-1F1E6","k":[4,15]},"umbrella_on_ground":{"a":"Umbrella on Ground","b":"26F1-FE0F","c":"26F1","k":[48,39],"o":5},"black_square_button":{"a":"Black Square Button","b":"1F532","j":["shape","input","frame"],"k":[28,0]},"zap":{"a":"High Voltage Sign","b":"26A1","j":["thunder","weather","lightning bolt","fast"],"k":[48,21],"o":4},"male_zombie":{"obsoletes":"1F9DF","a":"Male Zombie","b":"1F9DF-200D-2642-FE0F","c":"1F9DF-200D-2642","k":[46,20],"o":10},"flag-ro":{"a":"Romania Flag","b":"1F1F7-1F1F4","k":[4,17]},"snowflake":{"a":"Snowflake","b":"2744-FE0F","c":"2744","j":["winter","season","cold","weather","christmas","xmas"],"k":[49,51],"o":1},"white_square_button":{"a":"White Square Button","b":"1F533","j":["shape","input"],"k":[28,1]},"person_frowning":{"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB","non_qualified":null,"image":"1f64d-1f3fb.png","sheet_x":33,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F64D-1F3FC","non_qualified":null,"image":"1f64d-1f3fc.png","sheet_x":33,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F64D-1F3FD","non_qualified":null,"image":"1f64d-1f3fd.png","sheet_x":33,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F64D-1F3FE","non_qualified":null,"image":"1f64d-1f3fe.png","sheet_x":33,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F64D-1F3FF","non_qualified":null,"image":"1f64d-1f3ff.png","sheet_x":33,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F64D-200D-2640-FE0F","a":"Person Frowning","b":"1F64D","k":[33,30]},"flag-rs":{"a":"Serbia Flag","b":"1F1F7-1F1F8","k":[4,18]},"man-frowning":{"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2642-FE0F","non_qualified":"1F64D-1F3FB-200D-2642","image":"1f64d-1f3fb-200d-2642-fe0f.png","sheet_x":33,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F64D-1F3FC-200D-2642-FE0F","non_qualified":"1F64D-1F3FC-200D-2642","image":"1f64d-1f3fc-200d-2642-fe0f.png","sheet_x":33,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F64D-1F3FD-200D-2642-FE0F","non_qualified":"1F64D-1F3FD-200D-2642","image":"1f64d-1f3fd-200d-2642-fe0f.png","sheet_x":33,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F64D-1F3FE-200D-2642-FE0F","non_qualified":"1F64D-1F3FE-200D-2642","image":"1f64d-1f3fe-200d-2642-fe0f.png","sheet_x":33,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F64D-1F3FF-200D-2642-FE0F","non_qualified":"1F64D-1F3FF-200D-2642","image":"1f64d-1f3ff-200d-2642-fe0f.png","sheet_x":33,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Frowning","b":"1F64D-200D-2642-FE0F","c":"1F64D-200D-2642","k":[33,24]},"white_circle":{"a":"Medium White Circle","b":"26AA","j":["shape","round"],"k":[48,22],"o":4},"snowman":{"a":"Snowman","b":"2603-FE0F","c":"2603","j":["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],"k":[47,19],"o":1},"snowman_without_snow":{"a":"Snowman Without Snow","b":"26C4","k":[48,28],"o":5},"ru":{"a":"Russia Flag","b":"1F1F7-1F1FA","j":["russian","federation","flag","nation","country","banner"],"k":[4,19],"n":["flag-ru"]},"black_circle":{"a":"Medium Black Circle","b":"26AB","j":["shape","button","round"],"k":[48,23],"o":4},"woman-frowning":{"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2640-FE0F","non_qualified":"1F64D-1F3FB-200D-2640","image":"1f64d-1f3fb-200d-2640-fe0f.png","sheet_x":33,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F64D-1F3FC-200D-2640-FE0F","non_qualified":"1F64D-1F3FC-200D-2640","image":"1f64d-1f3fc-200d-2640-fe0f.png","sheet_x":33,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F64D-1F3FD-200D-2640-FE0F","non_qualified":"1F64D-1F3FD-200D-2640","image":"1f64d-1f3fd-200d-2640-fe0f.png","sheet_x":33,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F64D-1F3FE-200D-2640-FE0F","non_qualified":"1F64D-1F3FE-200D-2640","image":"1f64d-1f3fe-200d-2640-fe0f.png","sheet_x":33,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F64D-1F3FF-200D-2640-FE0F","non_qualified":"1F64D-1F3FF-200D-2640","image":"1f64d-1f3ff-200d-2640-fe0f.png","sheet_x":33,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F64D","a":"Woman Frowning","b":"1F64D-200D-2640-FE0F","c":"1F64D-200D-2640","k":[33,18]},"flag-rw":{"a":"Rwanda Flag","b":"1F1F7-1F1FC","k":[4,20]},"comet":{"a":"Comet","b":"2604-FE0F","c":"2604","j":["space"],"k":[47,20],"o":1},"person_with_pouting_face":{"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB","non_qualified":null,"image":"1f64e-1f3fb.png","sheet_x":33,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F64E-1F3FC","non_qualified":null,"image":"1f64e-1f3fc.png","sheet_x":33,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F64E-1F3FD","non_qualified":null,"image":"1f64e-1f3fd.png","sheet_x":33,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F64E-1F3FE","non_qualified":null,"image":"1f64e-1f3fe.png","sheet_x":34,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F64E-1F3FF","non_qualified":null,"image":"1f64e-1f3ff.png","sheet_x":34,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F64E-200D-2640-FE0F","a":"Person with Pouting Face","b":"1F64E","k":[33,48]},"red_circle":{"a":"Large Red Circle","b":"1F534","j":["shape","error","danger"],"k":[28,2]},"large_blue_circle":{"a":"Large Blue Circle","b":"1F535","j":["shape","icon","button"],"k":[28,3]},"man-pouting":{"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2642-FE0F","non_qualified":"1F64E-1F3FB-200D-2642","image":"1f64e-1f3fb-200d-2642-fe0f.png","sheet_x":33,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F64E-1F3FC-200D-2642-FE0F","non_qualified":"1F64E-1F3FC-200D-2642","image":"1f64e-1f3fc-200d-2642-fe0f.png","sheet_x":33,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F64E-1F3FD-200D-2642-FE0F","non_qualified":"1F64E-1F3FD-200D-2642","image":"1f64e-1f3fd-200d-2642-fe0f.png","sheet_x":33,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F64E-1F3FE-200D-2642-FE0F","non_qualified":"1F64E-1F3FE-200D-2642","image":"1f64e-1f3fe-200d-2642-fe0f.png","sheet_x":33,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F64E-1F3FF-200D-2642-FE0F","non_qualified":"1F64E-1F3FF-200D-2642","image":"1f64e-1f3ff-200d-2642-fe0f.png","sheet_x":33,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Pouting","b":"1F64E-200D-2642-FE0F","c":"1F64E-200D-2642","k":[33,42]},"flag-sa":{"a":"Saudi Arabia Flag","b":"1F1F8-1F1E6","k":[4,21]},"fire":{"a":"Fire","b":"1F525","j":["hot","cook","flame"],"k":[27,39]},"woman-pouting":{"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2640-FE0F","non_qualified":"1F64E-1F3FB-200D-2640","image":"1f64e-1f3fb-200d-2640-fe0f.png","sheet_x":33,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F64E-1F3FC-200D-2640-FE0F","non_qualified":"1F64E-1F3FC-200D-2640","image":"1f64e-1f3fc-200d-2640-fe0f.png","sheet_x":33,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F64E-1F3FD-200D-2640-FE0F","non_qualified":"1F64E-1F3FD-200D-2640","image":"1f64e-1f3fd-200d-2640-fe0f.png","sheet_x":33,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F64E-1F3FE-200D-2640-FE0F","non_qualified":"1F64E-1F3FE-200D-2640","image":"1f64e-1f3fe-200d-2640-fe0f.png","sheet_x":33,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F64E-1F3FF-200D-2640-FE0F","non_qualified":"1F64E-1F3FF-200D-2640","image":"1f64e-1f3ff-200d-2640-fe0f.png","sheet_x":33,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F64E","a":"Woman Pouting","b":"1F64E-200D-2640-FE0F","c":"1F64E-200D-2640","k":[33,36]},"flag-sb":{"a":"Solomon Islands Flag","b":"1F1F8-1F1E7","k":[4,22]},"droplet":{"a":"Droplet","b":"1F4A7","j":["water","drip","faucet","spring"],"k":[25,13]},"no_good":{"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB","non_qualified":null,"image":"1f645-1f3fb.png","sheet_x":32,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F645-1F3FC","non_qualified":null,"image":"1f645-1f3fc.png","sheet_x":32,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F645-1F3FD","non_qualified":null,"image":"1f645-1f3fd.png","sheet_x":32,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F645-1F3FE","non_qualified":null,"image":"1f645-1f3fe.png","sheet_x":32,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F645-1F3FF","non_qualified":null,"image":"1f645-1f3ff.png","sheet_x":32,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F645-200D-2640-FE0F","a":"Face with No Good Gesture","b":"1F645","k":[32,1]},"flag-sc":{"a":"Seychelles Flag","b":"1F1F8-1F1E8","k":[4,23]},"ocean":{"a":"Water Wave","b":"1F30A","j":["sea","water","wave","nature","tsunami","disaster"],"k":[6,2]},"man-gesturing-no":{"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2642-FE0F","non_qualified":"1F645-1F3FB-200D-2642","image":"1f645-1f3fb-200d-2642-fe0f.png","sheet_x":31,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F645-1F3FC-200D-2642-FE0F","non_qualified":"1F645-1F3FC-200D-2642","image":"1f645-1f3fc-200d-2642-fe0f.png","sheet_x":31,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F645-1F3FD-200D-2642-FE0F","non_qualified":"1F645-1F3FD-200D-2642","image":"1f645-1f3fd-200d-2642-fe0f.png","sheet_x":31,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F645-1F3FE-200D-2642-FE0F","non_qualified":"1F645-1F3FE-200D-2642","image":"1f645-1f3fe-200d-2642-fe0f.png","sheet_x":31,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F645-1F3FF-200D-2642-FE0F","non_qualified":"1F645-1F3FF-200D-2642","image":"1f645-1f3ff-200d-2642-fe0f.png","sheet_x":32,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Gesturing No","b":"1F645-200D-2642-FE0F","c":"1F645-200D-2642","k":[31,47]},"flag-sd":{"a":"Sudan Flag","b":"1F1F8-1F1E9","k":[4,24]},"woman-gesturing-no":{"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2640-FE0F","non_qualified":"1F645-1F3FB-200D-2640","image":"1f645-1f3fb-200d-2640-fe0f.png","sheet_x":31,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F645-1F3FC-200D-2640-FE0F","non_qualified":"1F645-1F3FC-200D-2640","image":"1f645-1f3fc-200d-2640-fe0f.png","sheet_x":31,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F645-1F3FD-200D-2640-FE0F","non_qualified":"1F645-1F3FD-200D-2640","image":"1f645-1f3fd-200d-2640-fe0f.png","sheet_x":31,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F645-1F3FE-200D-2640-FE0F","non_qualified":"1F645-1F3FE-200D-2640","image":"1f645-1f3fe-200d-2640-fe0f.png","sheet_x":31,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F645-1F3FF-200D-2640-FE0F","non_qualified":"1F645-1F3FF-200D-2640","image":"1f645-1f3ff-200d-2640-fe0f.png","sheet_x":31,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F645","a":"Woman Gesturing No","b":"1F645-200D-2640-FE0F","c":"1F645-200D-2640","k":[31,41]},"flag-se":{"a":"Sweden Flag","b":"1F1F8-1F1EA","k":[4,25]},"flag-sg":{"a":"Singapore Flag","b":"1F1F8-1F1EC","k":[4,26]},"ok_woman":{"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB","non_qualified":null,"image":"1f646-1f3fb.png","sheet_x":32,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F646-1F3FC","non_qualified":null,"image":"1f646-1f3fc.png","sheet_x":32,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F646-1F3FD","non_qualified":null,"image":"1f646-1f3fd.png","sheet_x":32,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F646-1F3FE","non_qualified":null,"image":"1f646-1f3fe.png","sheet_x":32,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F646-1F3FF","non_qualified":null,"image":"1f646-1f3ff.png","sheet_x":32,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F646-200D-2640-FE0F","a":"Face with Ok Gesture","b":"1F646","j":["women","girl","female","pink","human","woman"],"k":[32,19]},"flag-sh":{"a":"St. Helena Flag","b":"1F1F8-1F1ED","k":[4,27]},"man-gesturing-ok":{"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2642-FE0F","non_qualified":"1F646-1F3FB-200D-2642","image":"1f646-1f3fb-200d-2642-fe0f.png","sheet_x":32,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F646-1F3FC-200D-2642-FE0F","non_qualified":"1F646-1F3FC-200D-2642","image":"1f646-1f3fc-200d-2642-fe0f.png","sheet_x":32,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F646-1F3FD-200D-2642-FE0F","non_qualified":"1F646-1F3FD-200D-2642","image":"1f646-1f3fd-200d-2642-fe0f.png","sheet_x":32,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F646-1F3FE-200D-2642-FE0F","non_qualified":"1F646-1F3FE-200D-2642","image":"1f646-1f3fe-200d-2642-fe0f.png","sheet_x":32,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F646-1F3FF-200D-2642-FE0F","non_qualified":"1F646-1F3FF-200D-2642","image":"1f646-1f3ff-200d-2642-fe0f.png","sheet_x":32,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Gesturing Ok","b":"1F646-200D-2642-FE0F","c":"1F646-200D-2642","k":[32,13]},"flag-si":{"a":"Slovenia Flag","b":"1F1F8-1F1EE","k":[4,28]},"woman-gesturing-ok":{"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2640-FE0F","non_qualified":"1F646-1F3FB-200D-2640","image":"1f646-1f3fb-200d-2640-fe0f.png","sheet_x":32,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F646-1F3FC-200D-2640-FE0F","non_qualified":"1F646-1F3FC-200D-2640","image":"1f646-1f3fc-200d-2640-fe0f.png","sheet_x":32,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F646-1F3FD-200D-2640-FE0F","non_qualified":"1F646-1F3FD-200D-2640","image":"1f646-1f3fd-200d-2640-fe0f.png","sheet_x":32,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F646-1F3FE-200D-2640-FE0F","non_qualified":"1F646-1F3FE-200D-2640","image":"1f646-1f3fe-200d-2640-fe0f.png","sheet_x":32,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F646-1F3FF-200D-2640-FE0F","non_qualified":"1F646-1F3FF-200D-2640","image":"1f646-1f3ff-200d-2640-fe0f.png","sheet_x":32,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F646","a":"Woman Gesturing Ok","b":"1F646-200D-2640-FE0F","c":"1F646-200D-2640","k":[32,7]},"information_desk_person":{"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB","non_qualified":null,"image":"1f481-1f3fb.png","sheet_x":23,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F481-1F3FC","non_qualified":null,"image":"1f481-1f3fc.png","sheet_x":23,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F481-1F3FD","non_qualified":null,"image":"1f481-1f3fd.png","sheet_x":23,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F481-1F3FE","non_qualified":null,"image":"1f481-1f3fe.png","sheet_x":23,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F481-1F3FF","non_qualified":null,"image":"1f481-1f3ff.png","sheet_x":23,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F481-200D-2640-FE0F","a":"Information Desk Person","b":"1F481","k":[23,13]},"flag-sj":{"a":"Svalbard & Jan Mayen Flag","b":"1F1F8-1F1EF","k":[4,29]},"man-tipping-hand":{"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2642-FE0F","non_qualified":"1F481-1F3FB-200D-2642","image":"1f481-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F481-1F3FC-200D-2642-FE0F","non_qualified":"1F481-1F3FC-200D-2642","image":"1f481-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F481-1F3FD-200D-2642-FE0F","non_qualified":"1F481-1F3FD-200D-2642","image":"1f481-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F481-1F3FE-200D-2642-FE0F","non_qualified":"1F481-1F3FE-200D-2642","image":"1f481-1f3fe-200d-2642-fe0f.png","sheet_x":23,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F481-1F3FF-200D-2642-FE0F","non_qualified":"1F481-1F3FF-200D-2642","image":"1f481-1f3ff-200d-2642-fe0f.png","sheet_x":23,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Tipping Hand","b":"1F481-200D-2642-FE0F","c":"1F481-200D-2642","k":[23,7]},"flag-sk":{"a":"Slovakia Flag","b":"1F1F8-1F1F0","k":[4,30]},"flag-sl":{"a":"Sierra Leone Flag","b":"1F1F8-1F1F1","k":[4,31]},"woman-tipping-hand":{"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2640-FE0F","non_qualified":"1F481-1F3FB-200D-2640","image":"1f481-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F481-1F3FC-200D-2640-FE0F","non_qualified":"1F481-1F3FC-200D-2640","image":"1f481-1f3fc-200d-2640-fe0f.png","sheet_x":23,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F481-1F3FD-200D-2640-FE0F","non_qualified":"1F481-1F3FD-200D-2640","image":"1f481-1f3fd-200d-2640-fe0f.png","sheet_x":23,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F481-1F3FE-200D-2640-FE0F","non_qualified":"1F481-1F3FE-200D-2640","image":"1f481-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F481-1F3FF-200D-2640-FE0F","non_qualified":"1F481-1F3FF-200D-2640","image":"1f481-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F481","a":"Woman Tipping Hand","b":"1F481-200D-2640-FE0F","c":"1F481-200D-2640","k":[23,1]},"flag-sm":{"a":"San Marino Flag","b":"1F1F8-1F1F2","k":[4,32]},"raising_hand":{"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB","non_qualified":null,"image":"1f64b-1f3fb.png","sheet_x":33,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F64B-1F3FC","non_qualified":null,"image":"1f64b-1f3fc.png","sheet_x":33,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F64B-1F3FD","non_qualified":null,"image":"1f64b-1f3fd.png","sheet_x":33,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F64B-1F3FE","non_qualified":null,"image":"1f64b-1f3fe.png","sheet_x":33,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F64B-1F3FF","non_qualified":null,"image":"1f64b-1f3ff.png","sheet_x":33,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F64B-200D-2640-FE0F","a":"Happy Person Raising One Hand","b":"1F64B","k":[33,6]},"flag-sn":{"a":"Senegal Flag","b":"1F1F8-1F1F3","k":[4,33]},"man-raising-hand":{"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2642-FE0F","non_qualified":"1F64B-1F3FB-200D-2642","image":"1f64b-1f3fb-200d-2642-fe0f.png","sheet_x":33,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F64B-1F3FC-200D-2642-FE0F","non_qualified":"1F64B-1F3FC-200D-2642","image":"1f64b-1f3fc-200d-2642-fe0f.png","sheet_x":33,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F64B-1F3FD-200D-2642-FE0F","non_qualified":"1F64B-1F3FD-200D-2642","image":"1f64b-1f3fd-200d-2642-fe0f.png","sheet_x":33,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F64B-1F3FE-200D-2642-FE0F","non_qualified":"1F64B-1F3FE-200D-2642","image":"1f64b-1f3fe-200d-2642-fe0f.png","sheet_x":33,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F64B-1F3FF-200D-2642-FE0F","non_qualified":"1F64B-1F3FF-200D-2642","image":"1f64b-1f3ff-200d-2642-fe0f.png","sheet_x":33,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Raising Hand","b":"1F64B-200D-2642-FE0F","c":"1F64B-200D-2642","k":[33,0]},"flag-so":{"a":"Somalia Flag","b":"1F1F8-1F1F4","k":[4,34]},"woman-raising-hand":{"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2640-FE0F","non_qualified":"1F64B-1F3FB-200D-2640","image":"1f64b-1f3fb-200d-2640-fe0f.png","sheet_x":32,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F64B-1F3FC-200D-2640-FE0F","non_qualified":"1F64B-1F3FC-200D-2640","image":"1f64b-1f3fc-200d-2640-fe0f.png","sheet_x":32,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F64B-1F3FD-200D-2640-FE0F","non_qualified":"1F64B-1F3FD-200D-2640","image":"1f64b-1f3fd-200d-2640-fe0f.png","sheet_x":32,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F64B-1F3FE-200D-2640-FE0F","non_qualified":"1F64B-1F3FE-200D-2640","image":"1f64b-1f3fe-200d-2640-fe0f.png","sheet_x":32,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F64B-1F3FF-200D-2640-FE0F","non_qualified":"1F64B-1F3FF-200D-2640","image":"1f64b-1f3ff-200d-2640-fe0f.png","sheet_x":32,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F64B","a":"Woman Raising Hand","b":"1F64B-200D-2640-FE0F","c":"1F64B-200D-2640","k":[32,46]},"flag-sr":{"a":"Suriname Flag","b":"1F1F8-1F1F7","k":[4,35]},"bow":{"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB","non_qualified":null,"image":"1f647-1f3fb.png","sheet_x":32,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F647-1F3FC","non_qualified":null,"image":"1f647-1f3fc.png","sheet_x":32,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F647-1F3FD","non_qualified":null,"image":"1f647-1f3fd.png","sheet_x":32,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F647-1F3FE","non_qualified":null,"image":"1f647-1f3fe.png","sheet_x":32,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F647-1F3FF","non_qualified":null,"image":"1f647-1f3ff.png","sheet_x":32,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F647-200D-2642-FE0F","a":"Person Bowing Deeply","b":"1F647","k":[32,37]},"man-bowing":{"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2642-FE0F","non_qualified":"1F647-1F3FB-200D-2642","image":"1f647-1f3fb-200d-2642-fe0f.png","sheet_x":32,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F647-1F3FC-200D-2642-FE0F","non_qualified":"1F647-1F3FC-200D-2642","image":"1f647-1f3fc-200d-2642-fe0f.png","sheet_x":32,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F647-1F3FD-200D-2642-FE0F","non_qualified":"1F647-1F3FD-200D-2642","image":"1f647-1f3fd-200d-2642-fe0f.png","sheet_x":32,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F647-1F3FE-200D-2642-FE0F","non_qualified":"1F647-1F3FE-200D-2642","image":"1f647-1f3fe-200d-2642-fe0f.png","sheet_x":32,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F647-1F3FF-200D-2642-FE0F","non_qualified":"1F647-1F3FF-200D-2642","image":"1f647-1f3ff-200d-2642-fe0f.png","sheet_x":32,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F647","a":"Man Bowing","b":"1F647-200D-2642-FE0F","c":"1F647-200D-2642","k":[32,31]},"flag-ss":{"a":"South Sudan Flag","b":"1F1F8-1F1F8","k":[4,36]},"woman-bowing":{"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2640-FE0F","non_qualified":"1F647-1F3FB-200D-2640","image":"1f647-1f3fb-200d-2640-fe0f.png","sheet_x":32,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F647-1F3FC-200D-2640-FE0F","non_qualified":"1F647-1F3FC-200D-2640","image":"1f647-1f3fc-200d-2640-fe0f.png","sheet_x":32,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F647-1F3FD-200D-2640-FE0F","non_qualified":"1F647-1F3FD-200D-2640","image":"1f647-1f3fd-200d-2640-fe0f.png","sheet_x":32,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F647-1F3FE-200D-2640-FE0F","non_qualified":"1F647-1F3FE-200D-2640","image":"1f647-1f3fe-200d-2640-fe0f.png","sheet_x":32,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F647-1F3FF-200D-2640-FE0F","non_qualified":"1F647-1F3FF-200D-2640","image":"1f647-1f3ff-200d-2640-fe0f.png","sheet_x":32,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Bowing","b":"1F647-200D-2640-FE0F","c":"1F647-200D-2640","k":[32,25]},"flag-st":{"a":"São Tomé & Príncipe Flag","b":"1F1F8-1F1F9","k":[4,37]},"face_palm":{"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB","non_qualified":null,"image":"1f926-1f3fb.png","sheet_x":38,"sheet_y":42,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F926-1F3FC","non_qualified":null,"image":"1f926-1f3fc.png","sheet_x":38,"sheet_y":43,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F926-1F3FD","non_qualified":null,"image":"1f926-1f3fd.png","sheet_x":38,"sheet_y":44,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F926-1F3FE","non_qualified":null,"image":"1f926-1f3fe.png","sheet_x":38,"sheet_y":45,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F926-1F3FF","non_qualified":null,"image":"1f926-1f3ff.png","sheet_x":38,"sheet_y":46,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Face Palm","b":"1F926","k":[38,41],"o":9},"flag-sv":{"a":"El Salvador Flag","b":"1F1F8-1F1FB","k":[4,38]},"man-facepalming":{"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2642-FE0F","non_qualified":"1F926-1F3FB-200D-2642","image":"1f926-1f3fb-200d-2642-fe0f.png","sheet_x":38,"sheet_y":36,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F926-1F3FC-200D-2642-FE0F","non_qualified":"1F926-1F3FC-200D-2642","image":"1f926-1f3fc-200d-2642-fe0f.png","sheet_x":38,"sheet_y":37,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F926-1F3FD-200D-2642-FE0F","non_qualified":"1F926-1F3FD-200D-2642","image":"1f926-1f3fd-200d-2642-fe0f.png","sheet_x":38,"sheet_y":38,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F926-1F3FE-200D-2642-FE0F","non_qualified":"1F926-1F3FE-200D-2642","image":"1f926-1f3fe-200d-2642-fe0f.png","sheet_x":38,"sheet_y":39,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F926-1F3FF-200D-2642-FE0F","non_qualified":"1F926-1F3FF-200D-2642","image":"1f926-1f3ff-200d-2642-fe0f.png","sheet_x":38,"sheet_y":40,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Facepalming","b":"1F926-200D-2642-FE0F","c":"1F926-200D-2642","k":[38,35],"o":9},"flag-sx":{"a":"Sint Maarten Flag","b":"1F1F8-1F1FD","k":[4,39]},"flag-sy":{"a":"Syria Flag","b":"1F1F8-1F1FE","k":[4,40]},"woman-facepalming":{"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2640-FE0F","non_qualified":"1F926-1F3FB-200D-2640","image":"1f926-1f3fb-200d-2640-fe0f.png","sheet_x":38,"sheet_y":30,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F926-1F3FC-200D-2640-FE0F","non_qualified":"1F926-1F3FC-200D-2640","image":"1f926-1f3fc-200d-2640-fe0f.png","sheet_x":38,"sheet_y":31,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F926-1F3FD-200D-2640-FE0F","non_qualified":"1F926-1F3FD-200D-2640","image":"1f926-1f3fd-200d-2640-fe0f.png","sheet_x":38,"sheet_y":32,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F926-1F3FE-200D-2640-FE0F","non_qualified":"1F926-1F3FE-200D-2640","image":"1f926-1f3fe-200d-2640-fe0f.png","sheet_x":38,"sheet_y":33,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F926-1F3FF-200D-2640-FE0F","non_qualified":"1F926-1F3FF-200D-2640","image":"1f926-1f3ff-200d-2640-fe0f.png","sheet_x":38,"sheet_y":34,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Facepalming","b":"1F926-200D-2640-FE0F","c":"1F926-200D-2640","k":[38,29],"o":9},"shrug":{"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB","non_qualified":null,"image":"1f937-1f3fb.png","sheet_x":40,"sheet_y":7,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F937-1F3FC","non_qualified":null,"image":"1f937-1f3fc.png","sheet_x":40,"sheet_y":8,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F937-1F3FD","non_qualified":null,"image":"1f937-1f3fd.png","sheet_x":40,"sheet_y":9,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F937-1F3FE","non_qualified":null,"image":"1f937-1f3fe.png","sheet_x":40,"sheet_y":10,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F937-1F3FF","non_qualified":null,"image":"1f937-1f3ff.png","sheet_x":40,"sheet_y":11,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Shrug","b":"1F937","k":[40,6],"o":9},"flag-sz":{"a":"Swaziland Flag","b":"1F1F8-1F1FF","k":[4,41]},"flag-ta":{"a":"Tristan Da Cunha Flag","b":"1F1F9-1F1E6","k":[4,42]},"man-shrugging":{"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2642-FE0F","non_qualified":"1F937-1F3FB-200D-2642","image":"1f937-1f3fb-200d-2642-fe0f.png","sheet_x":40,"sheet_y":1,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F937-1F3FC-200D-2642-FE0F","non_qualified":"1F937-1F3FC-200D-2642","image":"1f937-1f3fc-200d-2642-fe0f.png","sheet_x":40,"sheet_y":2,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F937-1F3FD-200D-2642-FE0F","non_qualified":"1F937-1F3FD-200D-2642","image":"1f937-1f3fd-200d-2642-fe0f.png","sheet_x":40,"sheet_y":3,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F937-1F3FE-200D-2642-FE0F","non_qualified":"1F937-1F3FE-200D-2642","image":"1f937-1f3fe-200d-2642-fe0f.png","sheet_x":40,"sheet_y":4,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F937-1F3FF-200D-2642-FE0F","non_qualified":"1F937-1F3FF-200D-2642","image":"1f937-1f3ff-200d-2642-fe0f.png","sheet_x":40,"sheet_y":5,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Shrugging","b":"1F937-200D-2642-FE0F","c":"1F937-200D-2642","k":[40,0],"o":9},"woman-shrugging":{"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2640-FE0F","non_qualified":"1F937-1F3FB-200D-2640","image":"1f937-1f3fb-200d-2640-fe0f.png","sheet_x":39,"sheet_y":47,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F937-1F3FC-200D-2640-FE0F","non_qualified":"1F937-1F3FC-200D-2640","image":"1f937-1f3fc-200d-2640-fe0f.png","sheet_x":39,"sheet_y":48,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F937-1F3FD-200D-2640-FE0F","non_qualified":"1F937-1F3FD-200D-2640","image":"1f937-1f3fd-200d-2640-fe0f.png","sheet_x":39,"sheet_y":49,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F937-1F3FE-200D-2640-FE0F","non_qualified":"1F937-1F3FE-200D-2640","image":"1f937-1f3fe-200d-2640-fe0f.png","sheet_x":39,"sheet_y":50,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F937-1F3FF-200D-2640-FE0F","non_qualified":"1F937-1F3FF-200D-2640","image":"1f937-1f3ff-200d-2640-fe0f.png","sheet_x":39,"sheet_y":51,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Shrugging","b":"1F937-200D-2640-FE0F","c":"1F937-200D-2640","k":[39,46],"o":9},"flag-tc":{"a":"Turks & Caicos Islands Flag","b":"1F1F9-1F1E8","k":[4,43]},"massage":{"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB","non_qualified":null,"image":"1f486-1f3fb.png","sheet_x":24,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F486-1F3FC","non_qualified":null,"image":"1f486-1f3fc.png","sheet_x":24,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F486-1F3FD","non_qualified":null,"image":"1f486-1f3fd.png","sheet_x":24,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F486-1F3FE","non_qualified":null,"image":"1f486-1f3fe.png","sheet_x":24,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F486-1F3FF","non_qualified":null,"image":"1f486-1f3ff.png","sheet_x":24,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F486-200D-2640-FE0F","a":"Face Massage","b":"1F486","k":[24,10]},"flag-td":{"a":"Chad Flag","b":"1F1F9-1F1E9","k":[4,44]},"man-getting-massage":{"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2642-FE0F","non_qualified":"1F486-1F3FB-200D-2642","image":"1f486-1f3fb-200d-2642-fe0f.png","sheet_x":24,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F486-1F3FC-200D-2642-FE0F","non_qualified":"1F486-1F3FC-200D-2642","image":"1f486-1f3fc-200d-2642-fe0f.png","sheet_x":24,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F486-1F3FD-200D-2642-FE0F","non_qualified":"1F486-1F3FD-200D-2642","image":"1f486-1f3fd-200d-2642-fe0f.png","sheet_x":24,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F486-1F3FE-200D-2642-FE0F","non_qualified":"1F486-1F3FE-200D-2642","image":"1f486-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F486-1F3FF-200D-2642-FE0F","non_qualified":"1F486-1F3FF-200D-2642","image":"1f486-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Getting Massage","b":"1F486-200D-2642-FE0F","c":"1F486-200D-2642","k":[24,4]},"woman-getting-massage":{"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2640-FE0F","non_qualified":"1F486-1F3FB-200D-2640","image":"1f486-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F486-1F3FC-200D-2640-FE0F","non_qualified":"1F486-1F3FC-200D-2640","image":"1f486-1f3fc-200d-2640-fe0f.png","sheet_x":24,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F486-1F3FD-200D-2640-FE0F","non_qualified":"1F486-1F3FD-200D-2640","image":"1f486-1f3fd-200d-2640-fe0f.png","sheet_x":24,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F486-1F3FE-200D-2640-FE0F","non_qualified":"1F486-1F3FE-200D-2640","image":"1f486-1f3fe-200d-2640-fe0f.png","sheet_x":24,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F486-1F3FF-200D-2640-FE0F","non_qualified":"1F486-1F3FF-200D-2640","image":"1f486-1f3ff-200d-2640-fe0f.png","sheet_x":24,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F486","a":"Woman Getting Massage","b":"1F486-200D-2640-FE0F","c":"1F486-200D-2640","k":[23,50]},"flag-tg":{"a":"Togo Flag","b":"1F1F9-1F1EC","k":[4,46]},"haircut":{"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB","non_qualified":null,"image":"1f487-1f3fb.png","sheet_x":24,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F487-1F3FC","non_qualified":null,"image":"1f487-1f3fc.png","sheet_x":24,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F487-1F3FD","non_qualified":null,"image":"1f487-1f3fd.png","sheet_x":24,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F487-1F3FE","non_qualified":null,"image":"1f487-1f3fe.png","sheet_x":24,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F487-1F3FF","non_qualified":null,"image":"1f487-1f3ff.png","sheet_x":24,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F487-200D-2640-FE0F","a":"Haircut","b":"1F487","k":[24,28]},"flag-th":{"a":"Thailand Flag","b":"1F1F9-1F1ED","k":[4,47]},"man-getting-haircut":{"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2642-FE0F","non_qualified":"1F487-1F3FB-200D-2642","image":"1f487-1f3fb-200d-2642-fe0f.png","sheet_x":24,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F487-1F3FC-200D-2642-FE0F","non_qualified":"1F487-1F3FC-200D-2642","image":"1f487-1f3fc-200d-2642-fe0f.png","sheet_x":24,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F487-1F3FD-200D-2642-FE0F","non_qualified":"1F487-1F3FD-200D-2642","image":"1f487-1f3fd-200d-2642-fe0f.png","sheet_x":24,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F487-1F3FE-200D-2642-FE0F","non_qualified":"1F487-1F3FE-200D-2642","image":"1f487-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F487-1F3FF-200D-2642-FE0F","non_qualified":"1F487-1F3FF-200D-2642","image":"1f487-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Getting Haircut","b":"1F487-200D-2642-FE0F","c":"1F487-200D-2642","k":[24,22]},"flag-tj":{"a":"Tajikistan Flag","b":"1F1F9-1F1EF","k":[4,48]},"flag-tk":{"a":"Tokelau Flag","b":"1F1F9-1F1F0","k":[4,49]},"woman-getting-haircut":{"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2640-FE0F","non_qualified":"1F487-1F3FB-200D-2640","image":"1f487-1f3fb-200d-2640-fe0f.png","sheet_x":24,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F487-1F3FC-200D-2640-FE0F","non_qualified":"1F487-1F3FC-200D-2640","image":"1f487-1f3fc-200d-2640-fe0f.png","sheet_x":24,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F487-1F3FD-200D-2640-FE0F","non_qualified":"1F487-1F3FD-200D-2640","image":"1f487-1f3fd-200d-2640-fe0f.png","sheet_x":24,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F487-1F3FE-200D-2640-FE0F","non_qualified":"1F487-1F3FE-200D-2640","image":"1f487-1f3fe-200d-2640-fe0f.png","sheet_x":24,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F487-1F3FF-200D-2640-FE0F","non_qualified":"1F487-1F3FF-200D-2640","image":"1f487-1f3ff-200d-2640-fe0f.png","sheet_x":24,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F487","a":"Woman Getting Haircut","b":"1F487-200D-2640-FE0F","c":"1F487-200D-2640","k":[24,16]},"walking":{"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB","non_qualified":null,"image":"1f6b6-1f3fb.png","sheet_x":36,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F6B6-1F3FC","non_qualified":null,"image":"1f6b6-1f3fc.png","sheet_x":36,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F6B6-1F3FD","non_qualified":null,"image":"1f6b6-1f3fd.png","sheet_x":36,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F6B6-1F3FE","non_qualified":null,"image":"1f6b6-1f3fe.png","sheet_x":36,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F6B6-1F3FF","non_qualified":null,"image":"1f6b6-1f3ff.png","sheet_x":36,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F6B6-200D-2642-FE0F","a":"Pedestrian","b":"1F6B6","k":[36,21]},"flag-tl":{"a":"Timor-Leste Flag","b":"1F1F9-1F1F1","k":[4,50]},"man-walking":{"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2642-FE0F","non_qualified":"1F6B6-1F3FB-200D-2642","image":"1f6b6-1f3fb-200d-2642-fe0f.png","sheet_x":36,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2642-FE0F","non_qualified":"1F6B6-1F3FC-200D-2642","image":"1f6b6-1f3fc-200d-2642-fe0f.png","sheet_x":36,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2642-FE0F","non_qualified":"1F6B6-1F3FD-200D-2642","image":"1f6b6-1f3fd-200d-2642-fe0f.png","sheet_x":36,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2642-FE0F","non_qualified":"1F6B6-1F3FE-200D-2642","image":"1f6b6-1f3fe-200d-2642-fe0f.png","sheet_x":36,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2642-FE0F","non_qualified":"1F6B6-1F3FF-200D-2642","image":"1f6b6-1f3ff-200d-2642-fe0f.png","sheet_x":36,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F6B6","a":"Man Walking","b":"1F6B6-200D-2642-FE0F","c":"1F6B6-200D-2642","k":[36,15]},"flag-tm":{"a":"Turkmenistan Flag","b":"1F1F9-1F1F2","k":[4,51]},"woman-walking":{"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2640-FE0F","non_qualified":"1F6B6-1F3FB-200D-2640","image":"1f6b6-1f3fb-200d-2640-fe0f.png","sheet_x":36,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2640-FE0F","non_qualified":"1F6B6-1F3FC-200D-2640","image":"1f6b6-1f3fc-200d-2640-fe0f.png","sheet_x":36,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2640-FE0F","non_qualified":"1F6B6-1F3FD-200D-2640","image":"1f6b6-1f3fd-200d-2640-fe0f.png","sheet_x":36,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2640-FE0F","non_qualified":"1F6B6-1F3FE-200D-2640","image":"1f6b6-1f3fe-200d-2640-fe0f.png","sheet_x":36,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2640-FE0F","non_qualified":"1F6B6-1F3FF-200D-2640","image":"1f6b6-1f3ff-200d-2640-fe0f.png","sheet_x":36,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Walking","b":"1F6B6-200D-2640-FE0F","c":"1F6B6-200D-2640","k":[36,9]},"flag-tn":{"a":"Tunisia Flag","b":"1F1F9-1F1F3","k":[5,0]},"runner":{"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB","non_qualified":null,"image":"1f3c3-1f3fb.png","sheet_x":9,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F3C3-1F3FC","non_qualified":null,"image":"1f3c3-1f3fc.png","sheet_x":9,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F3C3-1F3FD","non_qualified":null,"image":"1f3c3-1f3fd.png","sheet_x":9,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F3C3-1F3FE","non_qualified":null,"image":"1f3c3-1f3fe.png","sheet_x":9,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F3C3-1F3FF","non_qualified":null,"image":"1f3c3-1f3ff.png","sheet_x":9,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F3C3-200D-2642-FE0F","a":"Runner","b":"1F3C3","k":[9,46],"n":["running"]},"flag-to":{"a":"Tonga Flag","b":"1F1F9-1F1F4","k":[5,1]},"man-running":{"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2642-FE0F","non_qualified":"1F3C3-1F3FB-200D-2642","image":"1f3c3-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2642-FE0F","non_qualified":"1F3C3-1F3FC-200D-2642","image":"1f3c3-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2642-FE0F","non_qualified":"1F3C3-1F3FD-200D-2642","image":"1f3c3-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2642-FE0F","non_qualified":"1F3C3-1F3FE-200D-2642","image":"1f3c3-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2642-FE0F","non_qualified":"1F3C3-1F3FF-200D-2642","image":"1f3c3-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F3C3","a":"Man Running","b":"1F3C3-200D-2642-FE0F","c":"1F3C3-200D-2642","k":[9,40]},"flag-tr":{"a":"Turkey Flag","b":"1F1F9-1F1F7","k":[5,2]},"flag-tt":{"a":"Trinidad & Tobago Flag","b":"1F1F9-1F1F9","k":[5,3]},"woman-running":{"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2640-FE0F","non_qualified":"1F3C3-1F3FB-200D-2640","image":"1f3c3-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2640-FE0F","non_qualified":"1F3C3-1F3FC-200D-2640","image":"1f3c3-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2640-FE0F","non_qualified":"1F3C3-1F3FD-200D-2640","image":"1f3c3-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2640-FE0F","non_qualified":"1F3C3-1F3FE-200D-2640","image":"1f3c3-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2640-FE0F","non_qualified":"1F3C3-1F3FF-200D-2640","image":"1f3c3-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Running","b":"1F3C3-200D-2640-FE0F","c":"1F3C3-200D-2640","k":[9,34]},"flag-tv":{"a":"Tuvalu Flag","b":"1F1F9-1F1FB","k":[5,4]},"dancer":{"skin_variations":{"1F3FB":{"unified":"1F483-1F3FB","non_qualified":null,"image":"1f483-1f3fb.png","sheet_x":23,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F483-1F3FC","non_qualified":null,"image":"1f483-1f3fc.png","sheet_x":23,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F483-1F3FD","non_qualified":null,"image":"1f483-1f3fd.png","sheet_x":23,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F483-1F3FE","non_qualified":null,"image":"1f483-1f3fe.png","sheet_x":23,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F483-1F3FF","non_qualified":null,"image":"1f483-1f3ff.png","sheet_x":23,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Dancer","b":"1F483","j":["female","girl","woman","fun"],"k":[23,37]},"flag-tw":{"a":"Taiwan Flag","b":"1F1F9-1F1FC","k":[5,5]},"man_dancing":{"skin_variations":{"1F3FB":{"unified":"1F57A-1F3FB","non_qualified":null,"image":"1f57a-1f3fb.png","sheet_x":29,"sheet_y":22,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F57A-1F3FC","non_qualified":null,"image":"1f57a-1f3fc.png","sheet_x":29,"sheet_y":23,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F57A-1F3FD","non_qualified":null,"image":"1f57a-1f3fd.png","sheet_x":29,"sheet_y":24,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F57A-1F3FE","non_qualified":null,"image":"1f57a-1f3fe.png","sheet_x":29,"sheet_y":25,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F57A-1F3FF","non_qualified":null,"image":"1f57a-1f3ff.png","sheet_x":29,"sheet_y":26,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Man Dancing","b":"1F57A","j":["male","boy","fun","dancer"],"k":[29,21],"o":9},"dancers":{"obsoleted_by":"1F46F-200D-2640-FE0F","a":"Woman with Bunny Ears","b":"1F46F","k":[21,1]},"flag-tz":{"a":"Tanzania Flag","b":"1F1F9-1F1FF","k":[5,6]},"flag-ua":{"a":"Ukraine Flag","b":"1F1FA-1F1E6","k":[5,7]},"man-with-bunny-ears-partying":{"a":"Man with Bunny Ears Partying","b":"1F46F-200D-2642-FE0F","c":"1F46F-200D-2642","k":[21,0]},"woman-with-bunny-ears-partying":{"obsoletes":"1F46F","a":"Woman with Bunny Ears Partying","b":"1F46F-200D-2640-FE0F","c":"1F46F-200D-2640","k":[20,51]},"flag-ug":{"a":"Uganda Flag","b":"1F1FA-1F1EC","k":[5,8]},"flag-um":{"a":"U.s. Outlying Islands Flag","b":"1F1FA-1F1F2","k":[5,9]},"person_in_steamy_room":{"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB","non_qualified":null,"image":"1f9d6-1f3fb.png","sheet_x":43,"sheet_y":41,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D6-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9D6-1F3FC","non_qualified":null,"image":"1f9d6-1f3fc.png","sheet_x":43,"sheet_y":42,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D6-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9D6-1F3FD","non_qualified":null,"image":"1f9d6-1f3fd.png","sheet_x":43,"sheet_y":43,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D6-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9D6-1F3FE","non_qualified":null,"image":"1f9d6-1f3fe.png","sheet_x":43,"sheet_y":44,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D6-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9D6-1F3FF","non_qualified":null,"image":"1f9d6-1f3ff.png","sheet_x":43,"sheet_y":45,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D6-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9D6-200D-2642-FE0F","a":"Person in Steamy Room","b":"1F9D6","k":[43,40],"o":10},"woman_in_steamy_room":{"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2640-FE0F","non_qualified":"1F9D6-1F3FB-200D-2640","image":"1f9d6-1f3fb-200d-2640-fe0f.png","sheet_x":43,"sheet_y":29,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2640-FE0F","non_qualified":"1F9D6-1F3FC-200D-2640","image":"1f9d6-1f3fc-200d-2640-fe0f.png","sheet_x":43,"sheet_y":30,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2640-FE0F","non_qualified":"1F9D6-1F3FD-200D-2640","image":"1f9d6-1f3fd-200d-2640-fe0f.png","sheet_x":43,"sheet_y":31,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2640-FE0F","non_qualified":"1F9D6-1F3FE-200D-2640","image":"1f9d6-1f3fe-200d-2640-fe0f.png","sheet_x":43,"sheet_y":32,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2640-FE0F","non_qualified":"1F9D6-1F3FF-200D-2640","image":"1f9d6-1f3ff-200d-2640-fe0f.png","sheet_x":43,"sheet_y":33,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman in Steamy Room","b":"1F9D6-200D-2640-FE0F","c":"1F9D6-200D-2640","k":[43,28],"o":10},"flag-un":{"a":"United Nations Flag","b":"1F1FA-1F1F3","k":[5,10]},"us":{"a":"United States Flag","b":"1F1FA-1F1F8","j":["united","states","america","flag","nation","country","banner"],"k":[5,11],"n":["flag-us"]},"man_in_steamy_room":{"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2642-FE0F","non_qualified":"1F9D6-1F3FB-200D-2642","image":"1f9d6-1f3fb-200d-2642-fe0f.png","sheet_x":43,"sheet_y":35,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D6-1F3FB"},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2642-FE0F","non_qualified":"1F9D6-1F3FC-200D-2642","image":"1f9d6-1f3fc-200d-2642-fe0f.png","sheet_x":43,"sheet_y":36,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D6-1F3FC"},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2642-FE0F","non_qualified":"1F9D6-1F3FD-200D-2642","image":"1f9d6-1f3fd-200d-2642-fe0f.png","sheet_x":43,"sheet_y":37,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D6-1F3FD"},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2642-FE0F","non_qualified":"1F9D6-1F3FE-200D-2642","image":"1f9d6-1f3fe-200d-2642-fe0f.png","sheet_x":43,"sheet_y":38,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D6-1F3FE"},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2642-FE0F","non_qualified":"1F9D6-1F3FF-200D-2642","image":"1f9d6-1f3ff-200d-2642-fe0f.png","sheet_x":43,"sheet_y":39,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D6-1F3FF"}},"obsoletes":"1F9D6","a":"Man in Steamy Room","b":"1F9D6-200D-2642-FE0F","c":"1F9D6-200D-2642","k":[43,34],"o":10},"person_climbing":{"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB","non_qualified":null,"image":"1f9d7-1f3fb.png","sheet_x":44,"sheet_y":7,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D7-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D7-1F3FC","non_qualified":null,"image":"1f9d7-1f3fc.png","sheet_x":44,"sheet_y":8,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D7-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D7-1F3FD","non_qualified":null,"image":"1f9d7-1f3fd.png","sheet_x":44,"sheet_y":9,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D7-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D7-1F3FE","non_qualified":null,"image":"1f9d7-1f3fe.png","sheet_x":44,"sheet_y":10,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D7-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D7-1F3FF","non_qualified":null,"image":"1f9d7-1f3ff.png","sheet_x":44,"sheet_y":11,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D7-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D7-200D-2640-FE0F","a":"Person Climbing","b":"1F9D7","k":[44,6],"o":10},"flag-uy":{"a":"Uruguay Flag","b":"1F1FA-1F1FE","k":[5,12]},"woman_climbing":{"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2640-FE0F","non_qualified":"1F9D7-1F3FB-200D-2640","image":"1f9d7-1f3fb-200d-2640-fe0f.png","sheet_x":43,"sheet_y":47,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D7-1F3FB"},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2640-FE0F","non_qualified":"1F9D7-1F3FC-200D-2640","image":"1f9d7-1f3fc-200d-2640-fe0f.png","sheet_x":43,"sheet_y":48,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D7-1F3FC"},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2640-FE0F","non_qualified":"1F9D7-1F3FD-200D-2640","image":"1f9d7-1f3fd-200d-2640-fe0f.png","sheet_x":43,"sheet_y":49,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D7-1F3FD"},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2640-FE0F","non_qualified":"1F9D7-1F3FE-200D-2640","image":"1f9d7-1f3fe-200d-2640-fe0f.png","sheet_x":43,"sheet_y":50,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D7-1F3FE"},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2640-FE0F","non_qualified":"1F9D7-1F3FF-200D-2640","image":"1f9d7-1f3ff-200d-2640-fe0f.png","sheet_x":43,"sheet_y":51,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D7-1F3FF"}},"obsoletes":"1F9D7","a":"Woman Climbing","b":"1F9D7-200D-2640-FE0F","c":"1F9D7-200D-2640","k":[43,46],"o":10},"flag-uz":{"a":"Uzbekistan Flag","b":"1F1FA-1F1FF","k":[5,13]},"man_climbing":{"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2642-FE0F","non_qualified":"1F9D7-1F3FB-200D-2642","image":"1f9d7-1f3fb-200d-2642-fe0f.png","sheet_x":44,"sheet_y":1,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2642-FE0F","non_qualified":"1F9D7-1F3FC-200D-2642","image":"1f9d7-1f3fc-200d-2642-fe0f.png","sheet_x":44,"sheet_y":2,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2642-FE0F","non_qualified":"1F9D7-1F3FD-200D-2642","image":"1f9d7-1f3fd-200d-2642-fe0f.png","sheet_x":44,"sheet_y":3,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2642-FE0F","non_qualified":"1F9D7-1F3FE-200D-2642","image":"1f9d7-1f3fe-200d-2642-fe0f.png","sheet_x":44,"sheet_y":4,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2642-FE0F","non_qualified":"1F9D7-1F3FF-200D-2642","image":"1f9d7-1f3ff-200d-2642-fe0f.png","sheet_x":44,"sheet_y":5,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Climbing","b":"1F9D7-200D-2642-FE0F","c":"1F9D7-200D-2642","k":[44,0],"o":10},"flag-va":{"a":"Vatican City Flag","b":"1F1FB-1F1E6","k":[5,14]},"person_in_lotus_position":{"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB","non_qualified":null,"image":"1f9d8-1f3fb.png","sheet_x":44,"sheet_y":25,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D8-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D8-1F3FC","non_qualified":null,"image":"1f9d8-1f3fc.png","sheet_x":44,"sheet_y":26,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D8-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D8-1F3FD","non_qualified":null,"image":"1f9d8-1f3fd.png","sheet_x":44,"sheet_y":27,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D8-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D8-1F3FE","non_qualified":null,"image":"1f9d8-1f3fe.png","sheet_x":44,"sheet_y":28,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D8-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D8-1F3FF","non_qualified":null,"image":"1f9d8-1f3ff.png","sheet_x":44,"sheet_y":29,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false,"obsoleted_by":"1F9D8-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D8-200D-2640-FE0F","a":"Person in Lotus Position","b":"1F9D8","k":[44,24],"o":10},"flag-vc":{"a":"St. Vincent & Grenadines Flag","b":"1F1FB-1F1E8","k":[5,15]},"flag-ve":{"a":"Venezuela Flag","b":"1F1FB-1F1EA","k":[5,16]},"woman_in_lotus_position":{"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2640-FE0F","non_qualified":"1F9D8-1F3FB-200D-2640","image":"1f9d8-1f3fb-200d-2640-fe0f.png","sheet_x":44,"sheet_y":13,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D8-1F3FB"},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2640-FE0F","non_qualified":"1F9D8-1F3FC-200D-2640","image":"1f9d8-1f3fc-200d-2640-fe0f.png","sheet_x":44,"sheet_y":14,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D8-1F3FC"},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2640-FE0F","non_qualified":"1F9D8-1F3FD-200D-2640","image":"1f9d8-1f3fd-200d-2640-fe0f.png","sheet_x":44,"sheet_y":15,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D8-1F3FD"},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2640-FE0F","non_qualified":"1F9D8-1F3FE-200D-2640","image":"1f9d8-1f3fe-200d-2640-fe0f.png","sheet_x":44,"sheet_y":16,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D8-1F3FE"},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2640-FE0F","non_qualified":"1F9D8-1F3FF-200D-2640","image":"1f9d8-1f3ff-200d-2640-fe0f.png","sheet_x":44,"sheet_y":17,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false,"obsoletes":"1F9D8-1F3FF"}},"obsoletes":"1F9D8","a":"Woman in Lotus Position","b":"1F9D8-200D-2640-FE0F","c":"1F9D8-200D-2640","k":[44,12],"o":10},"man_in_lotus_position":{"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2642-FE0F","non_qualified":"1F9D8-1F3FB-200D-2642","image":"1f9d8-1f3fb-200d-2642-fe0f.png","sheet_x":44,"sheet_y":19,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2642-FE0F","non_qualified":"1F9D8-1F3FC-200D-2642","image":"1f9d8-1f3fc-200d-2642-fe0f.png","sheet_x":44,"sheet_y":20,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2642-FE0F","non_qualified":"1F9D8-1F3FD-200D-2642","image":"1f9d8-1f3fd-200d-2642-fe0f.png","sheet_x":44,"sheet_y":21,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2642-FE0F","non_qualified":"1F9D8-1F3FE-200D-2642","image":"1f9d8-1f3fe-200d-2642-fe0f.png","sheet_x":44,"sheet_y":22,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2642-FE0F","non_qualified":"1F9D8-1F3FF-200D-2642","image":"1f9d8-1f3ff-200d-2642-fe0f.png","sheet_x":44,"sheet_y":23,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man in Lotus Position","b":"1F9D8-200D-2642-FE0F","c":"1F9D8-200D-2642","k":[44,18],"o":10},"flag-vg":{"a":"British Virgin Islands Flag","b":"1F1FB-1F1EC","k":[5,17]},"flag-vi":{"a":"U.s. Virgin Islands Flag","b":"1F1FB-1F1EE","k":[5,18]},"bath":{"skin_variations":{"1F3FB":{"unified":"1F6C0-1F3FB","non_qualified":null,"image":"1f6c0-1f3fb.png","sheet_x":36,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F6C0-1F3FC","non_qualified":null,"image":"1f6c0-1f3fc.png","sheet_x":36,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F6C0-1F3FD","non_qualified":null,"image":"1f6c0-1f3fd.png","sheet_x":36,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F6C0-1F3FE","non_qualified":null,"image":"1f6c0-1f3fe.png","sheet_x":36,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F6C0-1F3FF","non_qualified":null,"image":"1f6c0-1f3ff.png","sheet_x":36,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Bath","b":"1F6C0","j":["clean","shower","bathroom"],"k":[36,36]},"sleeping_accommodation":{"skin_variations":{"1F3FB":{"unified":"1F6CC-1F3FB","non_qualified":null,"image":"1f6cc-1f3fb.png","sheet_x":36,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F6CC-1F3FC","non_qualified":null,"image":"1f6cc-1f3fc.png","sheet_x":36,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F6CC-1F3FD","non_qualified":null,"image":"1f6cc-1f3fd.png","sheet_x":36,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F6CC-1F3FE","non_qualified":null,"image":"1f6cc-1f3fe.png","sheet_x":37,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F6CC-1F3FF","non_qualified":null,"image":"1f6cc-1f3ff.png","sheet_x":37,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Sleeping Accommodation","b":"1F6CC","k":[36,48],"o":7},"flag-vn":{"a":"Vietnam Flag","b":"1F1FB-1F1F3","k":[5,19]},"man_in_business_suit_levitating":{"skin_variations":{"1F3FB":{"unified":"1F574-1F3FB","non_qualified":null,"image":"1f574-1f3fb.png","sheet_x":28,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F574-1F3FC","non_qualified":null,"image":"1f574-1f3fc.png","sheet_x":28,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F574-1F3FD","non_qualified":null,"image":"1f574-1f3fd.png","sheet_x":28,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F574-1F3FE","non_qualified":null,"image":"1f574-1f3fe.png","sheet_x":28,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F574-1F3FF","non_qualified":null,"image":"1f574-1f3ff.png","sheet_x":28,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Man in Business Suit Levitating","b":"1F574-FE0F","c":"1F574","k":[28,45],"o":7},"flag-vu":{"a":"Vanuatu Flag","b":"1F1FB-1F1FA","k":[5,20]},"speaking_head_in_silhouette":{"a":"Speaking Head in Silhouette","b":"1F5E3-FE0F","c":"1F5E3","k":[30,14],"o":7},"bust_in_silhouette":{"a":"Bust in Silhouette","b":"1F464","j":["user","person","human"],"k":[15,40]},"flag-ws":{"a":"Samoa Flag","b":"1F1FC-1F1F8","k":[5,22]},"busts_in_silhouette":{"a":"Busts in Silhouette","b":"1F465","j":["user","person","human","group","team"],"k":[15,41]},"fencer":{"a":"Fencer","b":"1F93A","k":[40,48],"o":9},"flag-ye":{"a":"Yemen Flag","b":"1F1FE-1F1EA","k":[5,24]},"horse_racing":{"skin_variations":{"1F3FB":{"unified":"1F3C7-1F3FB","non_qualified":null,"image":"1f3c7-1f3fb.png","sheet_x":10,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F3C7-1F3FC","non_qualified":null,"image":"1f3c7-1f3fc.png","sheet_x":10,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F3C7-1F3FD","non_qualified":null,"image":"1f3c7-1f3fd.png","sheet_x":10,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F3C7-1F3FE","non_qualified":null,"image":"1f3c7-1f3fe.png","sheet_x":10,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F3C7-1F3FF","non_qualified":null,"image":"1f3c7-1f3ff.png","sheet_x":10,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Horse Racing","b":"1F3C7","j":["animal","betting","competition","gambling","luck"],"k":[10,20]},"flag-za":{"a":"South Africa Flag","b":"1F1FF-1F1E6","k":[5,26]},"skier":{"a":"Skier","b":"26F7-FE0F","c":"26F7","j":["sports","winter","snow"],"k":[48,44],"o":5},"flag-zm":{"a":"Zambia Flag","b":"1F1FF-1F1F2","k":[5,27]},"snowboarder":{"skin_variations":{"1F3FB":{"unified":"1F3C2-1F3FB","non_qualified":null,"image":"1f3c2-1f3fb.png","sheet_x":9,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F3C2-1F3FC","non_qualified":null,"image":"1f3c2-1f3fc.png","sheet_x":9,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F3C2-1F3FD","non_qualified":null,"image":"1f3c2-1f3fd.png","sheet_x":9,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F3C2-1F3FE","non_qualified":null,"image":"1f3c2-1f3fe.png","sheet_x":9,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F3C2-1F3FF","non_qualified":null,"image":"1f3c2-1f3ff.png","sheet_x":9,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Snowboarder","b":"1F3C2","j":["sports","winter"],"k":[9,28]},"golfer":{"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB","non_qualified":null,"image":"1f3cc-1f3fb.png","sheet_x":11,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CC-1F3FC","non_qualified":null,"image":"1f3cc-1f3fc.png","sheet_x":11,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CC-1F3FD","non_qualified":null,"image":"1f3cc-1f3fd.png","sheet_x":11,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CC-1F3FE","non_qualified":null,"image":"1f3cc-1f3fe.png","sheet_x":11,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CC-1F3FF","non_qualified":null,"image":"1f3cc-1f3ff.png","sheet_x":11,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoleted_by":"1F3CC-FE0F-200D-2642-FE0F","a":"Golfer","b":"1F3CC-FE0F","c":"1F3CC","k":[11,24],"o":7},"flag-zw":{"a":"Zimbabwe Flag","b":"1F1FF-1F1FC","k":[5,28]},"man-golfing":{"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2642-FE0F","non_qualified":"1F3CC-1F3FB-200D-2642","image":"1f3cc-1f3fb-200d-2642-fe0f.png","sheet_x":11,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2642-FE0F","non_qualified":"1F3CC-1F3FC-200D-2642","image":"1f3cc-1f3fc-200d-2642-fe0f.png","sheet_x":11,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2642-FE0F","non_qualified":"1F3CC-1F3FD-200D-2642","image":"1f3cc-1f3fd-200d-2642-fe0f.png","sheet_x":11,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2642-FE0F","non_qualified":"1F3CC-1F3FE-200D-2642","image":"1f3cc-1f3fe-200d-2642-fe0f.png","sheet_x":11,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2642-FE0F","non_qualified":"1F3CC-1F3FF-200D-2642","image":"1f3cc-1f3ff-200d-2642-fe0f.png","sheet_x":11,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F3CC-FE0F","a":"Man Golfing","b":"1F3CC-FE0F-200D-2642-FE0F","k":[11,18],"o":7},"flag-england":{"a":"England Flag","b":"1F3F4-E0067-E0062-E0065-E006E-E0067-E007F","k":[12,16],"o":7},"woman-golfing":{"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2640-FE0F","non_qualified":"1F3CC-1F3FB-200D-2640","image":"1f3cc-1f3fb-200d-2640-fe0f.png","sheet_x":11,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2640-FE0F","non_qualified":"1F3CC-1F3FC-200D-2640","image":"1f3cc-1f3fc-200d-2640-fe0f.png","sheet_x":11,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2640-FE0F","non_qualified":"1F3CC-1F3FD-200D-2640","image":"1f3cc-1f3fd-200d-2640-fe0f.png","sheet_x":11,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2640-FE0F","non_qualified":"1F3CC-1F3FE-200D-2640","image":"1f3cc-1f3fe-200d-2640-fe0f.png","sheet_x":11,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2640-FE0F","non_qualified":"1F3CC-1F3FF-200D-2640","image":"1f3cc-1f3ff-200d-2640-fe0f.png","sheet_x":11,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Golfing","b":"1F3CC-FE0F-200D-2640-FE0F","k":[11,12],"o":7},"flag-scotland":{"a":"Scotland Flag","b":"1F3F4-E0067-E0062-E0073-E0063-E0074-E007F","k":[12,17],"o":7},"flag-wales":{"a":"Wales Flag","b":"1F3F4-E0067-E0062-E0077-E006C-E0073-E007F","k":[12,18],"o":7},"surfer":{"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB","non_qualified":null,"image":"1f3c4-1f3fb.png","sheet_x":10,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F3C4-1F3FC","non_qualified":null,"image":"1f3c4-1f3fc.png","sheet_x":10,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F3C4-1F3FD","non_qualified":null,"image":"1f3c4-1f3fd.png","sheet_x":10,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F3C4-1F3FE","non_qualified":null,"image":"1f3c4-1f3fe.png","sheet_x":10,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F3C4-1F3FF","non_qualified":null,"image":"1f3c4-1f3ff.png","sheet_x":10,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F3C4-200D-2642-FE0F","a":"Surfer","b":"1F3C4","k":[10,12]},"man-surfing":{"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2642-FE0F","non_qualified":"1F3C4-1F3FB-200D-2642","image":"1f3c4-1f3fb-200d-2642-fe0f.png","sheet_x":10,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2642-FE0F","non_qualified":"1F3C4-1F3FC-200D-2642","image":"1f3c4-1f3fc-200d-2642-fe0f.png","sheet_x":10,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2642-FE0F","non_qualified":"1F3C4-1F3FD-200D-2642","image":"1f3c4-1f3fd-200d-2642-fe0f.png","sheet_x":10,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2642-FE0F","non_qualified":"1F3C4-1F3FE-200D-2642","image":"1f3c4-1f3fe-200d-2642-fe0f.png","sheet_x":10,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2642-FE0F","non_qualified":"1F3C4-1F3FF-200D-2642","image":"1f3c4-1f3ff-200d-2642-fe0f.png","sheet_x":10,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F3C4","a":"Man Surfing","b":"1F3C4-200D-2642-FE0F","c":"1F3C4-200D-2642","k":[10,6]},"woman-surfing":{"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2640-FE0F","non_qualified":"1F3C4-1F3FB-200D-2640","image":"1f3c4-1f3fb-200d-2640-fe0f.png","sheet_x":10,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2640-FE0F","non_qualified":"1F3C4-1F3FC-200D-2640","image":"1f3c4-1f3fc-200d-2640-fe0f.png","sheet_x":10,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2640-FE0F","non_qualified":"1F3C4-1F3FD-200D-2640","image":"1f3c4-1f3fd-200d-2640-fe0f.png","sheet_x":10,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2640-FE0F","non_qualified":"1F3C4-1F3FE-200D-2640","image":"1f3c4-1f3fe-200d-2640-fe0f.png","sheet_x":10,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2640-FE0F","non_qualified":"1F3C4-1F3FF-200D-2640","image":"1f3c4-1f3ff-200d-2640-fe0f.png","sheet_x":10,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Surfing","b":"1F3C4-200D-2640-FE0F","c":"1F3C4-200D-2640","k":[10,0]},"rowboat":{"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB","non_qualified":null,"image":"1f6a3-1f3fb.png","sheet_x":35,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6A3-1F3FC","non_qualified":null,"image":"1f6a3-1f3fc.png","sheet_x":35,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6A3-1F3FD","non_qualified":null,"image":"1f6a3-1f3fd.png","sheet_x":35,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6A3-1F3FE","non_qualified":null,"image":"1f6a3-1f3fe.png","sheet_x":35,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6A3-1F3FF","non_qualified":null,"image":"1f6a3-1f3ff.png","sheet_x":35,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoleted_by":"1F6A3-200D-2642-FE0F","a":"Rowboat","b":"1F6A3","k":[35,3]},"man-rowing-boat":{"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2642-FE0F","non_qualified":"1F6A3-1F3FB-200D-2642","image":"1f6a3-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2642-FE0F","non_qualified":"1F6A3-1F3FC-200D-2642","image":"1f6a3-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2642-FE0F","non_qualified":"1F6A3-1F3FD-200D-2642","image":"1f6a3-1f3fd-200d-2642-fe0f.png","sheet_x":35,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2642-FE0F","non_qualified":"1F6A3-1F3FE-200D-2642","image":"1f6a3-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2642-FE0F","non_qualified":"1F6A3-1F3FF-200D-2642","image":"1f6a3-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F6A3","a":"Man Rowing Boat","b":"1F6A3-200D-2642-FE0F","c":"1F6A3-200D-2642","k":[34,49]},"woman-rowing-boat":{"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2640-FE0F","non_qualified":"1F6A3-1F3FB-200D-2640","image":"1f6a3-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2640-FE0F","non_qualified":"1F6A3-1F3FC-200D-2640","image":"1f6a3-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2640-FE0F","non_qualified":"1F6A3-1F3FD-200D-2640","image":"1f6a3-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2640-FE0F","non_qualified":"1F6A3-1F3FE-200D-2640","image":"1f6a3-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2640-FE0F","non_qualified":"1F6A3-1F3FF-200D-2640","image":"1f6a3-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Rowing Boat","b":"1F6A3-200D-2640-FE0F","c":"1F6A3-200D-2640","k":[34,43]},"swimmer":{"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB","non_qualified":null,"image":"1f3ca-1f3fb.png","sheet_x":10,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F3CA-1F3FC","non_qualified":null,"image":"1f3ca-1f3fc.png","sheet_x":10,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F3CA-1F3FD","non_qualified":null,"image":"1f3ca-1f3fd.png","sheet_x":10,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F3CA-1F3FE","non_qualified":null,"image":"1f3ca-1f3fe.png","sheet_x":10,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F3CA-1F3FF","non_qualified":null,"image":"1f3ca-1f3ff.png","sheet_x":10,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F3CA-200D-2642-FE0F","a":"Swimmer","b":"1F3CA","k":[10,40]},"man-swimming":{"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2642-FE0F","non_qualified":"1F3CA-1F3FB-200D-2642","image":"1f3ca-1f3fb-200d-2642-fe0f.png","sheet_x":10,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2642-FE0F","non_qualified":"1F3CA-1F3FC-200D-2642","image":"1f3ca-1f3fc-200d-2642-fe0f.png","sheet_x":10,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2642-FE0F","non_qualified":"1F3CA-1F3FD-200D-2642","image":"1f3ca-1f3fd-200d-2642-fe0f.png","sheet_x":10,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2642-FE0F","non_qualified":"1F3CA-1F3FE-200D-2642","image":"1f3ca-1f3fe-200d-2642-fe0f.png","sheet_x":10,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2642-FE0F","non_qualified":"1F3CA-1F3FF-200D-2642","image":"1f3ca-1f3ff-200d-2642-fe0f.png","sheet_x":10,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F3CA","a":"Man Swimming","b":"1F3CA-200D-2642-FE0F","c":"1F3CA-200D-2642","k":[10,34]},"woman-swimming":{"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2640-FE0F","non_qualified":"1F3CA-1F3FB-200D-2640","image":"1f3ca-1f3fb-200d-2640-fe0f.png","sheet_x":10,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2640-FE0F","non_qualified":"1F3CA-1F3FC-200D-2640","image":"1f3ca-1f3fc-200d-2640-fe0f.png","sheet_x":10,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2640-FE0F","non_qualified":"1F3CA-1F3FD-200D-2640","image":"1f3ca-1f3fd-200d-2640-fe0f.png","sheet_x":10,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2640-FE0F","non_qualified":"1F3CA-1F3FE-200D-2640","image":"1f3ca-1f3fe-200d-2640-fe0f.png","sheet_x":10,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2640-FE0F","non_qualified":"1F3CA-1F3FF-200D-2640","image":"1f3ca-1f3ff-200d-2640-fe0f.png","sheet_x":10,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Swimming","b":"1F3CA-200D-2640-FE0F","c":"1F3CA-200D-2640","k":[10,28]},"person_with_ball":{"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB","non_qualified":null,"image":"26f9-1f3fb.png","sheet_x":49,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"26F9-1F3FC","non_qualified":null,"image":"26f9-1f3fc.png","sheet_x":49,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"26F9-1F3FD","non_qualified":null,"image":"26f9-1f3fd.png","sheet_x":49,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"26F9-1F3FE","non_qualified":null,"image":"26f9-1f3fe.png","sheet_x":49,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"26F9-1F3FF","non_qualified":null,"image":"26f9-1f3ff.png","sheet_x":49,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoleted_by":"26F9-FE0F-200D-2642-FE0F","a":"Person with Ball","b":"26F9-FE0F","c":"26F9","k":[49,6],"o":5},"man-bouncing-ball":{"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2642-FE0F","non_qualified":"26F9-1F3FB-200D-2642","image":"26f9-1f3fb-200d-2642-fe0f.png","sheet_x":49,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"26F9-1F3FC-200D-2642-FE0F","non_qualified":"26F9-1F3FC-200D-2642","image":"26f9-1f3fc-200d-2642-fe0f.png","sheet_x":49,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"26F9-1F3FD-200D-2642-FE0F","non_qualified":"26F9-1F3FD-200D-2642","image":"26f9-1f3fd-200d-2642-fe0f.png","sheet_x":49,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"26F9-1F3FE-200D-2642-FE0F","non_qualified":"26F9-1F3FE-200D-2642","image":"26f9-1f3fe-200d-2642-fe0f.png","sheet_x":49,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"26F9-1F3FF-200D-2642-FE0F","non_qualified":"26F9-1F3FF-200D-2642","image":"26f9-1f3ff-200d-2642-fe0f.png","sheet_x":49,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"26F9-FE0F","a":"Man Bouncing Ball","b":"26F9-FE0F-200D-2642-FE0F","k":[49,0],"o":5},"woman-bouncing-ball":{"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2640-FE0F","non_qualified":"26F9-1F3FB-200D-2640","image":"26f9-1f3fb-200d-2640-fe0f.png","sheet_x":48,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"26F9-1F3FC-200D-2640-FE0F","non_qualified":"26F9-1F3FC-200D-2640","image":"26f9-1f3fc-200d-2640-fe0f.png","sheet_x":48,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"26F9-1F3FD-200D-2640-FE0F","non_qualified":"26F9-1F3FD-200D-2640","image":"26f9-1f3fd-200d-2640-fe0f.png","sheet_x":48,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"26F9-1F3FE-200D-2640-FE0F","non_qualified":"26F9-1F3FE-200D-2640","image":"26f9-1f3fe-200d-2640-fe0f.png","sheet_x":48,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"26F9-1F3FF-200D-2640-FE0F","non_qualified":"26F9-1F3FF-200D-2640","image":"26f9-1f3ff-200d-2640-fe0f.png","sheet_x":48,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Bouncing Ball","b":"26F9-FE0F-200D-2640-FE0F","k":[48,46],"o":5},"weight_lifter":{"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB","non_qualified":null,"image":"1f3cb-1f3fb.png","sheet_x":11,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CB-1F3FC","non_qualified":null,"image":"1f3cb-1f3fc.png","sheet_x":11,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CB-1F3FD","non_qualified":null,"image":"1f3cb-1f3fd.png","sheet_x":11,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CB-1F3FE","non_qualified":null,"image":"1f3cb-1f3fe.png","sheet_x":11,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CB-1F3FF","non_qualified":null,"image":"1f3cb-1f3ff.png","sheet_x":11,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoleted_by":"1F3CB-FE0F-200D-2642-FE0F","a":"Weight Lifter","b":"1F3CB-FE0F","c":"1F3CB","k":[11,6],"o":7},"man-lifting-weights":{"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2642-FE0F","non_qualified":"1F3CB-1F3FB-200D-2642","image":"1f3cb-1f3fb-200d-2642-fe0f.png","sheet_x":11,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2642-FE0F","non_qualified":"1F3CB-1F3FC-200D-2642","image":"1f3cb-1f3fc-200d-2642-fe0f.png","sheet_x":11,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2642-FE0F","non_qualified":"1F3CB-1F3FD-200D-2642","image":"1f3cb-1f3fd-200d-2642-fe0f.png","sheet_x":11,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2642-FE0F","non_qualified":"1F3CB-1F3FE-200D-2642","image":"1f3cb-1f3fe-200d-2642-fe0f.png","sheet_x":11,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2642-FE0F","non_qualified":"1F3CB-1F3FF-200D-2642","image":"1f3cb-1f3ff-200d-2642-fe0f.png","sheet_x":11,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F3CB-FE0F","a":"Man Lifting Weights","b":"1F3CB-FE0F-200D-2642-FE0F","k":[11,0],"o":7},"woman-lifting-weights":{"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2640-FE0F","non_qualified":"1F3CB-1F3FB-200D-2640","image":"1f3cb-1f3fb-200d-2640-fe0f.png","sheet_x":10,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2640-FE0F","non_qualified":"1F3CB-1F3FC-200D-2640","image":"1f3cb-1f3fc-200d-2640-fe0f.png","sheet_x":10,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2640-FE0F","non_qualified":"1F3CB-1F3FD-200D-2640","image":"1f3cb-1f3fd-200d-2640-fe0f.png","sheet_x":10,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2640-FE0F","non_qualified":"1F3CB-1F3FE-200D-2640","image":"1f3cb-1f3fe-200d-2640-fe0f.png","sheet_x":10,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2640-FE0F","non_qualified":"1F3CB-1F3FF-200D-2640","image":"1f3cb-1f3ff-200d-2640-fe0f.png","sheet_x":10,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Lifting Weights","b":"1F3CB-FE0F-200D-2640-FE0F","k":[10,46],"o":7},"bicyclist":{"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB","non_qualified":null,"image":"1f6b4-1f3fb.png","sheet_x":35,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F6B4-1F3FC","non_qualified":null,"image":"1f6b4-1f3fc.png","sheet_x":35,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F6B4-1F3FD","non_qualified":null,"image":"1f6b4-1f3fd.png","sheet_x":35,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F6B4-1F3FE","non_qualified":null,"image":"1f6b4-1f3fe.png","sheet_x":35,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F6B4-1F3FF","non_qualified":null,"image":"1f6b4-1f3ff.png","sheet_x":35,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F6B4-200D-2642-FE0F","a":"Bicyclist","b":"1F6B4","k":[35,37]},"man-biking":{"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2642-FE0F","non_qualified":"1F6B4-1F3FB-200D-2642","image":"1f6b4-1f3fb-200d-2642-fe0f.png","sheet_x":35,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2642-FE0F","non_qualified":"1F6B4-1F3FC-200D-2642","image":"1f6b4-1f3fc-200d-2642-fe0f.png","sheet_x":35,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2642-FE0F","non_qualified":"1F6B4-1F3FD-200D-2642","image":"1f6b4-1f3fd-200d-2642-fe0f.png","sheet_x":35,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2642-FE0F","non_qualified":"1F6B4-1F3FE-200D-2642","image":"1f6b4-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2642-FE0F","non_qualified":"1F6B4-1F3FF-200D-2642","image":"1f6b4-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F6B4","a":"Man Biking","b":"1F6B4-200D-2642-FE0F","c":"1F6B4-200D-2642","k":[35,31]},"woman-biking":{"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2640-FE0F","non_qualified":"1F6B4-1F3FB-200D-2640","image":"1f6b4-1f3fb-200d-2640-fe0f.png","sheet_x":35,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2640-FE0F","non_qualified":"1F6B4-1F3FC-200D-2640","image":"1f6b4-1f3fc-200d-2640-fe0f.png","sheet_x":35,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2640-FE0F","non_qualified":"1F6B4-1F3FD-200D-2640","image":"1f6b4-1f3fd-200d-2640-fe0f.png","sheet_x":35,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2640-FE0F","non_qualified":"1F6B4-1F3FE-200D-2640","image":"1f6b4-1f3fe-200d-2640-fe0f.png","sheet_x":35,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2640-FE0F","non_qualified":"1F6B4-1F3FF-200D-2640","image":"1f6b4-1f3ff-200d-2640-fe0f.png","sheet_x":35,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Biking","b":"1F6B4-200D-2640-FE0F","c":"1F6B4-200D-2640","k":[35,25]},"mountain_bicyclist":{"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB","non_qualified":null,"image":"1f6b5-1f3fb.png","sheet_x":36,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FC":{"unified":"1F6B5-1F3FC","non_qualified":null,"image":"1f6b5-1f3fc.png","sheet_x":36,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FD":{"unified":"1F6B5-1F3FD","non_qualified":null,"image":"1f6b5-1f3fd.png","sheet_x":36,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FE":{"unified":"1F6B5-1F3FE","non_qualified":null,"image":"1f6b5-1f3fe.png","sheet_x":36,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true},"1F3FF":{"unified":"1F6B5-1F3FF","non_qualified":null,"image":"1f6b5-1f3ff.png","sheet_x":36,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":true}},"obsoleted_by":"1F6B5-200D-2642-FE0F","a":"Mountain Bicyclist","b":"1F6B5","k":[36,3]},"man-mountain-biking":{"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2642-FE0F","non_qualified":"1F6B5-1F3FB-200D-2642","image":"1f6b5-1f3fb-200d-2642-fe0f.png","sheet_x":35,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2642-FE0F","non_qualified":"1F6B5-1F3FC-200D-2642","image":"1f6b5-1f3fc-200d-2642-fe0f.png","sheet_x":35,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2642-FE0F","non_qualified":"1F6B5-1F3FD-200D-2642","image":"1f6b5-1f3fd-200d-2642-fe0f.png","sheet_x":36,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2642-FE0F","non_qualified":"1F6B5-1F3FE-200D-2642","image":"1f6b5-1f3fe-200d-2642-fe0f.png","sheet_x":36,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2642-FE0F","non_qualified":"1F6B5-1F3FF-200D-2642","image":"1f6b5-1f3ff-200d-2642-fe0f.png","sheet_x":36,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"obsoletes":"1F6B5","a":"Man Mountain Biking","b":"1F6B5-200D-2642-FE0F","c":"1F6B5-200D-2642","k":[35,49]},"woman-mountain-biking":{"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2640-FE0F","non_qualified":"1F6B5-1F3FB-200D-2640","image":"1f6b5-1f3fb-200d-2640-fe0f.png","sheet_x":35,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2640-FE0F","non_qualified":"1F6B5-1F3FC-200D-2640","image":"1f6b5-1f3fc-200d-2640-fe0f.png","sheet_x":35,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2640-FE0F","non_qualified":"1F6B5-1F3FD-200D-2640","image":"1f6b5-1f3fd-200d-2640-fe0f.png","sheet_x":35,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2640-FE0F","non_qualified":"1F6B5-1F3FE-200D-2640","image":"1f6b5-1f3fe-200d-2640-fe0f.png","sheet_x":35,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2640-FE0F","non_qualified":"1F6B5-1F3FF-200D-2640","image":"1f6b5-1f3ff-200d-2640-fe0f.png","sheet_x":35,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Mountain Biking","b":"1F6B5-200D-2640-FE0F","c":"1F6B5-200D-2640","k":[35,43]},"racing_car":{"a":"Racing Car","b":"1F3CE-FE0F","c":"1F3CE","j":["sports","race","fast","formula","f1"],"k":[11,31],"o":7},"racing_motorcycle":{"a":"Racing Motorcycle","b":"1F3CD-FE0F","c":"1F3CD","k":[11,30],"o":7},"person_doing_cartwheel":{"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB","non_qualified":null,"image":"1f938-1f3fb.png","sheet_x":40,"sheet_y":25,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F938-1F3FC","non_qualified":null,"image":"1f938-1f3fc.png","sheet_x":40,"sheet_y":26,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F938-1F3FD","non_qualified":null,"image":"1f938-1f3fd.png","sheet_x":40,"sheet_y":27,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F938-1F3FE","non_qualified":null,"image":"1f938-1f3fe.png","sheet_x":40,"sheet_y":28,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F938-1F3FF","non_qualified":null,"image":"1f938-1f3ff.png","sheet_x":40,"sheet_y":29,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Person Doing Cartwheel","b":"1F938","k":[40,24],"o":9},"man-cartwheeling":{"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2642-FE0F","non_qualified":"1F938-1F3FB-200D-2642","image":"1f938-1f3fb-200d-2642-fe0f.png","sheet_x":40,"sheet_y":19,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F938-1F3FC-200D-2642-FE0F","non_qualified":"1F938-1F3FC-200D-2642","image":"1f938-1f3fc-200d-2642-fe0f.png","sheet_x":40,"sheet_y":20,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F938-1F3FD-200D-2642-FE0F","non_qualified":"1F938-1F3FD-200D-2642","image":"1f938-1f3fd-200d-2642-fe0f.png","sheet_x":40,"sheet_y":21,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F938-1F3FE-200D-2642-FE0F","non_qualified":"1F938-1F3FE-200D-2642","image":"1f938-1f3fe-200d-2642-fe0f.png","sheet_x":40,"sheet_y":22,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F938-1F3FF-200D-2642-FE0F","non_qualified":"1F938-1F3FF-200D-2642","image":"1f938-1f3ff-200d-2642-fe0f.png","sheet_x":40,"sheet_y":23,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Cartwheeling","b":"1F938-200D-2642-FE0F","c":"1F938-200D-2642","k":[40,18],"o":9},"woman-cartwheeling":{"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2640-FE0F","non_qualified":"1F938-1F3FB-200D-2640","image":"1f938-1f3fb-200d-2640-fe0f.png","sheet_x":40,"sheet_y":13,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F938-1F3FC-200D-2640-FE0F","non_qualified":"1F938-1F3FC-200D-2640","image":"1f938-1f3fc-200d-2640-fe0f.png","sheet_x":40,"sheet_y":14,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F938-1F3FD-200D-2640-FE0F","non_qualified":"1F938-1F3FD-200D-2640","image":"1f938-1f3fd-200d-2640-fe0f.png","sheet_x":40,"sheet_y":15,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F938-1F3FE-200D-2640-FE0F","non_qualified":"1F938-1F3FE-200D-2640","image":"1f938-1f3fe-200d-2640-fe0f.png","sheet_x":40,"sheet_y":16,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F938-1F3FF-200D-2640-FE0F","non_qualified":"1F938-1F3FF-200D-2640","image":"1f938-1f3ff-200d-2640-fe0f.png","sheet_x":40,"sheet_y":17,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Cartwheeling","b":"1F938-200D-2640-FE0F","c":"1F938-200D-2640","k":[40,12],"o":9},"wrestlers":{"a":"Wrestlers","b":"1F93C","k":[40,51],"o":9},"man-wrestling":{"a":"Man Wrestling","b":"1F93C-200D-2642-FE0F","c":"1F93C-200D-2642","k":[40,50],"o":9},"woman-wrestling":{"a":"Woman Wrestling","b":"1F93C-200D-2640-FE0F","c":"1F93C-200D-2640","k":[40,49],"o":9},"water_polo":{"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB","non_qualified":null,"image":"1f93d-1f3fb.png","sheet_x":41,"sheet_y":13,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F93D-1F3FC","non_qualified":null,"image":"1f93d-1f3fc.png","sheet_x":41,"sheet_y":14,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F93D-1F3FD","non_qualified":null,"image":"1f93d-1f3fd.png","sheet_x":41,"sheet_y":15,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F93D-1F3FE","non_qualified":null,"image":"1f93d-1f3fe.png","sheet_x":41,"sheet_y":16,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F93D-1F3FF","non_qualified":null,"image":"1f93d-1f3ff.png","sheet_x":41,"sheet_y":17,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Water Polo","b":"1F93D","k":[41,12],"o":9},"man-playing-water-polo":{"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2642-FE0F","non_qualified":"1F93D-1F3FB-200D-2642","image":"1f93d-1f3fb-200d-2642-fe0f.png","sheet_x":41,"sheet_y":7,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F93D-1F3FC-200D-2642-FE0F","non_qualified":"1F93D-1F3FC-200D-2642","image":"1f93d-1f3fc-200d-2642-fe0f.png","sheet_x":41,"sheet_y":8,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F93D-1F3FD-200D-2642-FE0F","non_qualified":"1F93D-1F3FD-200D-2642","image":"1f93d-1f3fd-200d-2642-fe0f.png","sheet_x":41,"sheet_y":9,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F93D-1F3FE-200D-2642-FE0F","non_qualified":"1F93D-1F3FE-200D-2642","image":"1f93d-1f3fe-200d-2642-fe0f.png","sheet_x":41,"sheet_y":10,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F93D-1F3FF-200D-2642-FE0F","non_qualified":"1F93D-1F3FF-200D-2642","image":"1f93d-1f3ff-200d-2642-fe0f.png","sheet_x":41,"sheet_y":11,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Playing Water Polo","b":"1F93D-200D-2642-FE0F","c":"1F93D-200D-2642","k":[41,6],"o":9},"woman-playing-water-polo":{"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2640-FE0F","non_qualified":"1F93D-1F3FB-200D-2640","image":"1f93d-1f3fb-200d-2640-fe0f.png","sheet_x":41,"sheet_y":1,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F93D-1F3FC-200D-2640-FE0F","non_qualified":"1F93D-1F3FC-200D-2640","image":"1f93d-1f3fc-200d-2640-fe0f.png","sheet_x":41,"sheet_y":2,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F93D-1F3FD-200D-2640-FE0F","non_qualified":"1F93D-1F3FD-200D-2640","image":"1f93d-1f3fd-200d-2640-fe0f.png","sheet_x":41,"sheet_y":3,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F93D-1F3FE-200D-2640-FE0F","non_qualified":"1F93D-1F3FE-200D-2640","image":"1f93d-1f3fe-200d-2640-fe0f.png","sheet_x":41,"sheet_y":4,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F93D-1F3FF-200D-2640-FE0F","non_qualified":"1F93D-1F3FF-200D-2640","image":"1f93d-1f3ff-200d-2640-fe0f.png","sheet_x":41,"sheet_y":5,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Playing Water Polo","b":"1F93D-200D-2640-FE0F","c":"1F93D-200D-2640","k":[41,0],"o":9},"handball":{"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB","non_qualified":null,"image":"1f93e-1f3fb.png","sheet_x":41,"sheet_y":31,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F93E-1F3FC","non_qualified":null,"image":"1f93e-1f3fc.png","sheet_x":41,"sheet_y":32,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F93E-1F3FD","non_qualified":null,"image":"1f93e-1f3fd.png","sheet_x":41,"sheet_y":33,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F93E-1F3FE","non_qualified":null,"image":"1f93e-1f3fe.png","sheet_x":41,"sheet_y":34,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F93E-1F3FF","non_qualified":null,"image":"1f93e-1f3ff.png","sheet_x":41,"sheet_y":35,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Handball","b":"1F93E","k":[41,30],"o":9},"man-playing-handball":{"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2642-FE0F","non_qualified":"1F93E-1F3FB-200D-2642","image":"1f93e-1f3fb-200d-2642-fe0f.png","sheet_x":41,"sheet_y":25,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F93E-1F3FC-200D-2642-FE0F","non_qualified":"1F93E-1F3FC-200D-2642","image":"1f93e-1f3fc-200d-2642-fe0f.png","sheet_x":41,"sheet_y":26,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F93E-1F3FD-200D-2642-FE0F","non_qualified":"1F93E-1F3FD-200D-2642","image":"1f93e-1f3fd-200d-2642-fe0f.png","sheet_x":41,"sheet_y":27,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F93E-1F3FE-200D-2642-FE0F","non_qualified":"1F93E-1F3FE-200D-2642","image":"1f93e-1f3fe-200d-2642-fe0f.png","sheet_x":41,"sheet_y":28,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F93E-1F3FF-200D-2642-FE0F","non_qualified":"1F93E-1F3FF-200D-2642","image":"1f93e-1f3ff-200d-2642-fe0f.png","sheet_x":41,"sheet_y":29,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Playing Handball","b":"1F93E-200D-2642-FE0F","c":"1F93E-200D-2642","k":[41,24],"o":9},"woman-playing-handball":{"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2640-FE0F","non_qualified":"1F93E-1F3FB-200D-2640","image":"1f93e-1f3fb-200d-2640-fe0f.png","sheet_x":41,"sheet_y":19,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F93E-1F3FC-200D-2640-FE0F","non_qualified":"1F93E-1F3FC-200D-2640","image":"1f93e-1f3fc-200d-2640-fe0f.png","sheet_x":41,"sheet_y":20,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F93E-1F3FD-200D-2640-FE0F","non_qualified":"1F93E-1F3FD-200D-2640","image":"1f93e-1f3fd-200d-2640-fe0f.png","sheet_x":41,"sheet_y":21,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F93E-1F3FE-200D-2640-FE0F","non_qualified":"1F93E-1F3FE-200D-2640","image":"1f93e-1f3fe-200d-2640-fe0f.png","sheet_x":41,"sheet_y":22,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F93E-1F3FF-200D-2640-FE0F","non_qualified":"1F93E-1F3FF-200D-2640","image":"1f93e-1f3ff-200d-2640-fe0f.png","sheet_x":41,"sheet_y":23,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Playing Handball","b":"1F93E-200D-2640-FE0F","c":"1F93E-200D-2640","k":[41,18],"o":9},"juggling":{"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB","non_qualified":null,"image":"1f939-1f3fb.png","sheet_x":40,"sheet_y":43,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F939-1F3FC","non_qualified":null,"image":"1f939-1f3fc.png","sheet_x":40,"sheet_y":44,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F939-1F3FD","non_qualified":null,"image":"1f939-1f3fd.png","sheet_x":40,"sheet_y":45,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F939-1F3FE","non_qualified":null,"image":"1f939-1f3fe.png","sheet_x":40,"sheet_y":46,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F939-1F3FF","non_qualified":null,"image":"1f939-1f3ff.png","sheet_x":40,"sheet_y":47,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Juggling","b":"1F939","k":[40,42],"o":9},"man-juggling":{"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2642-FE0F","non_qualified":"1F939-1F3FB-200D-2642","image":"1f939-1f3fb-200d-2642-fe0f.png","sheet_x":40,"sheet_y":37,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F939-1F3FC-200D-2642-FE0F","non_qualified":"1F939-1F3FC-200D-2642","image":"1f939-1f3fc-200d-2642-fe0f.png","sheet_x":40,"sheet_y":38,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F939-1F3FD-200D-2642-FE0F","non_qualified":"1F939-1F3FD-200D-2642","image":"1f939-1f3fd-200d-2642-fe0f.png","sheet_x":40,"sheet_y":39,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F939-1F3FE-200D-2642-FE0F","non_qualified":"1F939-1F3FE-200D-2642","image":"1f939-1f3fe-200d-2642-fe0f.png","sheet_x":40,"sheet_y":40,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F939-1F3FF-200D-2642-FE0F","non_qualified":"1F939-1F3FF-200D-2642","image":"1f939-1f3ff-200d-2642-fe0f.png","sheet_x":40,"sheet_y":41,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Man Juggling","b":"1F939-200D-2642-FE0F","c":"1F939-200D-2642","k":[40,36],"o":9},"woman-juggling":{"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2640-FE0F","non_qualified":"1F939-1F3FB-200D-2640","image":"1f939-1f3fb-200d-2640-fe0f.png","sheet_x":40,"sheet_y":31,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FC":{"unified":"1F939-1F3FC-200D-2640-FE0F","non_qualified":"1F939-1F3FC-200D-2640","image":"1f939-1f3fc-200d-2640-fe0f.png","sheet_x":40,"sheet_y":32,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FD":{"unified":"1F939-1F3FD-200D-2640-FE0F","non_qualified":"1F939-1F3FD-200D-2640","image":"1f939-1f3fd-200d-2640-fe0f.png","sheet_x":40,"sheet_y":33,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FE":{"unified":"1F939-1F3FE-200D-2640-FE0F","non_qualified":"1F939-1F3FE-200D-2640","image":"1f939-1f3fe-200d-2640-fe0f.png","sheet_x":40,"sheet_y":34,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false},"1F3FF":{"unified":"1F939-1F3FF-200D-2640-FE0F","non_qualified":"1F939-1F3FF-200D-2640","image":"1f939-1f3ff-200d-2640-fe0f.png","sheet_x":40,"sheet_y":35,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":false,"has_img_messenger":false}},"a":"Woman Juggling","b":"1F939-200D-2640-FE0F","c":"1F939-200D-2640","k":[40,30],"o":9},"couple":{"a":"Man and Woman Holding Hands","b":"1F46B","j":["pair","people","human","love","date","dating","like","affection","valentines","marriage"],"k":[20,30],"n":["man_and_woman_holding_hands"]},"two_men_holding_hands":{"a":"Two Men Holding Hands","b":"1F46C","j":["pair","couple","love","like","bromance","friendship","people","human"],"k":[20,31]},"two_women_holding_hands":{"a":"Two Women Holding Hands","b":"1F46D","j":["pair","friendship","couple","love","like","female","people","human"],"k":[20,32]},"couplekiss":{"obsoleted_by":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","a":"Kiss","b":"1F48F","k":[24,41]},"woman-kiss-man":{"obsoletes":"1F48F","a":"Woman Kiss Man","b":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","c":"1F469-200D-2764-200D-1F48B-200D-1F468","k":[20,21]},"man-kiss-man":{"a":"Man Kiss Man","b":"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468","c":"1F468-200D-2764-200D-1F48B-200D-1F468","k":[18,10]},"woman-kiss-woman":{"a":"Woman Kiss Woman","b":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469","c":"1F469-200D-2764-200D-1F48B-200D-1F469","k":[20,22]},"couple_with_heart":{"obsoleted_by":"1F469-200D-2764-FE0F-200D-1F468","a":"Couple with Heart","b":"1F491","k":[24,43]},"woman-heart-man":{"obsoletes":"1F491","a":"Woman Heart Man","b":"1F469-200D-2764-FE0F-200D-1F468","c":"1F469-200D-2764-200D-1F468","k":[20,19]},"man-heart-man":{"a":"Man Heart Man","b":"1F468-200D-2764-FE0F-200D-1F468","c":"1F468-200D-2764-200D-1F468","k":[18,9]},"woman-heart-woman":{"a":"Woman Heart Woman","b":"1F469-200D-2764-FE0F-200D-1F469","c":"1F469-200D-2764-200D-1F469","k":[20,20]},"family":{"obsoleted_by":"1F468-200D-1F469-200D-1F466","a":"Family","b":"1F46A","k":[20,29],"n":["man-woman-boy"]},"man-woman-boy":{"obsoletes":"1F46A","a":"Man Woman Boy","b":"1F468-200D-1F469-200D-1F466","k":[17,2],"n":["family"]},"man-woman-girl":{"a":"Man Woman Girl","b":"1F468-200D-1F469-200D-1F467","k":[17,4]},"man-woman-girl-boy":{"a":"Man Woman Girl Boy","b":"1F468-200D-1F469-200D-1F467-200D-1F466","k":[17,5]},"man-woman-boy-boy":{"a":"Man Woman Boy Boy","b":"1F468-200D-1F469-200D-1F466-200D-1F466","k":[17,3]},"man-woman-girl-girl":{"a":"Man Woman Girl Girl","b":"1F468-200D-1F469-200D-1F467-200D-1F467","k":[17,6]},"man-man-boy":{"a":"Man Man Boy","b":"1F468-200D-1F468-200D-1F466","k":[16,49]},"man-man-girl":{"a":"Man Man Girl","b":"1F468-200D-1F468-200D-1F467","k":[16,51]},"man-man-girl-boy":{"a":"Man Man Girl Boy","b":"1F468-200D-1F468-200D-1F467-200D-1F466","k":[17,0]},"man-man-boy-boy":{"a":"Man Man Boy Boy","b":"1F468-200D-1F468-200D-1F466-200D-1F466","k":[16,50]},"man-man-girl-girl":{"a":"Man Man Girl Girl","b":"1F468-200D-1F468-200D-1F467-200D-1F467","k":[17,1]},"woman-woman-boy":{"a":"Woman Woman Boy","b":"1F469-200D-1F469-200D-1F466","k":[19,12]},"woman-woman-girl":{"a":"Woman Woman Girl","b":"1F469-200D-1F469-200D-1F467","k":[19,14]},"woman-woman-girl-boy":{"a":"Woman Woman Girl Boy","b":"1F469-200D-1F469-200D-1F467-200D-1F466","k":[19,15]},"woman-woman-boy-boy":{"a":"Woman Woman Boy Boy","b":"1F469-200D-1F469-200D-1F466-200D-1F466","k":[19,13]},"woman-woman-girl-girl":{"a":"Woman Woman Girl Girl","b":"1F469-200D-1F469-200D-1F467-200D-1F467","k":[19,16]},"man-boy":{"a":"Man Boy","b":"1F468-200D-1F466","k":[16,45]},"man-boy-boy":{"a":"Man Boy Boy","b":"1F468-200D-1F466-200D-1F466","k":[16,44]},"man-girl":{"a":"Man Girl","b":"1F468-200D-1F467","k":[16,48]},"man-girl-boy":{"a":"Man Girl Boy","b":"1F468-200D-1F467-200D-1F466","k":[16,46]},"man-girl-girl":{"a":"Man Girl Girl","b":"1F468-200D-1F467-200D-1F467","k":[16,47]},"woman-boy":{"a":"Woman Boy","b":"1F469-200D-1F466","k":[19,8]},"woman-boy-boy":{"a":"Woman Boy Boy","b":"1F469-200D-1F466-200D-1F466","k":[19,7]},"woman-girl":{"a":"Woman Girl","b":"1F469-200D-1F467","k":[19,11]},"woman-girl-boy":{"a":"Woman Girl Boy","b":"1F469-200D-1F467-200D-1F466","k":[19,9]},"woman-girl-girl":{"a":"Woman Girl Girl","b":"1F469-200D-1F467-200D-1F467","k":[19,10]},"selfie":{"skin_variations":{"1F3FB":{"unified":"1F933-1F3FB","non_qualified":null,"image":"1f933-1f3fb.png","sheet_x":39,"sheet_y":23,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F933-1F3FC","non_qualified":null,"image":"1f933-1f3fc.png","sheet_x":39,"sheet_y":24,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F933-1F3FD","non_qualified":null,"image":"1f933-1f3fd.png","sheet_x":39,"sheet_y":25,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F933-1F3FE","non_qualified":null,"image":"1f933-1f3fe.png","sheet_x":39,"sheet_y":26,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F933-1F3FF","non_qualified":null,"image":"1f933-1f3ff.png","sheet_x":39,"sheet_y":27,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Selfie","b":"1F933","j":["camera","phone"],"k":[39,22],"o":9},"muscle":{"skin_variations":{"1F3FB":{"unified":"1F4AA-1F3FB","non_qualified":null,"image":"1f4aa-1f3fb.png","sheet_x":25,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F4AA-1F3FC","non_qualified":null,"image":"1f4aa-1f3fc.png","sheet_x":25,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F4AA-1F3FD","non_qualified":null,"image":"1f4aa-1f3fd.png","sheet_x":25,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F4AA-1F3FE","non_qualified":null,"image":"1f4aa-1f3fe.png","sheet_x":25,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F4AA-1F3FF","non_qualified":null,"image":"1f4aa-1f3ff.png","sheet_x":25,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Flexed Biceps","b":"1F4AA","j":["arm","flex","hand","summer","strong","biceps"],"k":[25,16]},"point_left":{"skin_variations":{"1F3FB":{"unified":"1F448-1F3FB","non_qualified":null,"image":"1f448-1f3fb.png","sheet_x":14,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F448-1F3FC","non_qualified":null,"image":"1f448-1f3fc.png","sheet_x":14,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F448-1F3FD","non_qualified":null,"image":"1f448-1f3fd.png","sheet_x":14,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F448-1F3FE","non_qualified":null,"image":"1f448-1f3fe.png","sheet_x":14,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F448-1F3FF","non_qualified":null,"image":"1f448-1f3ff.png","sheet_x":14,"sheet_y":24,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"White Left Pointing Backhand Index","b":"1F448","j":["direction","fingers","hand","left"],"k":[14,19]},"point_right":{"skin_variations":{"1F3FB":{"unified":"1F449-1F3FB","non_qualified":null,"image":"1f449-1f3fb.png","sheet_x":14,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F449-1F3FC","non_qualified":null,"image":"1f449-1f3fc.png","sheet_x":14,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F449-1F3FD","non_qualified":null,"image":"1f449-1f3fd.png","sheet_x":14,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F449-1F3FE","non_qualified":null,"image":"1f449-1f3fe.png","sheet_x":14,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F449-1F3FF","non_qualified":null,"image":"1f449-1f3ff.png","sheet_x":14,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"White Right Pointing Backhand Index","b":"1F449","j":["fingers","hand","direction","right"],"k":[14,25]},"point_up":{"skin_variations":{"1F3FB":{"unified":"261D-1F3FB","non_qualified":null,"image":"261d-1f3fb.png","sheet_x":47,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"261D-1F3FC","non_qualified":null,"image":"261d-1f3fc.png","sheet_x":47,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"261D-1F3FD","non_qualified":null,"image":"261d-1f3fd.png","sheet_x":47,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"261D-1F3FE","non_qualified":null,"image":"261d-1f3fe.png","sheet_x":47,"sheet_y":30,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"261D-1F3FF","non_qualified":null,"image":"261d-1f3ff.png","sheet_x":47,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"White Up Pointing Index","b":"261D-FE0F","c":"261D","j":["hand","fingers","direction","up"],"k":[47,26],"o":1},"point_up_2":{"skin_variations":{"1F3FB":{"unified":"1F446-1F3FB","non_qualified":null,"image":"1f446-1f3fb.png","sheet_x":14,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F446-1F3FC","non_qualified":null,"image":"1f446-1f3fc.png","sheet_x":14,"sheet_y":9,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F446-1F3FD","non_qualified":null,"image":"1f446-1f3fd.png","sheet_x":14,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F446-1F3FE","non_qualified":null,"image":"1f446-1f3fe.png","sheet_x":14,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F446-1F3FF","non_qualified":null,"image":"1f446-1f3ff.png","sheet_x":14,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"White Up Pointing Backhand Index","b":"1F446","j":["fingers","hand","direction","up"],"k":[14,7]},"middle_finger":{"skin_variations":{"1F3FB":{"unified":"1F595-1F3FB","non_qualified":null,"image":"1f595-1f3fb.png","sheet_x":29,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F595-1F3FC","non_qualified":null,"image":"1f595-1f3fc.png","sheet_x":29,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F595-1F3FD","non_qualified":null,"image":"1f595-1f3fd.png","sheet_x":29,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F595-1F3FE","non_qualified":null,"image":"1f595-1f3fe.png","sheet_x":29,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F595-1F3FF","non_qualified":null,"image":"1f595-1f3ff.png","sheet_x":29,"sheet_y":43,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Reversed Hand with Middle Finger Extended","b":"1F595","k":[29,38],"n":["reversed_hand_with_middle_finger_extended"],"o":7},"point_down":{"skin_variations":{"1F3FB":{"unified":"1F447-1F3FB","non_qualified":null,"image":"1f447-1f3fb.png","sheet_x":14,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F447-1F3FC","non_qualified":null,"image":"1f447-1f3fc.png","sheet_x":14,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F447-1F3FD","non_qualified":null,"image":"1f447-1f3fd.png","sheet_x":14,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F447-1F3FE","non_qualified":null,"image":"1f447-1f3fe.png","sheet_x":14,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F447-1F3FF","non_qualified":null,"image":"1f447-1f3ff.png","sheet_x":14,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"White Down Pointing Backhand Index","b":"1F447","j":["fingers","hand","direction","down"],"k":[14,13]},"v":{"skin_variations":{"1F3FB":{"unified":"270C-1F3FB","non_qualified":null,"image":"270c-1f3fb.png","sheet_x":49,"sheet_y":31,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"270C-1F3FC","non_qualified":null,"image":"270c-1f3fc.png","sheet_x":49,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"270C-1F3FD","non_qualified":null,"image":"270c-1f3fd.png","sheet_x":49,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"270C-1F3FE","non_qualified":null,"image":"270c-1f3fe.png","sheet_x":49,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"270C-1F3FF","non_qualified":null,"image":"270c-1f3ff.png","sheet_x":49,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Victory Hand","b":"270C-FE0F","c":"270C","j":["fingers","ohyeah","hand","peace","victory","two"],"k":[49,30],"o":1},"crossed_fingers":{"skin_variations":{"1F3FB":{"unified":"1F91E-1F3FB","non_qualified":null,"image":"1f91e-1f3fb.png","sheet_x":38,"sheet_y":12,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F91E-1F3FC","non_qualified":null,"image":"1f91e-1f3fc.png","sheet_x":38,"sheet_y":13,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F91E-1F3FD","non_qualified":null,"image":"1f91e-1f3fd.png","sheet_x":38,"sheet_y":14,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F91E-1F3FE","non_qualified":null,"image":"1f91e-1f3fe.png","sheet_x":38,"sheet_y":15,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F91E-1F3FF","non_qualified":null,"image":"1f91e-1f3ff.png","sheet_x":38,"sheet_y":16,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Hand with Index and Middle Fingers Crossed","b":"1F91E","j":["good","lucky"],"k":[38,11],"n":["hand_with_index_and_middle_fingers_crossed"],"o":9},"spock-hand":{"skin_variations":{"1F3FB":{"unified":"1F596-1F3FB","non_qualified":null,"image":"1f596-1f3fb.png","sheet_x":29,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F596-1F3FC","non_qualified":null,"image":"1f596-1f3fc.png","sheet_x":29,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F596-1F3FD","non_qualified":null,"image":"1f596-1f3fd.png","sheet_x":29,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F596-1F3FE","non_qualified":null,"image":"1f596-1f3fe.png","sheet_x":29,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F596-1F3FF","non_qualified":null,"image":"1f596-1f3ff.png","sheet_x":29,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Raised Hand with Part Between Middle and Ring Fingers","b":"1F596","k":[29,44],"o":7},"the_horns":{"skin_variations":{"1F3FB":{"unified":"1F918-1F3FB","non_qualified":null,"image":"1f918-1f3fb.png","sheet_x":37,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F918-1F3FC","non_qualified":null,"image":"1f918-1f3fc.png","sheet_x":37,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F918-1F3FD","non_qualified":null,"image":"1f918-1f3fd.png","sheet_x":37,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F918-1F3FE","non_qualified":null,"image":"1f918-1f3fe.png","sheet_x":37,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F918-1F3FF","non_qualified":null,"image":"1f918-1f3ff.png","sheet_x":37,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Sign of the Horns","b":"1F918","k":[37,32],"n":["sign_of_the_horns"],"o":8},"call_me_hand":{"skin_variations":{"1F3FB":{"unified":"1F919-1F3FB","non_qualified":null,"image":"1f919-1f3fb.png","sheet_x":37,"sheet_y":39,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F919-1F3FC","non_qualified":null,"image":"1f919-1f3fc.png","sheet_x":37,"sheet_y":40,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F919-1F3FD","non_qualified":null,"image":"1f919-1f3fd.png","sheet_x":37,"sheet_y":41,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F919-1F3FE","non_qualified":null,"image":"1f919-1f3fe.png","sheet_x":37,"sheet_y":42,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F919-1F3FF","non_qualified":null,"image":"1f919-1f3ff.png","sheet_x":37,"sheet_y":43,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Call Me Hand","b":"1F919","j":["hands","gesture"],"k":[37,38],"o":9},"raised_hand_with_fingers_splayed":{"skin_variations":{"1F3FB":{"unified":"1F590-1F3FB","non_qualified":null,"image":"1f590-1f3fb.png","sheet_x":29,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F590-1F3FC","non_qualified":null,"image":"1f590-1f3fc.png","sheet_x":29,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F590-1F3FD","non_qualified":null,"image":"1f590-1f3fd.png","sheet_x":29,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F590-1F3FE","non_qualified":null,"image":"1f590-1f3fe.png","sheet_x":29,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F590-1F3FF","non_qualified":null,"image":"1f590-1f3ff.png","sheet_x":29,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Raised Hand with Fingers Splayed","b":"1F590-FE0F","c":"1F590","j":["hand","fingers","palm"],"k":[29,32],"o":7},"hand":{"skin_variations":{"1F3FB":{"unified":"270B-1F3FB","non_qualified":null,"image":"270b-1f3fb.png","sheet_x":49,"sheet_y":25,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"270B-1F3FC","non_qualified":null,"image":"270b-1f3fc.png","sheet_x":49,"sheet_y":26,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"270B-1F3FD","non_qualified":null,"image":"270b-1f3fd.png","sheet_x":49,"sheet_y":27,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"270B-1F3FE","non_qualified":null,"image":"270b-1f3fe.png","sheet_x":49,"sheet_y":28,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"270B-1F3FF","non_qualified":null,"image":"270b-1f3ff.png","sheet_x":49,"sheet_y":29,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Raised Hand","b":"270B","k":[49,24],"n":["raised_hand"]},"ok_hand":{"skin_variations":{"1F3FB":{"unified":"1F44C-1F3FB","non_qualified":null,"image":"1f44c-1f3fb.png","sheet_x":14,"sheet_y":44,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F44C-1F3FC","non_qualified":null,"image":"1f44c-1f3fc.png","sheet_x":14,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F44C-1F3FD","non_qualified":null,"image":"1f44c-1f3fd.png","sheet_x":14,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F44C-1F3FE","non_qualified":null,"image":"1f44c-1f3fe.png","sheet_x":14,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F44C-1F3FF","non_qualified":null,"image":"1f44c-1f3ff.png","sheet_x":14,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Ok Hand Sign","b":"1F44C","j":["fingers","limbs","perfect","ok","okay"],"k":[14,43]},"+1":{"skin_variations":{"1F3FB":{"unified":"1F44D-1F3FB","non_qualified":null,"image":"1f44d-1f3fb.png","sheet_x":14,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F44D-1F3FC","non_qualified":null,"image":"1f44d-1f3fc.png","sheet_x":14,"sheet_y":51,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F44D-1F3FD","non_qualified":null,"image":"1f44d-1f3fd.png","sheet_x":15,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F44D-1F3FE","non_qualified":null,"image":"1f44d-1f3fe.png","sheet_x":15,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F44D-1F3FF","non_qualified":null,"image":"1f44d-1f3ff.png","sheet_x":15,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Thumbs Up Sign","b":"1F44D","j":["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],"k":[14,49],"n":["thumbsup"]},"-1":{"skin_variations":{"1F3FB":{"unified":"1F44E-1F3FB","non_qualified":null,"image":"1f44e-1f3fb.png","sheet_x":15,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F44E-1F3FC","non_qualified":null,"image":"1f44e-1f3fc.png","sheet_x":15,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F44E-1F3FD","non_qualified":null,"image":"1f44e-1f3fd.png","sheet_x":15,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F44E-1F3FE","non_qualified":null,"image":"1f44e-1f3fe.png","sheet_x":15,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F44E-1F3FF","non_qualified":null,"image":"1f44e-1f3ff.png","sheet_x":15,"sheet_y":8,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Thumbs Down Sign","b":"1F44E","j":["thumbsdown","no","dislike","hand"],"k":[15,3],"n":["thumbsdown"]},"fist":{"skin_variations":{"1F3FB":{"unified":"270A-1F3FB","non_qualified":null,"image":"270a-1f3fb.png","sheet_x":49,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"270A-1F3FC","non_qualified":null,"image":"270a-1f3fc.png","sheet_x":49,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"270A-1F3FD","non_qualified":null,"image":"270a-1f3fd.png","sheet_x":49,"sheet_y":21,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"270A-1F3FE","non_qualified":null,"image":"270a-1f3fe.png","sheet_x":49,"sheet_y":22,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"270A-1F3FF","non_qualified":null,"image":"270a-1f3ff.png","sheet_x":49,"sheet_y":23,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Raised Fist","b":"270A","j":["fingers","hand","grasp"],"k":[49,18]},"facepunch":{"skin_variations":{"1F3FB":{"unified":"1F44A-1F3FB","non_qualified":null,"image":"1f44a-1f3fb.png","sheet_x":14,"sheet_y":32,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F44A-1F3FC","non_qualified":null,"image":"1f44a-1f3fc.png","sheet_x":14,"sheet_y":33,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F44A-1F3FD","non_qualified":null,"image":"1f44a-1f3fd.png","sheet_x":14,"sheet_y":34,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F44A-1F3FE","non_qualified":null,"image":"1f44a-1f3fe.png","sheet_x":14,"sheet_y":35,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F44A-1F3FF","non_qualified":null,"image":"1f44a-1f3ff.png","sheet_x":14,"sheet_y":36,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Fisted Hand Sign","b":"1F44A","j":["angry","violence","fist","hit","attack","hand"],"k":[14,31],"n":["punch"]},"left-facing_fist":{"skin_variations":{"1F3FB":{"unified":"1F91B-1F3FB","non_qualified":null,"image":"1f91b-1f3fb.png","sheet_x":37,"sheet_y":51,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F91B-1F3FC","non_qualified":null,"image":"1f91b-1f3fc.png","sheet_x":38,"sheet_y":0,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F91B-1F3FD","non_qualified":null,"image":"1f91b-1f3fd.png","sheet_x":38,"sheet_y":1,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F91B-1F3FE","non_qualified":null,"image":"1f91b-1f3fe.png","sheet_x":38,"sheet_y":2,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F91B-1F3FF","non_qualified":null,"image":"1f91b-1f3ff.png","sheet_x":38,"sheet_y":3,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Left-Facing Fist","b":"1F91B","k":[37,50],"o":9},"right-facing_fist":{"skin_variations":{"1F3FB":{"unified":"1F91C-1F3FB","non_qualified":null,"image":"1f91c-1f3fb.png","sheet_x":38,"sheet_y":5,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F91C-1F3FC","non_qualified":null,"image":"1f91c-1f3fc.png","sheet_x":38,"sheet_y":6,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F91C-1F3FD","non_qualified":null,"image":"1f91c-1f3fd.png","sheet_x":38,"sheet_y":7,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F91C-1F3FE","non_qualified":null,"image":"1f91c-1f3fe.png","sheet_x":38,"sheet_y":8,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F91C-1F3FF","non_qualified":null,"image":"1f91c-1f3ff.png","sheet_x":38,"sheet_y":9,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Right-Facing Fist","b":"1F91C","k":[38,4],"o":9},"raised_back_of_hand":{"skin_variations":{"1F3FB":{"unified":"1F91A-1F3FB","non_qualified":null,"image":"1f91a-1f3fb.png","sheet_x":37,"sheet_y":45,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F91A-1F3FC","non_qualified":null,"image":"1f91a-1f3fc.png","sheet_x":37,"sheet_y":46,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F91A-1F3FD","non_qualified":null,"image":"1f91a-1f3fd.png","sheet_x":37,"sheet_y":47,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F91A-1F3FE","non_qualified":null,"image":"1f91a-1f3fe.png","sheet_x":37,"sheet_y":48,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F91A-1F3FF","non_qualified":null,"image":"1f91a-1f3ff.png","sheet_x":37,"sheet_y":49,"added_in":"9.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Raised Back of Hand","b":"1F91A","j":["fingers","raised","backhand"],"k":[37,44],"o":9},"wave":{"skin_variations":{"1F3FB":{"unified":"1F44B-1F3FB","non_qualified":null,"image":"1f44b-1f3fb.png","sheet_x":14,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F44B-1F3FC","non_qualified":null,"image":"1f44b-1f3fc.png","sheet_x":14,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F44B-1F3FD","non_qualified":null,"image":"1f44b-1f3fd.png","sheet_x":14,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F44B-1F3FE","non_qualified":null,"image":"1f44b-1f3fe.png","sheet_x":14,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F44B-1F3FF","non_qualified":null,"image":"1f44b-1f3ff.png","sheet_x":14,"sheet_y":42,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Waving Hand Sign","b":"1F44B","j":["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],"k":[14,37]},"i_love_you_hand_sign":{"skin_variations":{"1F3FB":{"unified":"1F91F-1F3FB","non_qualified":null,"image":"1f91f-1f3fb.png","sheet_x":38,"sheet_y":18,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F91F-1F3FC","non_qualified":null,"image":"1f91f-1f3fc.png","sheet_x":38,"sheet_y":19,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F91F-1F3FD","non_qualified":null,"image":"1f91f-1f3fd.png","sheet_x":38,"sheet_y":20,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F91F-1F3FE","non_qualified":null,"image":"1f91f-1f3fe.png","sheet_x":38,"sheet_y":21,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F91F-1F3FF","non_qualified":null,"image":"1f91f-1f3ff.png","sheet_x":38,"sheet_y":22,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"I Love You Hand Sign","b":"1F91F","k":[38,17],"o":10},"writing_hand":{"skin_variations":{"1F3FB":{"unified":"270D-1F3FB","non_qualified":null,"image":"270d-1f3fb.png","sheet_x":49,"sheet_y":37,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"270D-1F3FC","non_qualified":null,"image":"270d-1f3fc.png","sheet_x":49,"sheet_y":38,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"270D-1F3FD","non_qualified":null,"image":"270d-1f3fd.png","sheet_x":49,"sheet_y":39,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"270D-1F3FE","non_qualified":null,"image":"270d-1f3fe.png","sheet_x":49,"sheet_y":40,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"270D-1F3FF","non_qualified":null,"image":"270d-1f3ff.png","sheet_x":49,"sheet_y":41,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Writing Hand","b":"270D-FE0F","c":"270D","j":["lower_left_ballpoint_pen","stationery","write","compose"],"k":[49,36],"o":1},"clap":{"skin_variations":{"1F3FB":{"unified":"1F44F-1F3FB","non_qualified":null,"image":"1f44f-1f3fb.png","sheet_x":15,"sheet_y":10,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F44F-1F3FC","non_qualified":null,"image":"1f44f-1f3fc.png","sheet_x":15,"sheet_y":11,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F44F-1F3FD","non_qualified":null,"image":"1f44f-1f3fd.png","sheet_x":15,"sheet_y":12,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F44F-1F3FE","non_qualified":null,"image":"1f44f-1f3fe.png","sheet_x":15,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F44F-1F3FF","non_qualified":null,"image":"1f44f-1f3ff.png","sheet_x":15,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Clapping Hands Sign","b":"1F44F","j":["hands","praise","applause","congrats","yay"],"k":[15,9]},"open_hands":{"skin_variations":{"1F3FB":{"unified":"1F450-1F3FB","non_qualified":null,"image":"1f450-1f3fb.png","sheet_x":15,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F450-1F3FC","non_qualified":null,"image":"1f450-1f3fc.png","sheet_x":15,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F450-1F3FD","non_qualified":null,"image":"1f450-1f3fd.png","sheet_x":15,"sheet_y":18,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F450-1F3FE","non_qualified":null,"image":"1f450-1f3fe.png","sheet_x":15,"sheet_y":19,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F450-1F3FF","non_qualified":null,"image":"1f450-1f3ff.png","sheet_x":15,"sheet_y":20,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Open Hands Sign","b":"1F450","j":["fingers","butterfly","hands","open"],"k":[15,15]},"raised_hands":{"skin_variations":{"1F3FB":{"unified":"1F64C-1F3FB","non_qualified":null,"image":"1f64c-1f3fb.png","sheet_x":33,"sheet_y":13,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F64C-1F3FC","non_qualified":null,"image":"1f64c-1f3fc.png","sheet_x":33,"sheet_y":14,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F64C-1F3FD","non_qualified":null,"image":"1f64c-1f3fd.png","sheet_x":33,"sheet_y":15,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F64C-1F3FE","non_qualified":null,"image":"1f64c-1f3fe.png","sheet_x":33,"sheet_y":16,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F64C-1F3FF","non_qualified":null,"image":"1f64c-1f3ff.png","sheet_x":33,"sheet_y":17,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Person Raising Both Hands in Celebration","b":"1F64C","j":["gesture","hooray","yea","celebration","hands"],"k":[33,12]},"palms_up_together":{"skin_variations":{"1F3FB":{"unified":"1F932-1F3FB","non_qualified":null,"image":"1f932-1f3fb.png","sheet_x":39,"sheet_y":17,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FC":{"unified":"1F932-1F3FC","non_qualified":null,"image":"1f932-1f3fc.png","sheet_x":39,"sheet_y":18,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FD":{"unified":"1F932-1F3FD","non_qualified":null,"image":"1f932-1f3fd.png","sheet_x":39,"sheet_y":19,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FE":{"unified":"1F932-1F3FE","non_qualified":null,"image":"1f932-1f3fe.png","sheet_x":39,"sheet_y":20,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false},"1F3FF":{"unified":"1F932-1F3FF","non_qualified":null,"image":"1f932-1f3ff.png","sheet_x":39,"sheet_y":21,"added_in":"10.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":false}},"a":"Palms Up Together","b":"1F932","k":[39,16],"o":10},"pray":{"skin_variations":{"1F3FB":{"unified":"1F64F-1F3FB","non_qualified":null,"image":"1f64f-1f3fb.png","sheet_x":34,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F64F-1F3FC","non_qualified":null,"image":"1f64f-1f3fc.png","sheet_x":34,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F64F-1F3FD","non_qualified":null,"image":"1f64f-1f3fd.png","sheet_x":34,"sheet_y":5,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F64F-1F3FE","non_qualified":null,"image":"1f64f-1f3fe.png","sheet_x":34,"sheet_y":6,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F64F-1F3FF","non_qualified":null,"image":"1f64f-1f3ff.png","sheet_x":34,"sheet_y":7,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Person with Folded Hands","b":"1F64F","j":["please","hope","wish","namaste","highfive"],"k":[34,2]},"handshake":{"a":"Handshake","b":"1F91D","j":["agreement","shake"],"k":[38,10],"o":9},"nail_care":{"skin_variations":{"1F3FB":{"unified":"1F485-1F3FB","non_qualified":null,"image":"1f485-1f3fb.png","sheet_x":23,"sheet_y":45,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F485-1F3FC","non_qualified":null,"image":"1f485-1f3fc.png","sheet_x":23,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F485-1F3FD","non_qualified":null,"image":"1f485-1f3fd.png","sheet_x":23,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F485-1F3FE","non_qualified":null,"image":"1f485-1f3fe.png","sheet_x":23,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F485-1F3FF","non_qualified":null,"image":"1f485-1f3ff.png","sheet_x":23,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Nail Polish","b":"1F485","j":["beauty","manicure","finger","fashion","nail"],"k":[23,44]},"ear":{"skin_variations":{"1F3FB":{"unified":"1F442-1F3FB","non_qualified":null,"image":"1f442-1f3fb.png","sheet_x":13,"sheet_y":46,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F442-1F3FC","non_qualified":null,"image":"1f442-1f3fc.png","sheet_x":13,"sheet_y":47,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F442-1F3FD","non_qualified":null,"image":"1f442-1f3fd.png","sheet_x":13,"sheet_y":48,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F442-1F3FE","non_qualified":null,"image":"1f442-1f3fe.png","sheet_x":13,"sheet_y":49,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F442-1F3FF","non_qualified":null,"image":"1f442-1f3ff.png","sheet_x":13,"sheet_y":50,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Ear","b":"1F442","j":["face","hear","sound","listen"],"k":[13,45]},"nose":{"skin_variations":{"1F3FB":{"unified":"1F443-1F3FB","non_qualified":null,"image":"1f443-1f3fb.png","sheet_x":14,"sheet_y":0,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FC":{"unified":"1F443-1F3FC","non_qualified":null,"image":"1f443-1f3fc.png","sheet_x":14,"sheet_y":1,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FD":{"unified":"1F443-1F3FD","non_qualified":null,"image":"1f443-1f3fd.png","sheet_x":14,"sheet_y":2,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FE":{"unified":"1F443-1F3FE","non_qualified":null,"image":"1f443-1f3fe.png","sheet_x":14,"sheet_y":3,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true},"1F3FF":{"unified":"1F443-1F3FF","non_qualified":null,"image":"1f443-1f3ff.png","sheet_x":14,"sheet_y":4,"added_in":"8.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_emojione":true,"has_img_facebook":true,"has_img_messenger":true}},"a":"Nose","b":"1F443","j":["smell","sniff"],"k":[13,51]},"footprints":{"a":"Footprints","b":"1F463","j":["feet","tracking","walking","beach"],"k":[15,39]},"eyes":{"a":"Eyes","b":"1F440","j":["look","watch","stalk","peek","see"],"k":[13,42]},"eye":{"a":"Eye","b":"1F441-FE0F","c":"1F441","j":["face","look","see","watch","stare"],"k":[13,44],"o":7},"eye-in-speech-bubble":{"a":"Eye in Speech Bubble","b":"1F441-FE0F-200D-1F5E8-FE0F","k":[13,43],"o":7},"brain":{"a":"Brain","b":"1F9E0","k":[46,22],"o":10},"tongue":{"a":"Tongue","b":"1F445","j":["mouth","playful"],"k":[14,6]},"lips":{"a":"Mouth","b":"1F444","j":["mouth","kiss"],"k":[14,5]},"kiss":{"a":"Kiss Mark","b":"1F48B","j":["face","lips","love","like","affection","valentines"],"k":[24,37]},"cupid":{"a":"Heart with Arrow","b":"1F498","j":["love","like","heart","affection","valentines"],"k":[24,50]},"heart":{"a":"Heavy Black Heart","b":"2764-FE0F","c":"2764","j":["love","like","valentines"],"k":[50,8],"l":["<3"],"m":"<3","o":1},"heartbeat":{"a":"Beating Heart","b":"1F493","j":["love","like","affection","valentines","pink","heart"],"k":[24,45]},"broken_heart":{"a":"Broken Heart","b":"1F494","j":["sad","sorry","break","heart","heartbreak"],"k":[24,46],"l":[" - أرسلَ %1$s صُّورة. - دعوة مِن %s - %1$s دَعى %2$s - دعاكَ %1$s - إنضّم %1$s إلى الغُرفة - غادر %1$s الغرفة - رفضَ %1$s الدعوة - %1$s طردَ %2$s - إنَّ %1$s قد رفعَ الحظر عن %2$s - إنَّ %1$s قد حظرَ %2$s - إنَّ %1$s قد غيَّرَ صورته الشخصية - إنَّ %1$s قد عيَّنَ اسمه الظاهر إلى %2$s + أرسلَ ⁨%1$s⁩ صورةً. + دعوة من ⁨%s⁩ + دعا ⁨%1$s⁩ ⁨%2$s⁩ + دعاكَ ⁨%1$s⁩ + انضمّ ⁨%1$s⁩ إلى الغرفة + غادرَ ⁨%1$s⁩ الغرفة + رفضَ ⁨%1$s⁩ الدعوة + طردَ ⁨%1$s⁩ ⁨%2$s⁩ + رفعَ ⁨%1$s⁩ المنع عن ⁨%2$s⁩ + منعَ ⁨%1$s⁩ ⁨%2$s⁩ + غيّر ⁨%1$s⁩ صورته الشخصية إنَّ %1$s قد غيَّرَ اسمه الظاهر من %2$s إلى %3$s إنَّ %1$s قد أزالَ اسمه الظاهر (لقد كان %2$s) إنَّ %1$s قد غيَّرَ الموضوع إلى: %2$s @@ -42,12 +41,12 @@ عُنوان البريد الإلكتروني رقم الهاتف ‏‏⁨%1$s⁩: ‏⁨%2$s⁩ - إنَّ %1$s قد سحبَ دعوة %2$s + سحبَ ⁨%1$s⁩ الدعوة الموجّهة إلى ⁨%2$s إنَّ %s قد أجرى مُكالمة مرئية. إنَّ %s قد أجرى مُكالمة صوتية. إنَّ %1$s قد قَبَل دعوة %2$s يتعذَّر التنقيح - أرسلَ %1$s مُلصقًا. + أرسلَ ⁨%1$s⁩ ملصقًا. (تمَّ تغيِّير الصُّورة أيضًا) دَعوة مِن ⁨%s⁩ غُرفة فارِغة @@ -61,20 +60,20 @@ %1$s و%2$d آخرون %1$s و%2$d آخرون - أرسلتَ صُّورة. - أرسلتَ مُلصقًا. - دعوة مِنكَ - أنشأ %1$s الغُرفة - أنتَ أنشأتَ الغُرفة - أنتَ دعوتَ %1$s - أنتَ انضممت إلى الغُرفة - أنتَ غادرتَ الغُرفة + أرسلتَ صورةً. + أرسلتَ ملصقًا. + دعوة منك + أنشأ ⁨%1$s⁩ الغرفة + أنشأتَ الغرفة + دعوتَ ⁨%1$s⁩ + انضممتَ إلى الغرفة + غادرتَ الغرفة رفضتَ الدعوة - طردتَ %1$s - أنتَ قد رفعتَ الحظر عن %1$s - أنتَ قد حظرتَ %1$s - أنتَ قد سحبتَ دعوة %1$s - أنتَ قد غيّرتَ صورتك الشخصية + طردتَ ⁨%1$s⁩ + رفعتَ المنع عن ⁨%1$s⁩ + منعتَ ⁨%1$s⁩ + سحبتَ الدعوة الموجّهة إلى ⁨%1$s⁩ + غيّرتَ صورتك الشخصية أنتَ قد عيَّنتَ اسمك الظاهر إلى %1$s أنتَ قد غيّرتَ اسمك الظاهر من ⁨%1$s⁩ إلى ⁨%2$s⁩ أنتَ قد أزلتَ اسمك الظاهر (لقد كان ⁨%1$s⁩) @@ -140,12 +139,12 @@ إنَّ %s قد قامَ بالترقية هُنا. أنتَ قد جعلتَ الرسائل المُستقبلية مرئية لـ %1$s إنَّ %1$s قد جعلَ الرسائل المُستقبلية مرئية لـ %2$s - غادرت الغرفة - غادر ⁨%1$s⁩ الغرفة - أنت انضممت - انضم %1$s - أنتَ أنشأتَ المُناقشة - أنشأ %1$s المُناقشة + غادرتَ الغرفة + غادرَ ⁨%1$s⁩ الغرفة + انضممت + انضمّ ⁨%1$s⁩ + أنشأتَ النقاش + أنشأ ⁨%1$s⁩ النقاش أنتَ قد سحبتَ دعوة %1$s. السبب: %2$s إنَّ %1$s قد سحبَ دعوة %2$s. السبب: %3$s أنتَ قد ألغيتَ دعوة %1$s للإنضمام إلى الغُرفة. السبب: %2$s diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index 8e99a0a924..d5a9ed5b3e 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -880,7 +880,7 @@ \n \nSessions desconegudes: - Escolliu un directori de sale + Tria un directori de sales És possible que el servidor no estigui disponible o que estigui sobrecarregat Introdueix un servidor base per veure les seves sales públiques URL del servidor base @@ -1513,7 +1513,7 @@ Crea sala nova No hi ha xarxa. Si us plau comproveu la vostra connexió a internet. Canviar - Canviar de xarxa + Canvia de xarxa Espereu, si us plau… Totes les comunitats Aquesta sala no es pot pre-visualitzar @@ -1702,7 +1702,7 @@ Activa lliscar per respondre a la cronologia Cerca a partir del nom o l\'ID Nom o ID (#exemple:matrix.org) - Veu el directori de la sala + Mostra el directori de la sales Crea una nova sala No trobes el què busques\? No s\'han trobat edicions @@ -2145,7 +2145,7 @@ %d usuari vetat %d usuaris vetats - No s\'ha pogut obtenir la visibilitat actual del directori de sala (%1$s). + No s\'ha pogut obtenir la visibilitat actual del directori de sales (%1$s). Publicar aquesta sala al directori públic de sales %1$s\? Publica l\'adreça Anul·la la publicació de l\'adreça @@ -2729,4 +2729,15 @@ Elimina la icona Canvia la icona El servidor local accepta fitxers adjunts (fotos, fitxers, etc) de fins a una mida de %s. + Directori de sales + Mostra totes les sales al directori de sales, incloses aquelles amb contingut explícit. + Mostra sales amb contingut explícit + Estàs segur que vols eliminar tots els missatges no enviats d\'aquesta sala\? + Elimina missatges no enviats + Missatges amb enviament fallit + Vols aturar l\'enviament del missatge\? + Elimina tots els missatges fallits + Ha fallat + Enviat + Enviant \ No newline at end of file diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index 03b733be05..6d6574a87e 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -1,24 +1,24 @@ %1$s: %2$s - Uživatel %1$s poslal obrázek. - Uživatel %1$s poslal nálepku. + %1$s poslal(a) obrázek. + %1$s poslal(a) nálepku. Pozvání od uživatele %s - Uživatel %1$s pozval uživatele %2$s - Uživatel %1$s vás pozval - %1$s vstoupil do místnosti - Uživatel %1$s odešel + %1$s pozval(a) %2$s + %1$s vás pozval(a) + %1$s vstoupil(a) do místnosti + Uživatel %1$s odešel z místnosti %1$s odmítli pozvání %1$s vykopli %2$s %1$s zrušil vykázání %2$s %1$s vykázali %2$s %1$s zrušili pozvání pro %2$s - %1$s změnili svůj profilový obrázek + %1$s změnil(a) svůj profilový obrázek %1$s nastavili své veřejné jméno na %2$s - %1$s změnili své veřejné jméno z %2$s na %3$s + %1$s změnil(a) své veřejné jméno z %2$s na %3$s %1$s odstranili své veřejné jméno (%2$s) %1$s změnili téma na: %2$s - %1$s změnili název místnosti na: %2$s + %1$s změnil(a) název místnosti na: %2$s %s uskutečnili videohovor. %s uskutečnili hlasový hovor. %s přijali hovor. @@ -103,7 +103,7 @@ Změnili jste své veřejné jméno z %1$s na %2$s Odstranili jste své veřejné jméno (%1$s) Změnili jste téma na: %1$s - %1$s změnili obrázek místnosti + %1$s změnil(a) obrázek místnosti Změnili jste obrázek místnosti Změnili jste jméno místnosti na: %1$s Zahájili jste video hovor. @@ -136,7 +136,7 @@ Vlastní (%1$d) Vlastní Změnili jste %1$s stupeň oprávnění. - %1$s změnili %2$s stupeň oprávnění. + %1$s změnil(a) %2$s stupeň oprávnění. %1$s z %2$s na %3$s Pozvání od %1$s. Důvod: %2$s Vaše pozvání. Důvod: %1$s @@ -211,7 +211,7 @@ Učinili jste budoucí zprávy viditelné pro %1$s %1$s učinili budoucí zprávy viditelné pro %2$s Odešli jste z místnosti - %1$s odešli z místnosti + Uživatel %1$s odešel z místnosti Vstoupili jste %1$s vstoupili Založili jste diskusi @@ -233,7 +233,7 @@ • Servery shodující se s %s byly odstraněny ze seznamu zakázaných. • Servery shodující se s %s jsou nyní zakázány. Změnili jste ACL serveru pro tuto místnost. - %s změnili ACL serveru pro tuto místnost. + %s změnil(a) ACL serveru pro tuto místnost. • Server shodující se doslovně s IP je povolen. • Server shodující se doslovně s IP je zakázán. • Server shodující se s %s je povolen. @@ -241,11 +241,11 @@ Nastavili jste ACL serveru pro tuto místnost. %s nastavili ACL serveru pro tuto místnost. Změnili jste adresy pro tuto místnost. - %1$s změnili adresy pro tuto místnost. + %1$s změnil(a) adresy pro tuto místnost. Změnili jste hlavní a alternativní adresu pro tuto místnost. - %1$s změnili hlavní a alternativní adresu pro tuto místnost. + %1$s změnil(a) hlavní a alternativní adresu pro tuto místnost. Změnili jste alternativní adresu pro tuto místnost. - %1$s změnili alternativní adresu pro tuto místnost. + %1$s změnil(a) alternativní adresu pro tuto místnost. Odstranili jste alternativní adresu %1$s pro tuto místnost. Odstranili jste alternativní adresy %1$s pro tuto místnost. @@ -1984,7 +1984,7 @@ Důvěryhodné Nedůvěryhodné Tato relace je důvěryhodná pro bezpečnou komunikaci, protože %1$s (%2$s) ji ověřil: - %1$s (%2$s) se přihlásil skrze novou relaci: + %1$s (%2$s) se přihlásil novou relací: Dokud tento uživatel nezačne důvěřovat této relaci, zprávy z ní odeslané a v ní přijaté budou označeny varováním. Volitelně ji můžete manuálně ověřit. Spustit křížové podepsání Resetovat klíče @@ -2417,7 +2417,7 @@ Zahrnuje události pozvat/vstoupit/opustit/vykopnout/vykázat a změny avatara/veřejného jména. Průzkum Tlačítka botů - Reagovali skrze: %s + Reagoval(a): %s Výsledek ověření Odkaz byl chybně zformován K zahájení hovoru v této místnosti nemáte oprávnění @@ -2712,4 +2712,15 @@ Přepnout Úvodní synchronizace: \nStahuji data… + Opravdu chcete smazat všechny neodeslané zprávy v této místnosti\? + Smazat neodeslané zprávy + Zprávy se nepodařilo odeslat + Chcete zrušit odesílání zprávy\? + Smazat všechny zprávy, které se nepodařilo odeslat + Selhalo + Odesláno + Odesílá se + Zobrazit všechny místnosti v adresáři místností, včetně místností s explicitním obsahem. + Zobrazit místnosti s explicitním obsahem + Adresář místností \ No newline at end of file diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 437c517574..52e67eb460 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -161,7 +161,7 @@ %1$s hat das %2$s Widget modifiziert Du hast das %1$s Widget modifiziert Administrator - Moderator:in + Moderator Standard Benutzerdefiniert (%1$d) Benutzerdefiniert @@ -348,12 +348,12 @@ Keine Ergebnisse Räume - Raum-Verzeichnis + Raumverzeichnis Keine Räume Keine öffentl. Räume verfügbar - %d Benutzer/in - %d Benutzer/innen + %d Benutzer + %d Benutzer Logdateien übermitteln Absturzberichte übermitteln @@ -494,8 +494,8 @@ ${app_name} benötigt die Berechtigung, auf Kamera und Mikrofon zu zugreifen, um Video-Anrufe durchzuführen. \n \nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen. - ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer:innen anhand ihrer Email-Adresse und Telefonnummer zu finden. Wenn du der Nutzung deines Adressbuchs zu diesem Zweck zustimmst, erlaube den Zugriff im nächsten Popup-Fenster. - ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer:innen anhand ihrer E-Mail-Adresse und Telefonnummer zu finden. + ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer anhand ihrer Email-Adresse und Telefonnummer zu finden. Wenn du der Nutzung deines Adressbuchs zu diesem Zweck zustimmst, erlaube den Zugriff im nächsten Popup-Fenster. + ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer anhand ihrer E-Mail-Adresse und Telefonnummer zu finden. \n \nStimmst du der Nutzung deines Adressbuchs zu diesem Zweck zu\? Entschuldige. Die Aktion wurde aufgrund fehlender Berechtigungen nicht ausgeführt @@ -540,22 +540,22 @@ Aus diesem Raum entfernen Verbannen Verbannung aufheben - Zum/r normalen Benutzer/in herabstufen - Zum/r Moderator:in machen + Zum normalen Benutzer herabstufen + Zum Moderator machen Zum Admin machen - Alle Nachrichten dieses/r Nutzers/in verbergen - Alle Nachrichten dieses/r Nutzers/in anzeigen + Alle Nachrichten dieses Nutzers verbergen + Alle Nachrichten dieses Nutzers anzeigen Nutzer-ID, Name oder E-Mail-Adresse Erwähnen Sitzungsliste anzeigen - Du wirst diese Änderung nicht rückgängig machen können, da der/die Benutzer!n dasselbe Berechtigungslevel wie du erhalten wirst. + Du wirst diese Änderung nicht rückgängig machen können, da der Benutzer dieselbe Berechtigungsstufe wie du erhalten wirst. \nBist du sicher\? "Bist du sicher, dass du %s in diesen Chat einladen willst?" Mit ID einladen LOKALE KONTAKTE (%d) Nur Matrix-Benutzer - Benutzer:in per ID einladen + Benutzer per ID einladen Bitte gib eine oder mehrere E-Mail-Adressen oder eine Matrix-ID ein E-Mail or Matrix-ID @@ -582,10 +582,10 @@ Fingerabdruck (%s): Konnte Identität des Remote-Servers nicht verifizieren. Dies kann bedeuten, dass jemand böswillig deinen Internetverkehr abfängt oder dass dein Telefon dem Zertifikat, dass der Remote-Server anbietet, nicht vertraut. - Wenn der/die Server-Administrator:in dir mitgeteilt hat, dass dies zu erwarten sei, stelle sicher, dass der Fingerabdruck unten mit dem von deinem/r Administrator:in bereitgestellten übereinstimmt. + Wenn der Server-Administrator dir mitgeteilt hat, dass dies zu erwarten sei, stelle sicher, dass der Fingerabdruck unten mit dem von deinem Administrator bereitgestellten übereinstimmt. Das Zertifikat unterscheidet sich von dem Zertifikat, dem dein Gerät ursprünglich vertraut hat. Dies ist SEHR UNGEWÖHNLICH. Es wird empfohlen, dass du dieses neue Zertifikat NICHT AKZEPTIERST. Das Zertifikat hat sich von einem ursprünglich vertrauenswürdigem Zertifikat in ein nicht vertrauenswürdiges Zertifikat geändert. Eventuell wurde das Zertifikat des Servers erneuert. Bitte erkundige dich beim Server-Administrator, welcher Fingerprint als vertrauenswürdig gilt. - Akzeptiere das Zertifikat nur dann, wenn der/die Server-Administrator:in einen Fingerprint veröffentlicht hat, der mit dem obigen übereinstimmt. + Akzeptiere das Zertifikat nur dann, wenn der Server-Administrator einen Fingerprint veröffentlicht hat, der mit dem obigen übereinstimmt. Raum-Details Personen @@ -596,7 +596,7 @@ TEILNEHMER Grund für das Melden dieses Inhalts - Möchtest du alle Nachrichten dieses/r Nutzers/in verbergen\? + Möchtest du alle Nachrichten dieses Nutzers verbergen\? \n \nBeachte: Diese Aktion wird die App neu starten und einige Zeit brauchen. Upload abbrechen @@ -822,7 +822,7 @@ Sperren Zulassen Sitzung verifizieren - Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des/der anderen Nutzer!n und bestätige: + Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des anderen Nutzers und bestätige: Falls sie nicht übereinstimmen, wurde die Kommunikation vielleicht kompromittiert. Ich bestätige, dass die Schlüssel übereinstimmen @@ -886,7 +886,7 @@ Schwarzes Design Synchronisiere… Auf Ereignisse lauschen - Nachrichten, die meinen Anzeigenamen enthalten + Nachrichten mit meinem Anzeigenamen Nachrichten, die meinen Benutzernamen enthalten Du hast die neue Sitzung \'%s\' hinzugefügt, die jetzt Verschlüsselungs-Schlüssel anfordert. Deine bislang nicht verifiziertes Sitzung \'%s\' fordert Verschlüsselungs-Schlüssel an. @@ -923,13 +923,13 @@ Sicher, dass du einen Sprachanruf starten möchtest\? Sicher, dass du einen Videoanruf starten möchtest\? Gruppenliste - Ein Bann führt zu einem Ausschluss eines/r Nutzers/in von diesem Raum und verhindert einen erneuten Beitritt. + Ein Bann führt zu einem Ausschluss eines Nutzers von diesem Raum und verhindert einen erneuten Beitritt. Alle Nachrichten (laut) Alle Nachrichten Nur Erwähnungen Stumm URL-Vorschau im Chat - Vibriere beim Erwähnen eines/r Nutzers/in + Vibriere beim Erwähnen eines Nutzers Benachrichtigungen Dieser Raum zeigt für keine Community Avatare an Neue Community-ID (z.B. +foo:matrix.org) @@ -951,14 +951,14 @@ Eingeladen Filter Gruppen-Mitglieder Filter Gruppen-Räume - Die Community-Administration hat keine lange Beschreibung für diese Community zur Verfügung gestellt. + Der Community-Administrator hat keine lange Beschreibung für diese Community zur Verfügung gestellt. Du wurdest von %2$s aus %1$s gekickt Du wurdest von %2$s aus %1$s verbannt Grund: %1$s Erneut beitreten Raum vergessen Zum Startbildschirm hinzufügen - Community Avatare + Community-Avatare Schütteln, um einen Fehler zu melden Aktionen Mitglieder auflisten @@ -1042,8 +1042,8 @@ \n \nDie Deaktivierung deines Konto wird standardmäßig keine deiner gesendeten Nachrichten löschen. Wenn du möchtest, dass auch deine Nachrichten gelöscht werden, wähle zusätzlich die Option unten. \n -\nDie Sichtbarkeit deiner Nachrichten ist ähnlich wie bei E-Mails: Wenn deine Nachrichten gelöscht werden, bedeutet dies, dass von dir verschickte Nachrichten nicht mit neuen oder unregistrierten Nutzer:innen geteilt werden. Aber registrierte Nutzer:innen, die bereits Zugang zu diesen Nachrichten haben, behalten weiterhin Zugriff auf ihre Kopie. - Bitte alle Nachrichten, die ich gesendet habe, löschen, wenn mein Account deaktiviert wird (Warnung: Unterhaltungen werden für zukünftige Nutzer:innen unvollständig erscheinen) +\nDie Sichtbarkeit deiner Nachrichten ist ähnlich wie bei E-Mails: Wenn deine Nachrichten gelöscht werden, bedeutet dies, dass von dir verschickte Nachrichten nicht mit neuen oder unregistrierten Nutzer geteilt werden. Aber registrierte Nutzer, die bereits Zugang zu diesen Nachrichten haben, behalten weiterhin Zugriff auf ihre Kopie. + Bitte alle Nachrichten, die ich gesendet habe, löschen, wenn mein Konto deaktiviert wird (Warnung: Unterhaltungen werden für zukünftige Nutzer unvollständig erscheinen) Um fortzufahren, bitte Passwort eingeben: Account deaktivieren Drittanbieter-Lizenzen @@ -1067,15 +1067,15 @@ Du bist aktuell kein Mitglied einer Community. Benutze die Enter-Taste der Tastatur zum Senden Zeigt Aktionen - Bannt Benutzer:in mit angegebener ID + Bannt Benutzer mit angegebener ID Hebt die Verbannung des Benutzers mit angegebener ID auf Bestimmt das Berechtigungslevel des Benutzers Setzt Berechtigungen des Benutzers zurück - Lädt Benutzer:in mit angegebener Kennung in den aktuellen Raum ein + Lädt Benutzer mit angegebener Kennung in den aktuellen Raum ein Tritt dem Raum mit angegebenen Alias bei Verlasse Raum Setzt das Raum-Thema - Kickt Benutzer:in mit angegebener ID + Kickt Benutzer mit angegebener ID Ändert deinen Anzeigenamen (De-)Aktiviert Markdown Um das Matrix-App-Management zu reparieren @@ -1121,10 +1121,10 @@ Ressourcen-Limit Kontaktiere Administrator kontaktiere deinen Service-Administrator - Dieser Home-Server hat eine seiner Ressourcen-Grenzen erreicht, sodass einige Nutzer:innen sich nicht anmelden können. + Dieser Home-Server hat eine seiner Ressourcen-Grenzen erreicht, sodass einige Nutzer sich nicht anmelden können. Dieser Home-Server hat einen seiner Ressourcen-Limits überschritten. - Dieser Home-Server hat seine Obergrenze an monatlich aktiven Nutzer:innen erreicht, sodass einige Nutzer:innen sich nicht anmelden können. - Dieser Home-Server hat seine Obergrenze an monatlich aktiven Nutzer:innen erreicht. + Dieser Home-Server hat seine Obergrenze an monatlich aktiven Nutzer erreicht, sodass einige Nutzer sich nicht anmelden können. + Dieser Home-Server hat seine Obergrenze an monatlich aktiven Nutzer erreicht. Bitte %s um dieses Limit anheben zu lassen. Bitte %s um diesen Dienst weiter zu nutzen. Fehler @@ -1151,7 +1151,7 @@ Grund Linkvorschau im Chat aktivieren, falls dein Home-Server diese Funktion unterstützt. Sende Schreibbenachrichtigungen - Lasse andere Benutzer:innen wissen, dass du tippst. + Lasse andere Benutzer wissen, dass du tippst. Markdown-Formatierung Formatiere Nachrichten mittels Markdown-Syntax, bevor sie gesendet werden. Dies erlaubt erweiterte Formatierungen, etwa Sternchen (*) um kursiven Text anzuzeigen. Zeige Lesebestätigungen @@ -1229,7 +1229,7 @@ Prüfung der Play-Dienste Google Play-Dienste-APK ist verfügbar und aktuell. Token-Registrierung - Wenn ein:e Benutzer:in ein abgestecktes Gerät mit ausgeschaltetem Bildschirm eine Weile nicht bewegt, wechselt es in den Bereitschaftsmodus. Dies hindert Apps daran, auf das Netzwerk zuzugreifen und verzögert die Ausführung von Aufgaben, Synchronisierungen und Standard-Alarmen. + Wenn ein Benutzer ein abgestecktes Gerät mit ausgeschaltetem Bildschirm eine Weile nicht bewegt, wechselt es in den Bereitschaftsmodus. Dies hindert Apps daran, auf das Netzwerk zuzugreifen und verzögert die Ausführung von Aufgaben, Synchronisierungen und Standard-Alarmen. Ignoriere Optimierungen Hintergrundverbindung ${app_name} muss eine Hintergrundverbindung (nur geringe Belastung) aufrechterhalten, um verlässliche Benachrichtigungen zu erhalten. @@ -1283,14 +1283,14 @@ Lösche Sicherung Präferenz der Benachrichtigungen nach Ereignis [%1$s] -\nDieser Fehler ist außerhalb von ${app_name} passiert. Google sagt, dass dieses Gerät zu viele Apps registriert hat um FCM zu nutzen. Der Fehler taucht nur auf, wenn sehr viele Apps installiert sind. Er sollte also den/die Durchschnittsnutzer:in nicht betreffen. +\nDieser Fehler ist außerhalb von ${app_name} passiert. Google sagt, dass dieses Gerät zu viele Apps registriert hat um FCM zu nutzen. Der Fehler taucht nur auf, wenn sehr viele Apps installiert sind. Er sollte also den Durchschnittsnutzer nicht betreffen. [%1$s] \nDieser Fehler liegt nicht unter der Kontrolle von ${app_name}. Er kann aus verschiedenen Gründen auftreten. Vielleicht wird es funktionieren, wenn du es später noch einmal probierst. Außerdem kannst Du prüfen, ob die Datennutzung der Google Play-Dienste unbeschränkt ist und die Geräteuhr richtig eingestellt ist. Der Fehler kann aber auch unter Custom-ROMs auftreten. [%1$s] \nDieser Fehler ist außerhalb von ${app_name} passiert. Es gibt kein Google-Konto auf dem Gerät. Bitte füge ein Google-Konto hinzu. Verwaltung der Krypto-Schlüssel Schlüssel-Sicherung verwalten - Nachrichten in verschlüsselten Räumen sind mit Ende-zu-Ende-Verschlüsselung gesichert. Nur du und der/die Empfänger!nnen haben die Schlüssel um diese Nachrichten zu lesen. + Nachrichten in verschlüsselten Räumen sind mit Ende-zu-Ende-Verschlüsselung gesichert. Nur du und der Empfänger haben die Schlüssel um diese Nachrichten zu lesen. \n \nSichere deine Schlüssel, um sie nicht zu verlieren. Der Wiederherstellungsschlüssel wurde nach \'%s\' gespeichert. @@ -1367,7 +1367,7 @@ Neue Schlüsselsicherung Eine neue Schlüsselsicherung wurde entdeckt. \n -\nWenn du die neue Wiederherstellungsmethode nicht festgelegt hast, kann ein/e Angreifer!n versuchen auf dein Konto zuzugreifen. Ändere dein Passwort und richte sofort eine neue Wiederherstellungsmethode in den Einstellungen ein. +\nWenn du die neue Wiederherstellungsmethode nicht festgelegt hast, kann ein Angreifer versuchen auf dein Konto zuzugreifen. Ändere dein Passwort und richte sofort eine neue Wiederherstellungsmethode in den Einstellungen ein. Ich war es Verliere nie mehr verschlüsselte Nachrichten Richte die Schlüsselsicherung ein @@ -1448,10 +1448,10 @@ Eingehende Verifizierungsanfrage Du hast eine eingehende Verifizierungsanfrage erhalten. Anfrage ansehen - Warte auf Bestätigung des/r anderen Nutzer*in… + Warte auf Bestätigung des Partners… Verifiziert! Du hast diese Sitzung erfolgreich verifiziert. - Sichere Nachrichten mit diesem/r Benutzer:in sind Ende-zu-Ende verschlüsselt und können nicht von Dritten mitgelesen werden. + Sichere Nachrichten mit diesem Benutzer sind Ende-zu-Ende verschlüsselt und können nicht von Dritten mitgelesen werden. Verstanden Schlüssel-Verifizierung Anfrage abgebrochen @@ -1460,7 +1460,7 @@ Interaktive Sitzungs-Verifizierung Verifizierungsanfrage %s möchte deine Sitzung verifizieren - Der/die Benutzer:in hat die Verifizierung abgebrochen + Der Benutzer hat die Verifizierung abgebrochen Der Verifizierungsprozess ist abgelaufen Die Sitzung hat eine unerwartete Nachricht erhalten Eine ungültige Nachricht wurde empfangen @@ -1483,7 +1483,7 @@ Reaktion hinzufügen Reaktionen ansehen Reaktionen - Ereignis von Benutzer:in gelöscht + Ereignis von Benutzer gelöscht Ereignis moderiert durch Raum-Administrator Zuletzt bearbeitet von %1$s am %2$s Neuen Raum erstellen @@ -1508,7 +1508,7 @@ Schlüsselaustausch anfragen Es sieht so aus, als hättest du bereits ein Setup-Schlüssel-Backup von einer anderen Sitzung. Möchtest du es durch das, was du gerade erstellt hast, ersetzen\? Für maximale Sicherheit empfehlen wir, dies persönlich zu tun, oder ein anderes vertrautes Kommunikationsmedium zu nutzen. - Überprüfe diese Sitzung, um sie als vertrauenswürdig zu markieren. Sitzungen von anderen Nutzer:innen zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende verschlüsselten Nachrichten. + Überprüfe diese Sitzung, um sie als vertrauenswürdig zu markieren. Sitzungen von anderen Nutzern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende verschlüsselten Nachrichten. Durch Verifizieren dieser Sitzung wird sie bei dir und deinem Gegenüber als vertrauenswürdig markiert. Verifiziere diese Sitzung, indem du bestätigst, dass das folgende Emoji auf dem Bildschirm deines Gegenübers angezeigt wird Verifiziere diese Sitzung, indem du bestätigst, dass die folgenden Zahlen auf dem Bildschirm deines Gegenübers angezeigt werden @@ -1618,8 +1618,8 @@ ausstehend Gib einen neuen Identitätsserver ein Konnte keine Verbindung zum Home-Server herstellen - Bitte frage den/die Administrator/in deines Home-Servers (%1$s) nach der Einrichtung eines TURN-Servers, damit Anrufe zuverlässig funktionieren. -\n + Bitte frage den Administrator deines Home-Servers (%1$s) nach der Einrichtung eines TURN-Servers, damit Anrufe zuverlässig funktionieren. +\n \nAlternativ kann ein öffentlicher Server auf %2$s genutzt werden. Dies wird jedoch weniger zuverlässig sein und deine IP-Adresse wird gegenüber diesem Server preisgegeben. Du kannst den Server auch in den Einstellungen anpassen. Dies ist keine Adresse eines Matrixservers Kann Home-Server nicht bei dieser URL erreichen. Bitte überprüfen @@ -1668,7 +1668,7 @@ Auffindbare Telefonnummern Bitte gib die Adresse des Identitätsservers ein Identitätsserver hat keine Nutzungsbedingungen - Der Identitätsserver den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du dem/r Besitzer!n des Dienstes vertraust + Der Identitätsserver den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du dem Besitzer des Dienstes vertraust Eine Textnachricht wurde an %s gesendet. Bitte gib den Verifizierungscode ein, den sie enthält. Aktiviere ausführliche Logs. Ausführliche Logs werden der Entwicklung der App dadurch helfen, dass mehr Informationen übertragen werden, wenn du einen Fehlerbericht sendest. Auch wenn dies aktiviert ist, werden keine Nachrichteninhalte oder andere privaten Daten aufgezeichnet. @@ -1692,8 +1692,8 @@ gelesen von %1$s und %2$s gelesen von %s - gelesen von einem Nutzer:in - gelesen von %d Nutzer:innen + gelesen von einem Nutzer + gelesen von %d Nutzern Die Datei \'%1$s\' (%2$s) ist zu groß, um sie hochzuladen. Das Limit ist %3$s. Beim Abrufen des Anhangs ist ein Fehler aufgetreten. @@ -1709,24 +1709,24 @@ Diesen Inhalt melden Meldegrund MELDEN - NUTZER:IN IGNORIEREN + NUTZER IGNORIEREN Inhalt gemeldet - Dieser Inhalt wurde gemeldet. -\n -\nWenn du keine weiteren Inhalte dieses/r Nutzers/in sehen möchtest, kannst sie/ihn ignorieren, um jene Nachrichten auszublenden. + Dieser Inhalt wurde gemeldet. +\n +\nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden. Als Spam gemeldet Dieser Inhalt wurde als Spam gemeldet. -\n -\nWenn du keine weiteren Inhalte dieses/r Nutzers/in sehen möchtest, kannst sie/ihn ignorieren, um jene Nachrichten auszublenden. +\n +\nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden. Als unangebracht gemeldet Dieser Inhalt wurde als unangebracht gemeldet. -\n -\nWenn du keine weiteren Inhalte dieses/r Nutzers/in sehen möchtest, kannst sie/ihn ignorieren, um jene Nachrichten auszublenden. +\n +\nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden. ${app_name} benötigt Berechtigungen, um deine E2E Schlüssel zu speichern. \n \nBitte erlaube den Zugriff im nächsten Pop-Up sodass du deine Schlüssel manuell exportieren kannst. Aktuell besteht keine Netzwerkverbindung - Nutzer:in ignorieren + Nutzer ignorieren Alle Nachrichten (laut) Alle Nachrichten Nur Erwähnungen @@ -1735,7 +1735,7 @@ Raum verlassen %1$s hat keine Änderungen gemacht Sendet die Nachricht als Spoiler - Du ignorierst keine Nutzer:innen + Du ignorierst keine Nutzer Halte auf einem Raum um mehr Optionen anzuzeigen %1$s hat den Raum für jeden, der den Link hat, öffentlich gemacht. Ungelesene Nachrichten @@ -1750,7 +1750,7 @@ Andere Benutzerdefinierte & erweiterte Einstellungen Fortfahren - Eine Trennung von deinem Identitätsserver würde bedeuten, dass du weder von anderen Nutzer:innen gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. + Eine Trennung von deinem Identitätsserver würde bedeuten, dass du weder von anderen Nutzern gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. Du teilst deine Email Adressen oder Telefonnummern momentan auf dem Identitätsserver %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. Stimme den Nutzungsbedingungen des Identitätsservers (%s) zu, um zu erlauben per E-Mail oder Telefonnummer gefunden zu werden. Zu teilende Daten nicht verarbeitbar @@ -1837,7 +1837,7 @@ Warnung Bitte löse das Captcha Veralteter Home-Server - Auf diesem Home-Server läuft eine zu alte Version, um eine Verbindung herzustellen. Bitten deine Home-Server-Administration um ein Upgrade. + Auf diesem Home-Server läuft eine zu alte Version, um eine Verbindung herzustellen. Bitte deinen Home-Server-Administrator um ein Upgrade. Es wurden zu viele Anfragen gesendet. Versuche es erneut in %1$d Sekunde… Es wurden zu viele Anfragen gesendet. Versuche es erneut in %1$d Sekunden… @@ -1850,11 +1850,11 @@ \n \n• Du hast diese Sitzung aus einer anderen Sitzung heraus gelöscht. \n -\n• Die Administration deines Servers hat deinen Zugriff aus Sicherheitsgründen ungültig gemacht. +\n• Der Administrator deines Servers hat deinen Zugriff aus Sicherheitsgründen ungültig gemacht. Melde dich erneut an Du bist abgemeldet Anmelden - Deine Home-Server-Administration (%1$s) hat dich von deinem Konto %2$s (%3$s) abgemeldet. + Dein Home-Server-Administrator (%1$s) hat dich von deinem Konto %2$s (%3$s) abgemeldet. Melden dich an, um ausschließlich auf diesem Gerät gespeicherte Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten auf jedem Gerät zu lesen. Anmelden Passwort @@ -1868,7 +1868,7 @@ \nMelde dich erneut an, um auf deine Kontodaten und Nachrichten zuzugreifen. Du verlierst den Zugriff auf verschlüsselte Nachrichten, außer, du meldest dich an, um den Verschlüsselungsschlüssel wiederherzustellen. Daten löschen - Die aktuelle Sitzung gehört dem/der Benutzer!n%1$s. Die angegebenen Anmeldeinformationen sind von Benutzer!n %2$s. Dies wird nicht von ${app_name} unterstützt. + Die aktuelle Sitzung gehört dem Benutzer %1$s. Die angegebenen Anmeldeinformationen sind vom Benutzer %2$s. Dies wird nicht von ${app_name} unterstützt. \nBitte zuerst die Daten löschen und dann erneut anmelden. matrix.to-Link fehlerhaft Die Beschreibung ist zu kurz @@ -1876,7 +1876,7 @@ Alle meine Sitzungen anzeigen Erweiterte Einstellungen Entwicklungsmodus - Der Entwicklungsmodus aktiviert versteckte Funktionen und kann die Anwendung weniger stabil machen. Nur für Entwickler!nnen! + Der Entwicklungsmodus aktiviert versteckte Funktionen und kann die Anwendung weniger stabil machen. Nur für Entwickler! Wutschütteln Erkennungsschwelle Schüttel dein Telefon, um die Erkennungsschwelle zu testen @@ -1894,9 +1894,9 @@ Nicht vertrauenswürdige Anmeldung Sie stimmen überein Sie stimmen nicht überein - Verifiziere diese/n Benutzer!n, indem du bestätigst, dass diese einzigartigen Emoji in derselben Reihenfolge auf dem Bildschirm deines Gegenübers angezeigt werden. + Verifiziere diesen Benutzer, indem du bestätigst, dass diese einzigartigen Emoji in derselben Reihenfolge auf dem Bildschirm deines Gegenübers angezeigt werden. Für ultimative Sicherheit verwende ein anderes vertrauenswürdiges Kommunikationsmittel oder mache es persönlich. - Suche nach dem grünen Schild, um sicherzustellen, dass ein/e Benutzer!n vertrauenswürdig ist. Vertraue allen Benutzer!nnen in einem Raum, um sicherzustellen, dass der Raum sicher ist. + Suche nach dem grünen Schild, um sicherzustellen, dass ein Benutzer vertrauenswürdig ist. Vertraue alle Benutzer in einem Raum, um sicherzustellen, dass der Raum sicher ist. Nicht sicher Eine der folgenden Möglichkeiten kann beeinträchtigt sein: \n @@ -1919,7 +1919,7 @@ Manuelle Verifizierung Ich Scanne den Code mit dem Gerät des Gegenüber für eine gegenseitige Überprüfung - Scanne ihren/seinen Code + Scanne Code des Anderen Kann nicht scannen Wenn ihr nicht am selben Ort seid, vergleicht Emoji stattdessen Verifizieren via Emoji-Vergleich @@ -1952,7 +1952,7 @@ Moderierende benutzerdefiniert Eingeladen - Nutzer:innen + Nutzer Admin in %1$s Moderation in %1$s Springen & als gelesen markieren @@ -1978,7 +1978,7 @@ Vergleiche die einzigartigen Emoji und stell sicher, dass sie in derselben Reihenfolge angezeigt werden. Vergleiche den Code mit dem Code auf dem Bildschirm deines Gegenübers. Nachrichten mit diesem Gegenüber sind Ende-zu-Ende verschlüsselt und können nicht von Dritten gelesen werden. - Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer!nnen sehen sie als vertrauenswürdig an. + Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer sehen sie als vertrauenswürdig an. Cross-Signing Cross-Signing ist aktiviert \nPrivate Schlüssel auf dem Gerät. @@ -2000,7 +2000,7 @@ %d aktive Sitzungen Verifiziere diese Sitzung - Andere Benutzer!nnen vertrauen ihr möglicherweise nicht + Andere Benutzer vertrauen ihr möglicherweise nicht Vollständige Sicherheit Nutze eine vorhandene Sitzung um diese Sitzung zu verifizieren und ihr Zugriff auf verschlüsselte Nachrichten zu gewähren. Verifizieren @@ -2012,7 +2012,7 @@ Nicht vertraut Diese Sitzung ist für sichere Nachrichtenübertragung vertrauenswürdig, weil %1$s (%2$s) sie verifiziert hat: %1$s (%2$s) hat sich in einer neuen Sitzung angemeldet: - Bis diese/r Benutzer!n dieser Sitzung vertraut, werden an und von ihr/ihm gesendete Nachrichten mit Warnungen gekennzeichnet. Alternativ kannst du dies manuell überprüfen. + Bis dieser Benutzer dieser Sitzung vertraut, werden an und von ihm gesendete Nachrichten mit Warnungen gekennzeichnet. Alternativ kannst du dies manuell überprüfen. Initialisiere Cross-Signing Schlüssel zurücksetzen QR-Code @@ -2049,8 +2049,8 @@ Möchtest du dieses Ereignis wirklich entfernen (löschen)\? Beachte, dass beim Löschen eines Raumnamens oder einer Themenänderung die Änderung rückgängig gemacht werden kann. Grund hinzufügen Grund für das Editieren - Ereignis gelöscht von Benutzer!n, Grund: %1$s - Ereignis vom Raumadministration moderiert, Grund: %1$s + Ereignis durch den Benutzer gelöscht, Grund: %1$s + Ereignis vom Raumadministrator moderiert, Grund: %1$s Schlüssel sind bereits aktuell! Spoiler Benutzerdefiniert (%1$d) in %2$s @@ -2063,8 +2063,8 @@ Benutze diese Sitzung um deine neue zu verfizieren, damit sie auf verschlüsselte Nachrichten zugreifen kann. Das war ich nicht Dein Account ist möglicherweise komprimittiert - Wenn du abbrichst, wirst du auf diesem Gerät keine verschlüsselten Nachrichten lesen können, und andere Benutzer:innen werden ihm nicht vertrauen - Wenn du abbrichst, wirst du auf deinem neuen Gerät keine verschlüsselten Nachrichten lesen können, und andere Benutzer:innen werden ihm nicht vertrauen + Wenn du abbrichst, wirst du auf diesem Gerät keine verschlüsselten Nachrichten lesen können, und andere Benutzer werden ihm nicht vertrauen + Wenn du abbrichst, wirst du auf deinem neuen Gerät keine verschlüsselten Nachrichten lesen können, und andere Benutzer werden ihm nicht vertrauen Du wirst %1$s (%2$s) nicht verifizieren, wenn du jetzt abbrichst. Beginne in deren Nutzerprofil erneut. Eines der folgenden könnte kom­pro­mit­tie­rt sein: \n @@ -2115,7 +2115,7 @@ Dies kann nicht von einem mobilen Gerät erfolgen Wenn Räume verbessert werden Verschlüsselung aktiviert - Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahre mehr & verifiziere Benutzer:innen in deren Profil. + Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahre mehr & verifiziere Benutzer in deren Profil. Die Verschlüsselung in diesem Raum wird nicht unterstützt Warte auf %s… %s setzen @@ -2185,20 +2185,20 @@ Dieser Link %1$s bringt dich zu einer anderen Seite: %2$s. \n \nWillst du wirklich fortfahren\? - Konnte Direktnachricht nicht erzeugen. Prüfe die Nutzer:in, die du einladen willst und versuche es erneut. + Konnte Direktnachricht nicht erzeugen. Prüfe die Nutzer, die du einladen willst und versuche es erneut. %1$s: %2$s %1$s: %2$s %3$s - Nutzer!n hinzufügen + Mitglieder hinzufügen EINLADEN - Benutzer:innen werden eingeladen… - Benutzer:innen einladen + Benutzer werden eingeladen… + Benutzer einladen Einladung gesendet an %1$s Einladungen gesendet an %1$s und %2$s Einladungen gesendet an %1$s und einen weiteren Benutzer Einladungen gesendet an %1$s und %2$d weitere Benutzer - Wir konnten die Benutzer:innen nicht einladen. Bitte überprüfe die Benutzer:innen, welchen du einladen möchtest, und versuche es erneut. + Wir konnten den Benutzer nicht einladen. Bitte überprüfe den Benutzernamen, welchen du einladen möchtest und versuche es erneut. Pause Kopieren Benachrichtigungen @@ -2207,7 +2207,7 @@ Ablehnen Erfolg Echtzeitverbindung konnte nicht hergestellt werden. -\nBitte den/die Administrator/in deines Home-Servers, einen TURN-Server so zu konfigurieren, dass Anrufe zuverlässig funktionieren. +\nBitte den Administrator deines Home-Servers, einen TURN-Server so zu konfigurieren, dass Anrufe zuverlässig funktionieren. Wähle Audiogerät aus Telefonie Lautsprecher @@ -2224,25 +2224,25 @@ Zum Anruf zurückkehren Einladung zurückziehen Möchtest du dich zurückstufen\? - Du kannst die Zurückstufung nicht rückgängig machen und du wirst die Rechte nur mit einem/r anderen berechtigten Benutzer:in im Raum zurückerlangen können. + Du kannst die Zurückstufung nicht rückgängig machen und du wirst die Rechte nur mit einem anderen berechtigten Benutzer im Raum zurückerlangen können. Zurückstufen - Benutzer:in ignorieren - Durch das Ignorieren werden für dich alle Nachrichten des/r Nutzers/in ausgeblendet. + Benutzer ignorieren + Durch das Ignorieren werden für dich alle Nachrichten des Nutzers ausgeblendet. \n \nDu kannst die Aktion jederzeit in den allgemeinen Einstellungen rückgängig machen. Ignorieren des Benutzers rückgängig machen Das Aufheben der Ignorierung wird alle Nachrichten des Benutzers wieder einblenden. Einladung zurückziehen - Bist du dir sicher, dass du die Einladung für diese:n Benutzer:in zurückziehen möchtest\? - Benutzer:in entfernen + Bist du dir sicher, dass du die Einladung für diesen Benutzer zurückziehen möchtest\? + Benutzer entfernen Grund für das Entfernen - Das Entfernen wird den/die Benutzer!n von diesem Raum ausschließen. + Das Entfernen wird den Benutzer von diesem Raum ausschließen. \n -\nUm einen erneuten Beitritt zu verhindern, solltest du ihn/sie stattdessen bannen. - Benutzer:in bannen +\nUm einen erneuten Beitritt zu verhindern, solltest du ihn stattdessen bannen. + Benutzer bannen Grund für den Bann Bann des Benutzers aufheben - Das Aufheben des Bannes wird dem/r Benutzer:in erlauben dem Raum wieder beizutreten. + Das Aufheben des Bannes wird dem Benutzer erlauben dem Raum wieder beizutreten. Sicheres Backup Verwalten Backup einrichten @@ -2292,7 +2292,7 @@ Sticker Administrative Aktionen Standard in %1$s - Dein:e Serveradministrator:in hat in privaten Räumen & Direktnachrichten Ende-zu-Ende Verschlüsselung standardmäßig deaktiviert. + Dein Serveradministrator hat in privaten Räumen und Direktnachrichten Ende-zu-Ende Verschlüsselung standardmäßig deaktiviert. Flugzeugmodus ist aktiv Gib eine Sicherheitsphrase ein, die nur du kennst. Diese wird benutzt um deine Daten auf dem Server geheim zu halten. Wenn du jetzt abbrichst und den Zugriff zu deinen Sitzungen verlierst, kannst du verschlüsselte Nachrichten & Daten verlieren. @@ -2447,7 +2447,7 @@ Die Applikation wartet auf den PUSH Push testen Die Suche in verschlüsselten Räumen wird noch nicht unterstützt. - Gebannte Nutzer:innen filtern + Gebannte Nutzer filtern Du bist nicht berechtigt einen Anruf zu starten Du hast keine Berechtigung ein Konferenzgespräch zu starten Zeige Details wie Raumnamen und Nachrichteninhalt. @@ -2462,7 +2462,7 @@ Zeigen das Gerät, mit dem du jetzt überprüfen kannst Zeigen %d Geräte, mit denen du jetzt überprüfen kannst - Du wirst ohne Nachrichtenverlauf, Nachrichten, vertraute Geräten und vertraute Nutzer!nnen neu starten + Du wirst ohne Nachrichtenverlauf, Nachrichten, vertraute Geräten und vertraute Nutzern neu starten Wenn du alles zurücksetzt Mache dies nur, wenn du kein anderes Gerät hast, mit dem du dieses verifizieren kannst. Alles zurücksetzen @@ -2474,9 +2474,9 @@ Einstellungen Nachrichten hier sind Ende-zu-Ende verschlüsselt. \n -\nDeine Nachrichten sind mit digitalen Schlüsseln gesichert. Nur du und der/die Empfänger!n haben die einzigen Schlüssel, um jene zu entsperren. +\nDeine Nachrichten sind mit digitalen Schlüsseln gesichert. Nur du und der Empfänger haben die einzigen Schlüssel, um jene zu entsperren. Nachrichten hier sind nicht Ende-zu-Ende verschlüsselt. - Dieser Homeserver läuft mit einer alten Version. Bitte deine Homeserver-Administration um ein Upgrade. Du kannst fortfahren, aber einige Funktionen funktionieren möglicherweise nicht richtig. + Dieser Homeserver läuft mit einer alten Version. Bitte deinen Homeserver-Administrator um eine Aktualisierung. Du kannst fortfahren, aber einige Funktionen funktionieren möglicherweise nicht richtig. Du hast dies auf Einladungen beschränkt. %1$s hat dies auf Einladungen beschränkt. Zeige vollständigen Verlauf in verschlüsselten Räumen an @@ -2519,7 +2519,7 @@ E-Mails und Telefonnummern senden Vorschläge Kontakte - Bekannte Nutzer:innen + Bekannte Nutzer Kürzlich QR-Code Hinzufügen via QR-Code @@ -2586,7 +2586,7 @@ Diese Adresse veröffentlichen Lokale Adresse hinzufügen Dieser Raum hat keine lokalen Adressen - Füge Adressen für diesen Raum hinzu, damit andere Nutzer:innen ihn auf %1$s finden können + Füge Adressen für diesen Raum hinzu, damit andere Nutzer ihn auf %1$s finden können Lokale Adresse Neue öffentliche Adresse (z.B. #alias:server) Noch keine weiteren öffentlichen Adressen vorhanden. @@ -2603,12 +2603,12 @@ Haupt-Adresse des Raums ändern Raum-Bild ändern Widgets verändern - Jede/n benachrichtigen + Jeden benachrichtigen Von anderen gesendete Nachrichten entfernen - Nutzer:in verbannen - Nutzer:in entfernen + Nutzer verbannen + Nutzer entfernen Einstellungen ändern - Nutzer:in einladen + Nutzer einladen Nachrichten senden Standard Rolle Berechtigungen @@ -2641,7 +2641,7 @@ Erneute Authentifizierung erforderlich Cross Signing konnte nicht eingerichtet werden Nicht autorisierte, fehlende gültige Authentifizierungsdaten - Nutzer:innen + Nutzer Beim Übertragen des Anrufs ist ein Fehler aufgetreten Übertragen Verbinden @@ -2722,4 +2722,16 @@ \nLade Daten herunter… Erste Synchronisation: \nWarte auf Serverantwort… + Gesendet + Raumverzeichnis + Wechseln + Zeige alle Räume im Raumverzeichnis, inklusive der Räume mit anstößigen Inhalten. + Zeige Räume mit anstößigen Inhalten + Bist du dir sicher, dass du alle nicht gesendete Nachrichten in diesem Raum löschen willst\? + Nicht gesendete Nachrichten löschen + Fehlgeschlagen + Willst du zu sendende Nachrichten zurückziehen\? + Alle fehgeschlagene Nachrichten löschen + Senden der Nachricht gescheitert + Wird gesendet \ No newline at end of file diff --git a/vector/src/main/res/values-eo/strings.xml b/vector/src/main/res/values-eo/strings.xml index 747511f745..4e506e1d59 100644 --- a/vector/src/main/res/values-eo/strings.xml +++ b/vector/src/main/res/values-eo/strings.xml @@ -1596,7 +1596,7 @@ Por ligi al ĉambro, ĝi devas havi adreson. Nur anoj (ekde aliĝo) Nur anoj (ekde sia aliĝo) - Nur anoj (ekde elekto de ĉi tiu elekteblo) + Nur anoj (ekde ĉi tiu elekto) Ĉiu ajn Kio povas aliri ĉi tiun ĉambron\? Kiu povas legi historion\? @@ -2393,4 +2393,92 @@ Aldoni Ekbabili Implicita de sistemo + Ĉu forlasi la nunan grupan vokon kaj iri al la alia\? + Versio de ĉambro + Ne povis akiri la nunan videblecon en la katalogo de ĉambroj (%1$s). + Ĉu publikigi ĉi tiun ĉambron per la katalogo de ĉambroj de %1$s\? + Malpublikigi ĉi tiun adreson + Publikigi ĉi tiun adreson + Aldoni lokan adreson + Ĉi tiu ĉambro ne havas lokajn adresojn + Agordu adresojn por ĉi tiu ĉambro, por ke uzantoj ĝin facile trovu per via hejmservilo (%1$s) + Ŝanĝi la temon + Gradaltigi la ĉambron + Sendi eventojn de la speco «m.room.server_acl» + Ŝanĝi permesojn + Ŝanĝi nomon de ĉambro + Ŝanĝi videblecon de historio + Ŝalti tutvojan ĉifradon + Ŝanĝi ĉefadreson de la ĉambro + Ŝanĝi bildon de ĉambro + Ŝanĝi fenestraĵojn + Sciigi ĉiujn + Forigi mesaĝojn senditajn de aliuloj + Forbari uzantojn + Forpeli uzantojn + Ŝanĝi agordojn + Inviti uzantojn + Sendi mesaĝon + Ordinara rolo + Permesoj en ĉambro + Nerajtigite, mankas validaj aŭtentikigiloj + Montri ĉiujn ĉambrojn en la katalogo de ĉambro, inkluzive tiujn kun konsterna enhavo. + Montri ĉambrojn kun konsterna enhavo + Katalogo de ĉambroj + Nova valoro + Vi ŝanĝis la adresojn por ĉi tiu ĉambro. + %1$s ŝanĝis la adresojn por ĉi tiu ĉambro. + Vi ŝanĝis la ĉefan kaj alternativajn adresojn por ĉi tiu ĉambro. + %1$s ŝanĝis la ĉefan kaj alternativajn adresojn por ĉi tiu ĉambro. + Vi ŝanĝis la alternativajn adresojn por ĉi tiu ĉambro. + %1$s ŝanĝis la alternativajn adresojn por ĉi tiu ĉambro. + + Vi forigis la alternativan adreson %1$s por ĉi tiu ĉambro. + Vi forigis la alternativajn adresojn %1$s por ĉi tiu ĉambro. + + + %1$s forigis la alternativan adreson %2$s por ĉi tiu ĉambro. + %1$s forigis la alternativajn adresojn %2$s por ĉi tiu ĉambro. + + + Vi aldonis la alternativan adreson %1$s por ĉi tiu ĉambro. + Vi aldonis la alternativajn adresojn %1$s por ĉi tiu ĉambro. + + + %1$s aldonis la alternativan adreson %2$s por ĉi tiu ĉambro. + %1$s aldonis la alternativajn adresojn %2$s por ĉi tiu ĉambro. + + Komenca spegulado: +\nElŝutante datumojn… + Komenca spegulado: +\nAtendante respondon de servilo… + Malplena ĉambro (estis %s) + + %1$s, %2$s, %3$s, kaj %4$d alia + %1$s, %2$s, %3$s, kaj %4$d aliaj + + %1$s, %2$s, %3$s kaj %4$s + %1$s, %2$s kaj %3$s + Vi ŝanĝis grupan vidvokon + Grupan vidvokon ŝanĝis %1$s + Vi finis grupan vidvokon + Grupan vidvokon finis %1$s + Vi komencis grupan vidvokon + Grupan vidvokon komencis %1$s + 🎉 Partoprenado de ĉiuj serviloj estas malpermesita! Ĉi tiu ĉambro ne plu uzeblas. + Senŝanĝe. + • Serviloj akordaj kun precizaj IP-adresoj nun estas forbaritaj. + • Serviloj akordaj kun precizaj IP-adresoj nun estas permesitaj. + • Serviloj akordaj kun %s foriĝis de la listo de forbaritaj. + • Serviloj akordaj kun %s foriĝis de la listo de permesitaj. + • Serviloj akordaj kun %s nun estas permesitaj. + • Serviloj akordaj kun %s nun estas forbaritaj. + Vi ŝanĝis la alirpermesojn por serviloj por ĉi tiu ĉambro. + %s ŝanĝis la alirpermesojn por serviloj por ĉi tiu ĉambro. + • Serviloj akordaj kun precizaj IP-adresoj estas forbaritaj. + • Serviloj akordaj kun precizaj IP-adresoj estas permesitaj. + • Serviloj akordaj kun %s estas permesitaj. + • Serviloj akordaj kun %s estas forbaritaj. + Vi agordis la alirpermesojn por serviloj por ĉi tiu ĉambro. + %s agordis la alirpermesojn por serviloj por ĉi tiu ĉambro. \ No newline at end of file diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 2dd13ede4a..2b4a0e0590 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -2556,7 +2556,7 @@ Muuda vestlusajaloo nähtavust Võta jututoas kasutusele krüptimine Muuda jututoa põhiaadressi - Muuda jututoa profiilipilti ehk avatari + Muuda jututoa tunnuspilti ehk avatari Muuda vidinaid Teavita kõiki Kustuta teiste saadetud sõnumid @@ -2598,7 +2598,7 @@ See kõne on lõppenud %1$s keeldus kõnest Sa keeldusid %1$s kõnest - Sul on parasjagu see mõne pooleli + Sul on parasjagu see kõne pooleli %1$s alustas kõnet Sa alustasid kõnet Sina panid kõne ootele @@ -2661,4 +2661,15 @@ \nLaadin andmed alla… Esmane sünkroniseerimine: \nOotan serveri vastust… + Näita jututubade kataloogist kõiki jututubasid, sealhulgas neid, kus on ebasobilikku sisu. + Näita jututubasid, kus on ebasobilikku sisu + Nende sõnumite saatmine ei õnnestunud + Kas sa kindlasti soovid sellest jututoast kustutada kõik saatmata sõnumid\? + Kustuta saatmata sõnumid + Kas sa soovid katkestada sõnumi saatmist\? + Kustuta kõik sõnumid, mille saatmine ei õnnestunud + Saatmine ei õnnestunud + Saadetud + Saadan + Jututubade kataloog \ No newline at end of file diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml index 0c7ab7a71c..7937f4f4b2 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -2661,4 +2661,15 @@ تطبیق سرور %s اجازه داده شده‌است. تطبیق سرور %s ممنوع شده‌است. شما ACL های سرور را برای این اتاق تنظیم کردید. + آیا مطمئن هستید که می خواهید همه پیام های ارسال نشده در این اتاق را حذف کنید؟ + حذف پیام‌های ارسال نشده + پیام ارسال نشد + آیا می خواهید ارسال پیام را لغو کنید؟ + حذف تمامی پیام‌های ناموفق + ناموفق + ارسال شد + در حال ارسال + نمایش همه‌ی اتاق‌های داخل فهرست. + نمایش‌ها همه‌ی اتاق‌ها + فهرست اتاق‌ها \ No newline at end of file diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 1661178b50..33f19760af 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -102,7 +102,7 @@ %1$s poisti tältä huoneelta osoitteen %2$s. - %1$s poisti tältä huoneelta osoitteet %3$s. + %1$s poisti tältä huoneelta osoitteet %2$s. %1$s lisäsi tälle huoneelle osoitteen %2$s ja poisti osoitteen %3$s. %1$s asetti tämän huoneen pääosoitteeksi %2$s. @@ -2096,7 +2096,7 @@ Näytä emoji-näppäimistö Tiliisi ei ole lisätty sähköpostiosoitetta Tiliisi ei ole lisätty puhelinnumeroa - Ilmoittaa kaikille + Ilmoita kaikille Haluatko peruuttaa tämän käyttäjän kutsun\? Tämä huone ei ole julkinen. Jos poistut, et voi liittyä takaisin ilman kutsua. Tämän asetuksen käyttöön ottaminen lisää FLAG_SECURE kaikkiin toimintoihin. Käynnistä sovellus uudestaan, jotta muutos tulee voimaan. @@ -2122,7 +2122,7 @@ Hallitse Matrix-tiliisi linkitettyjä sähköpostiosoitteita ja puhelinnumeroita Aseta tilille uusi salasana… Katselet ilmoitusta! Napsauta minua! - Napsauta ilmoitusta. Jos ilmoitusta ei näy, tarkista järjestelmäasetukset. + Napsauta ilmoitusta. Jos ilmoitusta ei näy, tarkasta järjestelmäasetukset. Ilmoitusnäyttö Vianmääritys Ilmoitustapa diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 315e3fe978..31eb24371c 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -100,7 +100,7 @@ %1$s a supprimé %2$s comme adresse pour ce salon. - %1$s a supprimé %3$s comme adresses pour ce salon. + %1$s a supprimé %2$s comme adresses pour ce salon. %1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon. %1$s a défini %2$s comme adresse principale pour ce salon. @@ -811,7 +811,7 @@ Prendre une photo Prendre une vidéo Statistiques d’utilisation - Utiliser la caméra native + Utiliser la caméra de l’appareil Rapport d’anomalie Attention ! @@ -1481,7 +1481,7 @@ Envoyer un nouveau message privé Voir le répertoire des salons Nom ou identifiant (#exemple:matrix.org) - Activer le balayage pour répondre dans les l’historique + Activer le balayage pour répondre dans l’historique Lien copié dans le presse-papiers Gestionnaire d’intégrations Aucun gestionnaire d’intégrations n’est configuré. @@ -2182,7 +2182,7 @@ Retour à l’appel Annuler l’invitation Ignorer l’utilisateur - Ignorer cet utilisateur aura pour effet de supprimer ses messages des espaces que vous partagez. + Ignorer cet utilisateur aura pour effet de supprimer ses messages des salons que vous partagez. \n \nVous pouvez annuler cette action à tout moment dans les paramètres généraux. Annuler l’invitation @@ -2372,10 +2372,10 @@ Aucune adresse e-mail n’a été ajoutée à votre compte Adresses e-mail Aucun numéro de téléphone n’a été ajouté à votre compte - La recherche dans les espaces chiffrés n\'est pas encore prise en charge. + La recherche dans les salons chiffrés n\'est pas encore prise en charge. Filtrer les utilisateurs exclus Ne plus ignorer cet utilisateur aura pour effet de ré-afficher ses messages. - expulser un utilisateur le supprimera de cet espace. + expulser un utilisateur le supprimera de ce salon. \n \nPour l’empêcher de revenir, vous devez plutôt le bannir. Motif d’expulsion @@ -2526,8 +2526,8 @@ Rejoindre Consulter d’abord - 1 appel en cours (%1$d) ⋅ 1 appel en attente - 1 appel en cours (%1$d) ⋅ %2$d appels en attente + 1 appel en cours (%1$s) ⋅ 1 appel en attente + 1 appel en cours (%1$s) ⋅ %2$d appels en attente Appel en attente @@ -2643,16 +2643,16 @@ Vous avez modifié les adresses alternatives de ce salon. %1$s a modifié les adresses alternatives de ce salon. - Vous avez supprimé l’adresse alternative %2$s de ce salon. - Vous avez supprimé les adresses alternatives %2$s de ce salon. + Vous avez supprimé l’adresse alternative %1$s de ce salon. + Vous avez supprimé les adresses alternatives %1$s de ce salon. %1$s a supprimé l’adresse alternative %2$s de ce salon. %1$s a supprimé les adresses alternatives %2$s de ce salon. - Vous avez ajouté %2$s comme adresse alternative pour ce salon. - Vous avez ajouté %2$s comme adresses alternatives pour ce salon. + Vous avez ajouté %1$s comme adresse alternative pour ce salon. + Vous avez ajouté %1$s comme adresses alternatives pour ce salon. %1$s a ajouté %2$s comme adresse alternative pour ce salon. @@ -2668,4 +2668,15 @@ %1$s a mis fin à la téléconférence Vous avez démarré la téléconférence Téléconférence démarrée par %1$s + Êtes-vous sûr de vouloir supprimer tous les messages non envoyés dans ce salon \? + Supprimer les messages non envoyés + Messages non envoyés + Voulez-vous annuler l’envoi du message \? + Supprimer tous les messages en échec + Échec + Envoyé + Envoi + Afficher tous les salons dans le répertoire, y compris ceux au contenu choquant. + Afficher les salons au contenu choquant + Répertoire des salons \ No newline at end of file diff --git a/vector/src/main/res/values-ga/strings.xml b/vector/src/main/res/values-ga/strings.xml new file mode 100644 index 0000000000..ee84da7b92 --- /dev/null +++ b/vector/src/main/res/values-ga/strings.xml @@ -0,0 +1,165 @@ + + + CUAIRTEOIRÍ + Socruithe + Comhaid + Daoine + Ceadanna + Tabhair neamhaird ar + Logáil amach + Cuir muinín i + Cuardaigh + "%1$s, " + Cúis + Taispeáin na teachtaireachtaí an úsáideora seo + Tabhair neamhaird ar teachtaireachtaí an úsáideora seo + Bain ceadanna + Luaigh + Caith amach + Bain an coisc + Coisc + Tabhair cuireadh + SEISIÚIN + GLAOIGH AR + Díomhaoin + As líne + Ar Líne + Cruthaigh + + %dl + %dl + %dl + %dl + %dl + + + %du + + %du + %du + %du + + + %dn + + + %dn + %dn + + + + + + %ds + + + Ag sioncronú… + Diúltaigh + Réamhamharc + Téigh isteach + Bain + Lean ar aghaidh + NÍL + + Sábháilte + Eolas + Fan + Tosaigh arís + ag Glaoch ar… + Glaoigh ar + Glaonna + Inniu + Inné + Beag + Meánach + Mór + Bunchóip + Pasfhocal + Léim + Cuir isteach + Ar Ais + Tosaigh + Gléas cinn + Callaire + Guthán + Cuardaigh + Ainm úsáideora + Léite + Pobail + Tabhair cuireadh + Seomraí + Comhráite + Cuirí + Pobail + Seomraí + Daoine + Ceanáin + Fógraí + Tús + Rath + Earráid + Rabhadh + Deimhniú + Fill + Cuir as feidhm + Neamhfoilsigh + Athraigh + Cuir + Cóipeáil + Dún + Oscail + Stairiúil + Gníomhartha + Fág + Diúltaigh + Glac + Diúltaigh + Athbhreithnigh + Déan neamhaird de + Tobscoir + Críochnaithe + Léim + Glac + As líne + Tabhair cuireadh + + Fís + Guth + Athshocraigh + Cuir uait + Cuir ar sos + Cuir ar siúl + Dícheangail + Cúlghair + Níl aon cheann + Athainmnigh + Bain amach + Nasc buan + Seol ar aghaidh + Níos deireanaí + Glan + Labhair + Roinn le + Íoslódáil + Luaigh + Bain + Athsheol + Seol + Fan + Fág + Sábháil + Cealaigh + Ceart go leor + Ag lódáil… + Stairiúil + Socruithe + Seomra + Teachtaireachtaí + Ag sioncronú… + Saincheaptha + Réamhshocrú + Modhnóir + Riarthóir + aon duine. + %1$s: %2$s + \ No newline at end of file diff --git a/vector/src/main/res/values-ga/strings_no_weblate.xml b/vector/src/main/res/values-ga/strings_no_weblate.xml new file mode 100644 index 0000000000..ec03f726fd --- /dev/null +++ b/vector/src/main/res/values-ga/strings_no_weblate.xml @@ -0,0 +1,8 @@ + + + + ga + IE + Latn + + \ No newline at end of file diff --git a/vector/src/main/res/values-gl/strings.xml b/vector/src/main/res/values-gl/strings.xml index bbfe838ca7..61beb07197 100644 --- a/vector/src/main/res/values-gl/strings.xml +++ b/vector/src/main/res/values-gl/strings.xml @@ -830,4 +830,28 @@ Copia de apoio da chave Iniciando o servizo Por defecto no sistema + Debido á falta de permisos, esta acción non é posible. + Iniciar Chat + Restablecer + Desbotar + Deter + Reproducir + Desconectar + Revogar + Nada + Permanecer + Vas perder o acceso ás túas mensaxes cifradas a non ser que fagas unha copia de apoio das chaves antes de desconectar. + Copiar + Tes a certeza\? + Usa a Copia de apoio das Chaves + Copiando as chaves… + Non quero as miñas mensaxes cifradas + A Copia Segura das Chaves está activa para tódalas túas sesións para evitar perder o acceso ás mensaxes cifradas. + Estase realizando a copia de apoio. Se desconectas agora perderás o acceso ás mensaxes cifradas. + Vas perdelas mensaxes cifradas se desconectas agora + Non rematou a copia das chaves, agarda… + Sincr. inicial: +\nDescargando datos… + Sincr. inicial: +\nAgardando resposta do servidor… \ No newline at end of file diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml index af8863355c..3afab6fa23 100644 --- a/vector/src/main/res/values-in/strings.xml +++ b/vector/src/main/res/values-in/strings.xml @@ -1,11 +1,9 @@ - + Undang dari %s Undangan Ruang %1$s dan %2$s - Ruang kosong - %1$s dan %2$d yang lain @@ -30,37 +28,30 @@ Panggilan Video Balasan Cepat Peringatan - Konfirmasi Buka Tutup Nonaktifkan - Favorit Cari ruang Cari favorit Cari orang Cari ruang - Direktori Pengguna Tidak ada hasil - Ruang umum belum tersedia Direktori ruang Kirim log Deskripsikan kendala Anda di sini Laporan bug telah berhasil dikirimkan Baca - Daftar Masuk Copot akun URL Server Mula Cari - Mulai Obrolan Baru Ambil foto atau video - Kirim Password Password Baru @@ -85,29 +76,24 @@ Tidak dapat memulai panggilan Keluar Offline - Pencarian global Tandai semua sudah dibaca Orang Ruang - Undangan Percakapan Buku alamat lokal Prioritas rendah - Hanya kontak Matrix Ruang Laporan bug "Aplikasi gagal saat terakhir digunakan. Apakah Anda ingin membuka halaman laporan kegagalan?" - Gabung di Ruang URL Server Identity Mulai Panggilan Suara Masuk Buat Akun Mulai Panggilan Video - Kirim file Password terlalu pendek (min 6) Alamat email ini sudah terdefinisi. @@ -124,35 +110,29 @@ Tidak bisa masuk Tidak bisa registrasi Masukkan URL yang benar - Nama pengguna sudah terpakai Asli Besar Sedang Kecil - Kemarin Batalkan unduhan? Batalkan unggahan? Hari ini - Nama ruang Panggilan terhubung Menyambungkan panggilan… Panggilan diakhiri - Informasi Tersimpan Simpan di Downloads? YA TIDAK Lanjut - Hapus Gabung Pratinjau Tolak - Nanti Kirim Saja ${app_name} belum diijinkan untuk mengakses kontak lokal @@ -169,13 +149,12 @@ Pilih direktori ruang Terdapat perangkat tidak diketahui di ruang Saya verifikasi bahwa kuncinya sesuai - perangkat tidak diketahui Terverifikasi Jejak Percakapan - Hapus - Panggilan massal sedang berlangsung.\nBergabunglah lewat %1$s atau %2$s. + Panggilan massal sedang berlangsung. +\nBergabung sebagai %1$s atau %2$s suara video Beberapa fitur tidak dapat digunakan karena aplikasi belum mendapat ijin… @@ -189,14 +168,12 @@ %d pengguna - Kirim tampilan layar Mohon uraikan bug tersebut. Apa yang Anda lakukan? Apa yang Anda harapkan terjadi? Apa yang sebenarnya terjadi? Log dari klien akan dikirim bersama laporan gangguan ini untuk mendalami kendala yang Anda temukan. Laporan gangguan ini, termasuk log dan rekalayar, tidak akan dilihat oleh khalayak umum. Jika Anda hanya ingin mengirimkan tulisan di atas, silahkan hapus centang: Sepertinya Anda mengguncang telepon akibat frustrasi. Apakah Anda ingin membuka halaman laporan bug? Pengiriman laporan bug gagal (%s) Kemajuan (%s%%) - Kirim ke Nama Pengguna Nama pengguna dan/atau kata sandi salah @@ -207,18 +184,17 @@ Alamat email atau nomor telpon belum dimasukkan Gunakan server lain (lanjutan) Silahkan periksa email Anda untuk melanjutkan pendaftaran - Pendaftaran dengan email sekaligus nomor telpon belum didukung sampai API dihadirkan. Hanya nomor telpon yang akan digunakan. - -Anda dapat menambah email di profile Anda dalam pengaturan nantinya. + Pendaftaran dengan menggunakan email dan nomor telepon tidak didukung sampai API dihadirkan. Hanya nomor telepon anda yang akan digunakan. +\n +\nAnda dapat menambahkan email di profil anda melalui menu pengaturan. Nama pengguna yang terpakai Untuk menyetel ulang kata sandi Anda, silahkan masukkan alamat email yang tertaut ke akun Anda: Anda perlu memasukkan alamat email yang tertaut pada akun. - "Selembar email telah dikirim ke %s. Setelah Anda mengikuti tautan yang termuat di dalamnya, klik yang di bawah." + Surel telah dikirim ke alamat %s. Setelah Anda mengikuti tautan yang termuat di dalamnya, klik di bawah. Verifikasi alamat email gagal: pastikan tautan yang termuat di email telah diklik Kata sandi Anda telah disetel ulang. - -Anda telah dikeluarkan dari semua perangkat dan tidak lagi menerima pemberitahuan dorongan. Untuk menerima kembali pemberitahuan, masuk kembali dengan tiap perangkat. - +\n +\nAnda telah dikeluarkan dari seluruh sesi dan tidak lagi menerima push notification. Untuk kembali menerima pemberitahuan, masuklah kembali dengan tiap perangkat. Tidak dapat masuk: Gangguan jaringan Tidak dapat mendaftar: Gangguan jaringan Tidak dapat mendaftar : gagal memastikan kepemilikan alamat email @@ -228,58 +204,47 @@ Anda telah dikeluarkan dari semua perangkat dan tidak lagi menerima pemberitahua Tidak berisi JSON yang sah Pengajuan yang dikirimkan terlalu banyak Tautan email masih belum diklik - Baca Daftar Penerimaan - - "Kirim sebagai " + Kirim sebagai %d d %1$dm %2$dd - Topik ruang - Memanggil… Panggilan Masuk Panggilan Video Masuk Panggilan Suara Masuk Panggilan Sedang Berlangsung… - Hubungan Media Gagal Server Identitas: Tidak dapat memulai kamera panggilan terjawab di tempat lain Ambil gambar atau video Tidak bisa merekam video - - ${app_name} membutuhkan permisi atas akses galeri foto dan video Anda untuk mengirim dan menyimpan lampiran. - -Harap berikan akses pada halaman berikut agar berkas dapat dikirim dari ponsel Anda. + ${app_name} membutuhkan izin untuk mengakses galeri foto dan video Anda untuk mengirim dan menyimpan lampiran. +\n +\nHarap berikan akses pada halaman berikut ini agar berkas dapat dikirim dari ponsel Anda. ${app_name} membutuhkan izin Anda untuk mengakses kamera untuk mengambil gambar dan melakukan panggilan video. - - -Harap berikan akses pada halaman berikut agar dapat melakukan panggilan. + " +\n +\nHarap berikan akses pada halaman berikut ini agar dapat melakukan panggilan." ${app_name} membutuhkan permisi atas akses mikrofon Anda untuk melakukan panggilan audio. - - -Harap berikan akses pada halaman berikut agar dapat melakukan panggilan. - ${app_name} membutuhkan permisi atas akses kamera dan mikrofon Anda untuk melakukan panggilan video. - -Harap berikan akses pada halaman selanjutnya untuk melakukan panggilan. + " +\n +\nHarap berikan akses pada halaman berikut ini agar dapat melakukan panggilan." + ${app_name} membutuhkan izin untuk mengakses kamera dan mikrofon Anda untuk melakukan panggilan video. +\n +\nHarap berikan akses pada halaman berikut ini untuk melakukan panggilan. Tema Terang Tema Kelam Tema Gelap - - Sedang Sinkronisasi + Menyinkronkan… Pemberitahuan Berisik Pemberitahuan Tenteram - Laporan Gangguan Detail Komunitas Kirimkan Sticker - Lisensi Pihak Ketiga - Memuat… - Unduh Bicaralah Bersihkan @@ -287,69 +252,50 @@ Harap berikan akses pada halaman selanjutnya untuk melakukan panggilan. Keluar Tindakan Komunitas - Mencari komunitas - Peringatan Sistem - Undang Komunitas Tidak ada grup - Mohon deskripsikan dengan bahasa Inggris apabila memungkinkan. Guncang perangkat untuk laporan gangguan - Kirim Pesan Suara - Apa benar Anda ingin memulai percakapan baru dengan %s? Apa benar Anda ingin memulai panggilan suara? Apa benar Anda ingin memulai panggilan video? - Kirim Sticker Ambil foto Ambil video - Saat ini Anda belum memiliki pak stiker. \n \nMau tambah sekarang\? - lanjutkan dengan… Maaf, tidak ada aplikasi eksternal yang mendukung apa yang ingin dilakukan. - Meminta ulang kunci enkripsi dari perangkat Anda yang lain. - Permintaan kunci terkirim. - Permintaan terkirim Jalankan ${app_name} di perangkat yang dapat mendekripsi pesan tersebut agar kunci dapat dikirim ke perangkat ini. - Daftar Grup - %d perubahan keanggotaan - Panggilan ${app_name} memerlukan permisi untuk mengakses daftar kontak agar dapat mencari pengguna Matrix lain berdasarkan email dan nomor telepon. Ijinkan akses lewat halaman selanjutnya untuk menemukan pengguna ${app_name} yang terdapat di daftar kontak Anda. - ${app_name} memerlukan permisi akses daftar kontak Anda untuk menemukan pengguna Matrix lain berdasarkan email dan nomor telepon mereka. - -Bolehkah ${app_name} mengakses daftar kontak Anda? - - "Maaf. Tidak dapat dilakukan karena belum menerima permisi" - + ${app_name} memerlukan izin untuk mengakses daftar kontak Anda untuk menemukan pengguna Matrix lain berdasarkan email dan nomor telepon mereka. +\n +\nApakah anda bersedia bila ${app_name} mengakses daftar kontak Anda\? + Mohon Maaf. Aksi ini tidak dapat dilakukan karena belum menerima izin terkait Daftar Anggota Buka kop Menyinkronkan… Arahkan ke pesan pertama yang belum terbaca. - Anda telah diundang untuk bergabung ke ruang ini oleh %s Undangan ini dikirim oleh %s, yang tidak terhubung dengan akun ini. -Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda. +\nAnda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda. Anda sedang berupaya untuk mengakses %s. Maukah Anda bergabung untuk berpartisipasi dalam diskusi ini? Ini adalah pratinjau untuk ruang ini. Interaksi dengan ruang belum dapat dilakukan. - Percakapan Baru Tambah anggota @@ -359,7 +305,6 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda %d anggota 1 anggota - %dd @@ -372,23 +317,19 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda $dh - Tinggalkan ruang Apa benar Anda ingin meninggalkan ruang ini? Apa benar Anda ingin mengeluarkan %s dari percakapan ini? Buat - Online Offline Berdiam Diri %1$s sekarang %1$s %2$s yang lalu - PERALATAN ADMIN PANGGIL PERCAKAPAN LANGSUNG PERANGKAT - Undang Tinggalkan ruang ini Keluarkan dari ruang ini @@ -402,15 +343,12 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda ID Pengguna, Nama atau email Sebut Tunjukkan Daftar Perangkat - Anda tidak akan dapat membalik perubahan ini karena Anda mengangkat pengguna ini agar memiliki kuasa yang setara dengan Anda. -Yakin? - - Apa benar Anda ingin melarang pengguna ini dari percakapan ini? - + Anda tidak akan dapat mengembalikan perubahan ini setelah Anda mengangkat pengguna ini agar memiliki kuasa yang setara dengan Anda. +\nApakah anda yakin untuk melanjutkan\? + Melakukan banning pengguna akan mengeluarkannya dari ruangan ini dan mencegahnya untuk kembali masuk. Apa benar Anda ingin mengundang %s ke percakapan ini? %1$s dan %2$s %1$s %2$s - Gagal terjawab oleh pihak lain. ruang "%1$s, " @@ -418,11 +356,9 @@ Yakin? KONTAK LOKAL (%d) DIREKTORI PENGGUNA (%s) Pengguna Matrix saja - Undang pengguna dengan ID Masukkan satu atau lebih alamat email atau ID Matrix Email atau ID Matrix - Cari %s sedang mengetik… %1$s & %2$s sedang mengetik… @@ -443,7 +379,6 @@ Yakin? %d pesan baru - Percaya Tidak percaya Keluar @@ -455,7 +390,6 @@ Yakin? Sertifikat ini tidak lagi sesuai dengan yang dipercayai oleh perangkat Anda sebelumnya. Ini SANGAT JANGGAL. Kami rekomendasikan Anda untuk TIDAK MENERIMA sertifikat baru ini. Terdapat perubahan sertifikat yang tidak lagi dipercayai perangkat. Server mungkin telah memperbaharui sertifikatnya. Hubungi administrator server untuk pencocokan sidik jari. Hanya terima sertifikat ini apabila administrator server telah menerbitkan sidik jari yang cocok dengan yang tertera di atas. - Detail Ruang Orang Berkas @@ -466,14 +400,12 @@ Yakin? ID tidak sesuai. Seharusnya alamat email atau ID Matrix semisal \'@localport:domain\' DIUNDANG BERGABUNG - Alasan laporan konten ini - Apa benar Anda ingin menyembunyikan semua pesan dari pengguna ini? - -Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu. + Apa Anda ingin menyembunyikan seluruh pesan dari pengguna ini\? +\n +\nHarap diperhatikan bahwa tindakan ini akan me-restart aplikasi dan mungkin akan memakan waktu beberapa saat. Batalkan Unggahan Batalkan Unduhan - Cari Saring anggota ruang Tiada hasil @@ -481,7 +413,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema PESAN ORANG BERKAS - GABUNG DIREKTORI FAVORIT @@ -493,7 +424,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Gabung ke ruang Gabung ke ruang Ketik id atau alias ruang - Jelajahi direktori %d ruang @@ -502,7 +432,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema %1$s ruang ditemukan untuk %2$s Mencari direktori… - Semua pesan (berisik) Semua pesan Hanya sebutan @@ -513,7 +442,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Tinggalkan Percakapan Lupakan Tambahkan Shortcut pada Homescreen - Pesan Pengaturan Versi @@ -521,7 +449,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Pemberitahuan pihak ketiga Hak Cipta Kebijakan Pribadi - Gambar Profil Nama Layar Email @@ -530,7 +457,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Tambahkan nomor telepon Tampilkan info aplikasi dalam pengaturan sistem. Info aplikasi - Kerahasiaan pemberitahuan Normal Kerahasiaan diperlemah @@ -540,12 +466,10 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema • Isi pesan pemberitahuan tersimpan langsung dengan aman di homeserver Matrix • Pemberitahuan memuat meta data dan data pesan • Pemberitahuan tidak akan menunjukkan isi pesan - Suara pemberitahuan Perbolehkan pemberitahuan untuk akun ini Perbolehkan pemberitahuan untuk perangkat ini Nyalakan layar selama 3 detik - Pesan yang berisikan nama layarku Pesan berisikan nama layarku Pesan percakapan empat mata @@ -553,13 +477,11 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Kapan saya diundang ke suatu ruang Undangan panggilan Pesan yang dikirim bot - Mulai sedari boot Sinkronisasi di balik layar Perbolehkan sinkronisasi di balik layar Batas waktu permohonan sinkronisasi Masa tunda sebelum permohonan berikutnya - Versi versi olm Syarat & ketentuan @@ -567,9 +489,7 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema %d ruang %1$s dalam %2$s - Cari sejarah - Anda butuh permisi untuk mengurus widget di ruang ini Pembuatan widget gagal Buat panggilan konferensi dengan jitsi @@ -577,7 +497,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema %d widget aktif - Tidak dapat membuat widget. Gagal mengirim permohonan. Tingkat energi harus bilangan positif. @@ -592,16 +511,13 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Gunakan kamera bawaan Gunakan tombol enter keyboard untuk mengirim pesan Kirim pesan suara - Anda menambahkan perangkat baru \'%s\', yang sedang meminta kunci enkripsi. Perangkat Anda yang belum terverifikasi \'%s\' sedang meminta kunci enkripsi. Mulai verifikasi Bagikan tanpa verifikasi Abaikan verifikasi - Peringatan! Panggilan konferensi masih sedang pengembangan dan mungkin belum dapat diandalkan. - Kesalahan perintah Perintah tak dikenal: %s Tunjukkan tindakan @@ -616,116 +532,90 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Ubah nama panggilan layar Anda Mati/Nyalakan markdown Untuk memperbaiki kepengurusan Apps Matrix - Mati Berisik - Pesan terenkripsi - Buat Buat Komunitas Nama komunitas Contoh Id Komunitas contoh - Pangkal Orang Ruang Tidak ada pengguna - Ruang Telah bergabung Telah Diundang Saring anggota grup Saring ruang grup - %d anggota - %d ruang Admin komunitas belum menyediakan deskripsi panjang untuk komunitas ini. - Anda telah dikeluarkan dari %1$s oleh %2$s Anda telah dilarang dari %1$s oleh %2$s Alasan: %1$s Gabung lagi Lupakan ruang - Untuk terus menggunakan homeserver %1$s Anda harus membaca dan menyetujui syarat dan ketentuan. Avatar - Baca sekarang - Deaktivasi Akun - Ini akan mengakibatkan akun Anda tidak dapat digunakan secara permanen. Anda tidak akan dapat masuk dan orang lain tidak dapat mendaftar ulang dengan ID pengguna yang sama. Ini akan mengakibatkan akun Anda keluar dari semua ruang di mana Anda berpartisipasi dan menghapus semua detail akun dari identity server Anda. Tindakan ini tidak dapat dibalikkan. - -Mendeaktivasi akun Anda tidak semerta membuat kami melupakan pesan-pesan yang Anda kirim. Jika Anda ingin kami melupakan pesan-pesan Anda, mohon centang kotak berikut. - -Pembacaan pesan di Matrix serupa dengan email. Dengan kami melupakan pesan-pesan Anda berarti pesan-pesan yang Anda kirim tidak akan dibagikan kepada pengguna baru atau yang belum terdaftar, tapi pengguna yang terdaftar dan telah dapat mengakses pesan-pesan tersebut masih bisa membaca rangkap yang mereka simpan. + Ini akan mengakibatkan akun Anda tidak dapat digunakan secara permanen. Anda tidak akan dapat masuk dan orang lain tidak dapat mendaftar ulang dengan ID pengguna yang sama. Ini akan mengakibatkan akun Anda keluar dari semua ruang tempat Anda berpartisipasi serta menghapus semua detail akun dari identity server Anda. Tindakan ini tidak dapat diubah kembali. +\n +\nMenonaktifkan akun Anda tidak serta-merta membuat kami melupakan pesan-pesan yang Anda kirim. Jika Anda ingin kami melupakan pesan-pesan Anda, mohon centang kotak berikut. +\n +\nKeterbacaan pesan di Matrix serupa dengan email. Dengan kami melupakan pesan-pesan Anda, berarti pesan-pesan yang Anda kirim tidak akan dibagikan kepada pengguna baru ataupun yang belum terdaftar. Tetapi pengguna yang terdaftar dan telah dapat mengakses pesan-pesan tersebut masih bisa membaca rangkap yang mereka simpan. Mohon lupakan semua pesan yang telah kukirim ketika akunku dideaktivasi (Peringatan: ini akan mengakibatkan pengguna mendatang membaca percakapan yang tidak lengkap) Untuk melanjutkan, masukkan kata sandi Anda: Deaktivasi Akun - Mohon masukkan kata sandi Anda. Ruang ini telah berubah dan tidak lagi aktif Percakapan berlanjut di sini Ruang ini adalah kelanjutan percakapan lain Klik di sini untuk melihat pesan lama - Melampaui Batasan Sumber Daya Kontak Administrator - kontak administrator layanan Anda - Homeserver ini telah melampaui salah satu batas sumber dayanya sehingga beberapa pengguna tidak dapat masuk. Homeserver ini telah melampaui salah satu batasan sumber dayanya. - Homeserver ini telah mencapai batas Pengguna Aktif Bulanan sehingga beberapa pengguna tidak dapat masuk. Homeserver ini telah mencapai batas Pengguna Aktif Bulanan. - Mohon %s untuk meningkatkan batasan ini. Mohon %s untuk terus menggunakan layanan ini. - Impor kunci ruang terenkripsi Impor kunci ruang Impor kunci dari berkas lokal Impor Hanya enkripsi ke perangkat terverifikasi Jangan kirim pesan terenkripsi ke perangkat yang tidak terverifikasi dari perangkat ini. - TIDAK terverifikasi Ter-blacklist - tidak ada - Verifikasi Batalkan verifikasi Blacklist Batalkan blacklist - Verifikasi perangkat Untuk memastikan perangkat dapat dipercaya, mohon kontak pengguna dengan medium lain (misalnya tatap muka atau panggilan telepon) dan tanya apakah kunci yang mereka lihat di Pengaturan Pengguna untuk perangkat ini cocok dengan kunci berikut: Apabila cocok, tekan tombol verifikasi berikut. Apabila tidak, seseorang sedang menyadap perangkat ini dan mungkin perlu diblokir. Di masa mendatang proses verifikasi ini akan dimutakhirkan. - - Ruang ini terisi oleh perangkat tak dikenal yang belum diverifikasi. -Ini berarti tidak ada jaminan pengguna perangkat tersebut sesuai dengan klaim mereka. -Kami sarankan Anda untuk memverifikasi untuk setiap perangkat terlebih dahulu sebelum melanjutkan, tapi Anda boleh mengirim ulang pesan tanpa verifikasi jika Anda mau. - -Perangkat tak dikenal: - + Ruang ini terdapat sesi yang yang belum diverifikasi. +\nIni artinya, tidak ada jaminan pengguna sesi tersebut sesuai dengan klaim mereka. +\nKami sarankan Anda untuk memverifikasi untuk setiap sesi terlebih dahulu sebelum melanjutkan, namun Anda juga boleh mengirim ulang pesan tanpa verifikasi bila anda memilih demikian. +\n +\nSesi yang tak dikenal: Server mungkin belum siap atau kelebihan beban Ketik homeserver yang ingin Anda lihat daftar ruang publiknya Semua ruang dalam server %s Semua ruang bawaan %s - Ketik di sini… - %d pesan pemberitahuan yang belum terbaca @@ -734,7 +624,6 @@ Perangkat tak dikenal: Prioritas rendah Tidak Ada - Akses dan visibilitas Daftarkan ruang ini di direktori ruang Pemberitahuan @@ -742,19 +631,15 @@ Perangkat tak dikenal: Singkapan Sejarah Ruang Siapa yang bisa membaca sejarah? Siapa yang bisa mengakses ruang ini? - Siapapun Hanya anggota (dimulai sejak opsi ini dipilih) Hanya anggota (dimulai sejak mereka diundang) Hanya anggota (dimulai sejak mereka bergabung) - Ruang harus memiliki alamat agar dapat ditautkan. Hanya orang yang telah diundang Siapapun yang tahu tautan ruang, selain tamu Siapapun yang tahu tautan ruang, termasuk tamu - Pengguna yang dilarang - Lanjutan ID internal ruang ini Alamat @@ -765,38 +650,28 @@ Perangkat tak dikenal: Anda perlu keluar dulu untuk mengaktifkan enkripsi. Enkripsi ke perangkat terverifikasi saja Jangan mengirim pesan terenkripsi ke perangkat yang belum diverifikasi di ruang ini dengan perangkat ini. - Ruang ini tidak punya alamat lokal Alamat baru (misalnya #foo:matrix.org) - Ruang ini tidak menunjukkan flair untuk komunitas manapun ID komunitas baru (misalnya +foo:matrix.org) ID komunitas tidak valid \'%s\' bukan ID komunitas yang valid - - Format alias tidak valid \'%s\' bukanlah format alias yang valid Anda tidak akan mendapat alamat utama untuk ruang ini. Peringatan alamat utama - Tentukan sebagai Alamat Utama Jangan tentukan sebagai Alamat Utama Salin ID Ruang Salin Alamat Ruang - Enkripsi diaktifkan untuk ruang ini. Enkripsi dinonaktifkan untuk ruang ini. Aktifkan enkripsi -(peringatan: tidak lagi bisa dinonaktifkan!) - +\n(peringatan: tidak dapat dinonaktifkan kembali!) Direktori Tema - %s sedang mencoba memuat titik tertentu di rentang waktu ruang ini tapi belum dapat menemukannya. - Informasi enkripsi ujung-ke-ujung - Informasi peristiwa Id pengguna Kunci identitas Curve25519 @@ -804,7 +679,6 @@ Perangkat tak dikenal: Algoritma ID Sesi Kesalahan dekripsi - Informasi perangkat pengirim Nama perangkat Nama @@ -812,17 +686,15 @@ Perangkat tak dikenal: Kunci perangkat Verifikasi Sidik jari Ed25519 - Ekspor kunci ruang terenkripsi Ekspor ruang kunci Ekspor kunci ke berkas lokal Ekspor Masukkan kata sandi Tegaskan kata sandi - Kunci ruang terenkripsi telah disimpan di \'%s\'. - -Peringatan: berkas ini mungkin ikut terhapus jika aplikasi dihapus. - + Kunci E2E ruang tersebut telah disimpan di \'%s\'. +\n +\nPeringatan: berkas ini mungkin ikut terhapus bila aplikasi ini dihapus. Mendengarkan peristiwa Pemberitahuan pihak ketiga Hak Cipta @@ -830,7 +702,6 @@ Peringatan: berkas ini mungkin ikut terhapus jika aplikasi dihapus. Bersihkan cache Bersihkan cache media Pertahankan media - Pengaturan pengguna Pemberitahuan Pengguna yang diabaikan @@ -850,23 +721,18 @@ Peringatan: berkas ini mungkin ikut terhapus jika aplikasi dihapus. Tampilkan waktu kirim dalam format 12 jam Bergetar ketika menyebut seorang pengguna Pratinjau media sebelum dikirim - Deaktivasi akun Deaktivasi akunku - Kerahasiaan Notifikasi ${app_name} dapat beroperasi di balik layar untuk mengurus pemberitahuan Anda dengan aman dan rahasia. Ini dapat mempengaruhi masa tahan baterai. Kabulkan permisi Pilih opsi lain - Analitik Kirim data analitik ${app_name} mengumpulkan data analitik anonim dalam upaya kami meningkatkan aplikasi. Mohon aktifkan analitik untuk membantu kami meningkatkan ${app_name}. Ya, saya ingin membantu! - Mode hemat data - Rincian perangkat ID Nama @@ -874,42 +740,34 @@ Peringatan: berkas ini mungkin ikut terhapus jika aplikasi dihapus. Terakhir terlihat %1$s @ %2$s Operasi ini membutuhkan otentikasi tambahan. -Untuk melanjutkan, masukkan kata sandi Anda. +\nUntuk melanjutkan operasi ini, mohon masukkan kata sandi anda. Otentikasi Kata Sandi: Serahkan - Masuk sebagai Home Server Server Identitas - Antarmuka pengguna Bahasa Pilih bahasa - Verifikasi Tertunda Mohon cek email Anda dan klik tautan yang termuat di sana. Setelah itu, klik lanjutkan. Tidak dapat memverifikasi alamat email. Mohon periksa email Anda dan klik tautan yang termuat di sana. Setelah itu, klik lanjutkan. Alamat email ini telah digunakan. Gagal mengirim email: Alamat email ini tidak dapat ditemukan. Nomor telepon ini telah digunakan. - Ubah kata sandi Sandi lama Sandi baru Ulangi sandi Gagal memperbaharui kata sandi Kata sandi Anda telah diperbaharui - Tunjukkan semua pesan dari %s? - -Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu. - + Tunjukkan semua pesan dari %s\? +\n +\nMohon perhatikan bahwa tindakan ini akan me-restart aplikasi dan mungkin akan memakan waktu. Apa benar Anda ingin menyingkirkan sasaran pemberitahuan ini? - Apa benar Anda ingin menyingkirkan %1$s %2$s? - Pilih negara - Negara Mohon pilih negara Nomor telepon @@ -919,44 +777,32 @@ Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.Masukkan kode aktivasi Ada kesalahan ketika memvalidasi nomor telepon Anda Kode - - Flair Anda belum menjadi anggota komunitas manapun saat ini. - 3 hari 1 minggu 1 bulan Selamanya - Foto Ruang Nama Ruang Topik Tanda Ruang Ditandai sebagai: - Favorit Avatar pemberitahu Avatar penerima Demosi pengguna dengan id berikut - Tetap Panggil Terima - Error - Mohon telaah dan terima kebijakan homeserver ini: - Panggilan Gunakan nada dering semula ${app_name} untuk panggilan masuk Nada dering panggilan masuk Pilih nada dering untuk panggilan: - Panggilan Video Sedang Berlangsung… - Keluarkan Alasan - Versi %s Periksa Keadaan Pemberitahuan Hasil diagnosa pemeriksaan keadaan @@ -965,70 +811,58 @@ Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.Diagnosa dasar berlangsung lancar. Apabila Anda masih belum dapat menerima pemberitahuan, mohon kirim laporan bug untuk kami selidiki. Satu atau beberapa ujicoba gagal, coba sugesti yang kami tawarkan. Satu atau beberapa ujicoba gagal, mohon kirim laporan bug untuk kami selidiki. - Pengaturan Sistem. Pemberitahuan diperbolehkan dalam pengaturan sistem. - Pemberitahuan tidak diperbolehkan dalam pengaturan sistem. -Silakan periksa pengaturan sistem. + Notifikasi dinonaktifkan dalam pengaturan sistem. +\nMohon periksa pengaturan sistem anda. Buka Pengaturan - Pengaturan Akun. Pemberitahuan diperbolehkan dalam pengaturan akun Anda. - Pemberitahuan tidak diperbolehkan dalam pengaturan akun Anda. -Mohon periksa pengaturan akun. + Notifikasi dinonaktifkan dalam pengaturan akun anda. +\nMohon periksa pengaturan akun anda. Perbolehkan - Pengaturan Perangkat. Pemberitahuan diperbolehkan untuk perangkat ini. - Pemberitahuan tidak diperbolehkan untuk perangkat ini. -Mohon periksa pengaturan ${app_name}. + Notifikasi tidak diaktifkan pada sesi ini. +\nMohon periksa pengaturan ${app_name}. Perbolehkan - Pemeriksaan Layanan Google Play APK Layanan Google Play ditemukan dan telah diperbaharui. ${app_name} menggunakan Layanan Google Play untuk mendorong pesan tapi tampaknya tidak diatur sebagaimana harusnya. \n%1$s Perbaiki Layanan Google Play - Token Firebase - Sukses mengambil token FCM. -%1$s - Gagal mengambil token FCM. -%1$s - + Sukses mengambil token FCM: +\n%1$s + Gagal mengambil token FCM: +\n%1$s Pendaftaran Token Sukses mendaftarkan token FCM di HomeServer. - Gagal mendaftarkan token FCM di HomeServer. -%1$s - + Gagal mendaftarkan token FCM ke HomeServer: +\n%1$s Layanan Pemberitahuan Layanan Pemberitahuan sedang berjalan. - Layanan Pemberitahuan terhenti. -Coba nyalakan kembali aplikasi. + Layanan notifikasi tidak berjalan. +\nCoba buka ulang aplikasi ini. Mulai Layanan - Nyalakan Layanan Pemberitahuan dengan Otomatis Layanan terhenti dan dinyalakan kembali secara otomatis. Layanan tidak dapat dinyalakan kembali - Mulai ketika menyalakan perangkat Layanan akan dimulai ketika perangkat dinyalakan kembali. Layanan tidak akan mulai ketika perangkat dinyalakan kembali, Anda tidak akan menerima pemberitahuan hingga Anda membuka ${app_name}. Perbolehkan memulai ketika perangkat dinyalakan - Periksa halangan di balik layar - Halangan di balik layar dimatikan untuk ${app_name}. Ujicoba ini harus dijalankan menggunakan jaringan data (bukan WIFI). -%1$s - Halangan di balik layar dinyalakan untuk ${app_name}. -Aktivitas yang dilakukan aplikasi ini akan terhalang ketika beroperasi di balik layar, dan ini dapat mempengaruhi pemberitahuan. -%1$s + Larangan background dinonaktifkan untuk ${app_name}. Percobaan ini sebaiknya dijalankan menggunakan jaringan mobile data (bukan WIFI). +\n%1$s + Larangan background dinonaktifkan untuk ${app_name}. +\nAktivitas yang dilakukan aplikasi ini akan terhalang ketika beroperasi di balik layar, dan ini dapat mempengaruhi pemunculan notifikasi. +\n%1$s Matikan penghalang - Optimisasi Baterai ${app_name} tidak terpengaruh oleh Optimisasi Baterai. Cadangkan Kunci Gunakan Cadangan Kunci - Pencadangan kunci belum selesai, mohon tunggu… Pesan terenkripsi Anda akan hilang apabila Anda mencopot akun sekarang Pencadangan kunci sedang berlangsung. Pesan terenkripsi Anda akan hilang apabila Anda mencopot akun sekarang. @@ -1039,21 +873,17 @@ Aktivitas yang dilakukan aplikasi ini akan terhalang ketika beroperasi di balik Yakin\? Cadangkan Akses ke pesan terenkripsi akan hilang apabila Anda tidak mencadangkan kunci sebelum mencopot akun. - Lewatkan Selesai Hentikan - Anda yakin ingin mencopot akun\? Pengaturan Pemberitahuan Lanjutan Urgensi pemberitahuan lewat kejadian - Pengaturan Sesukanya. Perhatikan bahwa sebagian jenis pesan tersetel diam (mengeluarkan pemberitahuan tanpa suara). Sebagian pemberitahuan dimatikan dalam aturan Anda. Gagal memuat aturan, mohon coba lagi. Periksa Aturan - [%1$s] \nError ini di luar kendali ${app_name} dan menurut Google, error ini muncul ketika terlalu banyak aplikasi terdaftar dengan FCM pada perangkat tersebut. Error ini tidak seharusnya mempengaruhi pengguna biasa. [%1$s] @@ -1061,16 +891,12 @@ Aktivitas yang dilakukan aplikasi ini akan terhalang ketika beroperasi di balik [%1$s] \nError ini di luar kendali ${app_name}. Tidak terdapat akun Google pada perangkat. Mohon buka pengelola akun dan tambahkan akun Google. Tambah Akun - Apabila perangkat tidak sedang diisi atau dipergunakan dengan layar dimatikan, perangkat masuk mode Doze. Ini akan menghalangi aplikasi mengakses jaringan dan menunda tugas, sinkronisasi, dan alarm standar. Abaikan Optimisasi - Kelola Pemberitahuan Berisik Kelola Pemberitahuan Panggilan Kelola Pemberitahuan Diam Pilih warna LED, getaran, suara… - - Pengelolaan Kunci Kriptografi Pratinjau tautan dalam obrolan apabila homeserver mendukung fitur ini. Kirim pemberitahuan mengetik @@ -1082,4 +908,4 @@ Aktivitas yang dilakukan aplikasi ini akan terhalang ketika beroperasi di balik Tunjukkan kejadian bergabung dan meninggalkan Undangan, pengeluaran, dan larangan tidak terpengaruh. Tunjukkan kejadian akun - + \ No newline at end of file diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 39ca954053..a60946c593 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -2668,4 +2668,70 @@ Riprendi Non autorizzato, credenziali di autenticazione valide mancanti Torna + Vuoi veramente eliminare tutti i messaggi non inviati in questa stanza\? + Elimina i messaggi non inviati + Invio dei messaggi fallito + Vuoi annullare l\'invio del messaggio\? + Elimina tutti i messaggi falliti + Fallito + Inviato + Invio in corso + Contenuto dell\'evento + Evento di stato inviato! + Evento inviato! + Evento malformato + Tipo di messaggio mancante + Nessun contenuto + Contenuto dell\'evento + Chiave dello stato + Tipo + Invia evento di stato personalizzato + Modifica contenuto + Eventi di stato + Invia evento di stato + Invia evento personalizzato + Esplora stato stanza + Strumenti Svil + Vedi le conferme di lettura + Non notificare + Notifica senza suono + Notifica con suono + Messaggio non inviato per un errore + Selezionato + Chiudi selettore emoji + Apri selettore emoji + Livello di fiducia di affidabilità + Livello di fiducia di allerta + Livello di fiducia predefinito + Selezionato + Video + Questa stanza ha una bozza non inviata + Alcuni messaggi non sono stati inviati + Elimina avatar + Cambia avatar + Immagine + Importa chiave da file + Apri i widget + Schermata + + %d elemento + %d elementi + + Il limite è sconosciuto. + Il tuo homeserver accetta allegati (file, multimedia, ecc.) di una dimensione fino a %s. + Limite di invio file al server + Versione server + Nome server + Impostazioni stanza + Abbandonare la conferenza attuale e passare all\'altra\? + Versione stanza + Mostra tutte le stanze nell\'elenco, incluse quelle con contenuti espliciti. + Mostra stanze con contenuti espliciti + Elenco delle stanze + Nuovo valore + Cambia + Sinc. iniziale: +\nScaricamento dati… + Sinc. iniziale: +\nIn attesa di risposta dal server… \ No newline at end of file diff --git a/vector/src/main/res/values-kab/strings.xml b/vector/src/main/res/values-kab/strings.xml index 516ccc1caf..53b4ee9214 100644 --- a/vector/src/main/res/values-kab/strings.xml +++ b/vector/src/main/res/values-kab/strings.xml @@ -3,8 +3,8 @@ %1$s: %2$s %1$s t.yuzen tugna. Tuzneḍ tugna. - Tinubga n %s - Tinubga-k•m + Tinnubga n %s + Tinnubga-k•m %1$s yesnulfa-d taxxamt Tesnulfaḍ-d taxxamt-a %1$s inced-d %2$s @@ -2406,4 +2406,74 @@ Sken %d ibenkani swayes i tzemreḍ ad tesneqdeḍ tura Rnu adiwenni usrid amaynut s usulay n Matrix + Sken anasiw n yimujiten + Beddel asentel + Azen ineḍruyen m.room.server_acl + Beddel tisirag + Beddel isem n texxamt + Rmed awgelhen n texxamt + Beddel tansa tagejdant n texxamt + Beddel avaṭar n texxamt + Snifel iwiǧiten + Kkes iznan n wiyaḍ + Gdel iseqdacen + Snifel iɣewwaren + Nced iseqdacen + Azen iznan + Tisirag + Tisirag n texxamt + Taxxamt-a mačči d tazayezt. Ur tzemmreḍ ara ad tedduḍ ɣur-d war tinnubga. + Ṭṭef + Akaram n texxamin + Azal amaynut + Uɣal + Beddel + Am unagraw + Tugiḍ i yinebgawen ad d-asen ɣer da. + %1$s yugi i yinebgawen ad d-asen ɣer da. + Tessirgeḍ inebgawen ad d-asen ɣer da. + %1$s yessireg inebgawen ad d-asen ɣer da. + Tbeddleḍ tansiwin n texxamt-a. + %1$s ibeddel tansiwin n texxamt-a. + Teffɣeḍ. Tamentilt: %1$s + %1$s yeffeɣ. Tamentilt: %2$s + %1$s yedda. Tamentilt: %2$s + Teddiḍ. Tamentilt: %1$s + Amtawi n tazwara: +\nAsader n yisefka… + Amtawi n tazwara: +\nAraju n tririt n uqeddac… + Taxxamt tilemt (tella %s) + + %1$s, %2$s, %3$s d %4$d-nniḍen + %1$s, %2$s, %3$s d %4$d n wiyaḍ + + %1$s, %2$s, %3$s d %4$s + %1$s, %2$s d %3$s + Tbeddleḍ asarag s tvidyut + Asarag s tvidyut yettwabeddel sɣur %1$s + Tesḥebseḍ asarag s tvidyut + %1$s yesseḥbes asarag s tvidyut + Tebdiḍ asarag s tvidyut + Asarag s tvidyut yebda-d sɣur %1$s + Tugiḍ tinnubga n %1$s + %1$s yugi tinnubga n %2$s + Tnecdeḍ-d %1$s + %1$s t•yenced-d %2$s + 🎉 Iqeddcen akk ttwagedlen seg uttekki! Taxxamt-a dayen ur tettuseqdac ara. + Ulac abeddel. + • Iqeddacen i yemsaḍan d %s ttwakksen seg tebdert n usireg. + • Iqeddacen i yemsaḍan d %s ttusirgen tura. + • Iqeddacen i yemsaḍan d %s ttwakksen seg tebdert n ugdal. + • Iqeddacen i yemsaḍan d %s ttwagedlen tura. + • Iqeddacen i yemsaḍan d %s ttusirgen. + • Iqeddacen i yemsaḍan d %s ttwagedlen. + Terriḍ iznan i d-itteddun ad d-ttbinen i %1$s + %1$s t•yerra iznan i d-itteddun ad d-ttbinen i %2$s + Teffɣeḍ seg texxamt + %1$s t•yeffeɣ seg texxamt + Teddiḍ-d ɣer texxamt + %1$s t•yedda-d ɣer texxamt + Tesnulfaḍ-d adiwenni + %1$s t•yesnulfa-d adiwenni \ No newline at end of file diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml index b5226f43ce..ee98765a7c 100644 --- a/vector/src/main/res/values-lv/strings.xml +++ b/vector/src/main/res/values-lv/strings.xml @@ -246,13 +246,13 @@ Dalībnieka informācija Bijušie Lai notiek - Tomēr nē + Atcelt Saglabāt Pamest Nosūtīt Sūtīt atkārtoti Rediģēt - Citāts + Citēt Dalīties Vēlāk Pārsūtīt @@ -293,10 +293,10 @@ Izlase Cilvēki Istabas - Meklēt istabas - Meklēt favorītus - Meklēt cilvēkus - Meklēt istabas + Filtrēt istabu nosaukumus + Filtrēt izlasi + Filtrēt cilvēkus + Filtrēt istabu nosaukumus Uzaicinājumi Zema prioritāte Sarunas @@ -365,7 +365,7 @@ Ielādējas… Izslēgt Kopienas - Meklēt kopienas + Filtrēt kopienu nosaukumus Uzaicināt Kopienas Nav grupu @@ -378,8 +378,8 @@ Epasta adrese (izvēles) Tālruņa numurs Tālruņa numurs (izvēles) - Parole (atkārtoti) - Apstiprini savu jauno paroli + Parole atkārtoti + Apstipriniet savu jauno paroli Nepareizs lietotājvārds un/vai parole Lietotājvārdi var tikai saturēt burtus, ciparus, punktus, domuzīmes un apakšsvītras Parole par īsu (< 6 simboliem) @@ -388,17 +388,17 @@ Šķiet ievadīts nekorekts tālruņa numurs Šī epasta adrese jau tiek izmantota. Iztrūkst epasta adreses - Iztrūkst tālruņa # - Iztrūkst epasta adrese vai tālruņa # + Iztrūkst tālruņa numurs + Iztrūkst epasta adrese vai tālruņa numurs Nederīgs tokens Paroles nesakrīt - Aizmirsi paroli? + Aizmirsāt paroli\? Izmantot servera īpašus parametrus Pārbaudi epastu, lai turpinātu reģistrāciju Reģistrēšanās ar epastu un tālruņa numuru vienlaicīgi pagaidām netiek atbalstīta. Ar kontu būs saistīts vienīgi tālruņa numuru. \n \nSavu epastu varat pievienot profilam iestatījumos. - Bāzes serveris vēlas pārbaudīt, vai neesi robots + Bāzes serveris vēlas pārbaudīt, vai neesat robots Šāds lietotājvārds jau ir aizņemts Bāzes serveris: Identitāšu serveris: @@ -406,8 +406,8 @@ Lai atiestatītu paroli, ievadiet epasta adresi, kura piesaistīta kontam: Ir jābūt ievadītai kontam piesaistītajai epasta adresei. Jāievada jauna parole. - Epasts ir nosūtīts uz %s. Pēc tam, kad nospiedīsi uz tajā ietverto tīmekļa saiti, noklikšķini zemāk. - Neizdevās verificēt epasta adresi: pārbaudi vai esi noklikšķinājis(usi) uz saiti atsūtītajā epastā + Epasts ir nosūtīts uz %s. Kad nospiedīsiet uz tajā ietverto tīmekļa saiti, noklikšķiniet zemāk. + Neizdevās verificēt epasta adresi: pārbaudiet, vai esi noklikšķinājis(usi) uz saiti atsūtītajā epastā Jūsu parole ir atstatīta. \n \nJūs esat izrakstīts no visām sesijām un nesaņemsit push paziņojumus. Lai atkārtoti iespējotu paziņojumus, pierakstieties katrā savā ierīcē par jaunu. @@ -567,7 +567,7 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Iemesls ziņojumam par šo saturu Vai vēlaties paslēpt visas ziņas no šī lietotāja\? \n -\nŅemiet vērā, ka ar šo darbību tiks restartēta lietotne, un tas var aizņemt kādu laiku. +\nŅemiet vērā, ka ar šo darbību tiks pārstartēta lietotne, un tas var aizņemt kādu laiku. Atcelt augšupielādi Atcelt lejupielādi Meklēšana @@ -587,9 +587,9 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Izveidot istabu Ieiet istabā Ieiet istabā - Ievadiet istabas ID vai aliasi - Skatīt katalogu - Meklēju katalogā… + Ievadiet istabas ID vai aliasu + Pārlūkot katalogu + Meklē katalogā… Visas ziņas (ar skaņu) Visas ziņas Tikai pieminējumi @@ -637,14 +637,14 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Trešo pušu paziņojumi Autortiesības Privātuma politika - Iztīrīt ķešu - Iztīrīt mēdija ķešu + Iztīrīt kešatmiņu + Iztīrīt mediju kešatmiņu Turēt mēdiju (?) Lietotāja iestatījumi Paziņojumi Ignorētie dalībnieki Citi - Papildus + Papildu Kriptogrāfija Paziņojumus nosūtīt uz Lokālie kontakti @@ -653,7 +653,7 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Galvenais ekrāns Piestiprināt istabas ar garām palaistiem paziņojumiem Piestiprināt istabas ar neizlasītām ziņām - Ierīces + Sesijas Iespējot URL priekšskatu pēc noklusējuma Vienmēr rādīt ziņu laiku Rādīt ziņu laiku 12 stundu formātā (piem. 12:12pm) @@ -691,18 +691,18 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Parole nomainīta Rādīt visas ziņas no %s\? \n -\nŅemiet vērā, ka ar šo darbību tiks restartēta lietotne, un tas var aizņemt kādu laiku. +\nŅemiet vērā, ka ar šo darbību tiks pārstartēta lietotne, un tas var aizņemt kādu laiku. Vai tiešām vēlies turpmāk paziņojumus uz šo ierīci nesūtīt? Vai tiešām vēlies dzēst %1$s %2$s? Izvēlies valsti Valsts Lūdzu izvēlies valsti - Tālruņa # - Priekš šīs valsts nekorekts tālruņa # + Tālruņa numurs + Priekš šīs valsts nekorekts tālruņa numurs Tālruņa verifikācija Tika nosūtīts SMS ar aktivācijas kodu. Lūdzu ievadi šo kodu zemāk. Ievadi aktivācijas kodu - Tālruņa # validācija nesekmīga + Klūda tālruņa numura validācijā Kods Gaidas 3 dienas @@ -721,7 +721,7 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Iekļaut šo istabu katalogā Paziņojumi Piekļuve istabai - Piekļuve Istabas vēsturei + Piekļuve istabas vēsturei Kas var lasīt vēsturi? Kas var piekļūt šai istabai? Jebkurš @@ -730,10 +730,10 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Tikai dalībnieki (kopš tie pievienojušies) Lai ģenerētu saiti uz istabu, ir jābūt dotai adresei. Tikai uzaicinātie - Visi, kuri zin saiti uz istabu, izņemot viesus - Visi, kuri zin saiti uz istabu, ieskatot viesus + Visi, kuri zina saiti uz istabu, izņemot viesus + Visi, kuri zina saiti uz istabu, ieskatot viesus Lietotāji, kuriem liegta pieeja - Papildus + Papildu Šīs istabas iekšējais ID Adreses Izmēģinājumu lauciņš @@ -818,23 +818,23 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē Eksportēt istabas atslēgas Eksportēt atslēgas vietējā failā Eksportēt - Ievadi paroli (paroles frāzi) - Apstiprināt paroli + Ievadiet frāzveida paroli + Apstiprināt frāzveida paroli Istabas šifrēšanas atslēgas tika salabātas \'%s\'. \n \nBrīdinājums: fails var tikt izdzēsts, ja lietotne tiek atinstalēta. Importēt E2E istabas atslēgas Importēt istabas atslēgas Importēt atslēgas no vietējā faila - Imports + Importēt Šifrēt vienīgi uz pārbaudītām ierīcēm Nekad nesūtīt šifrētas ziņas uz nepārbaudītām ierīcēm no šīs ierīces. Neverificēta Verificēta Melnajā sarakstā - nepazīstama ierīce + nepazīstama sesija nekā - Apstiprināt + Verificēt Apstiprinājumu atcelt Ietvert melnajā sarakstā Izņemt no melnā saraksta @@ -872,7 +872,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %1$s iekš %2$s Meklēt vēsturē - Šrifta izmērs + Burtu izmērs Sīks Mazs Normāls @@ -959,7 +959,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verificējiet visas savas sesijas, lai nodrošinātos, ka jūsu konts un ziņas ir drošībā Pārskatiet savas pierakstīšanās Nešifrēts - vai kāda cita Matrix lietotne ar cross-signing atbalstu + vai kādu citu Matrix lietotni ar cross-signing atbalstu Šis konts ir deaktivizēts. Šifrētas ziņas grupas čatos Šifrētas ziņas viens-pret-vienu čatos @@ -968,17 +968,17 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Ziņas šajā istabā ir nodrošinātas ar pilnīgu šifrēšanu. Šifrēšana iespējota Jauna pierakstīšanās. Vai tas bijāt jūs\? - Vai tiešām vēlies dzēst šo notikumu\? Ņem vērā, ka istabas nosaukuma vai tēmas nosaukuma maiņa var ietekmēt (atsaukt) izmaiņas. + Vai tiešām vēlaties dzēst šo notikumu\? Ņemiet vērā, ka istabas nosaukuma vai temata maiņa var atcelt izmaiņas. Apstipriniet dzēšanu - QA kods + QR kods Neuzticama Uzticama Sesijas Neizdevās iegūt sesijas Brīdinājums - Pārbaudīta + Verificēta Apstiprināt Verificējiet šo sesiju Jūsu servera administrators privātajās telpās un tiešajās ziņās pēc noklusējuma ir atspējojis pilnīgu šifrēšanu. @@ -1005,10 +1005,10 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Drošība Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. \n -\nJūsu ziņas tiek nodrošināti ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. +\nJūsu ziņas tiek nodrošinātas ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. \n -\nJūsu ziņas tiek nodrošināti ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. +\nJūsu ziņas tiek nodrošinātas ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. Ziņas šeit nav nodrošinātas ar pilnīgu šifrēšanu. Ziņām šajā istabā netiek piemērota pilnīga šifrēšana. Jūs akceptējāt @@ -1028,7 +1028,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Parole Pierakstīties Pierakstīties - Matrix Id + Matrix ID Brīdinājums Šāds lietotājvārds jau ir aizņemts Tālāk @@ -1054,16 +1054,16 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Jūsu parole ir atiestatīta. Esmu verificējis(usi) savu epasta adresi Turpināt - Mainot paroli, tiks atiestatītas visas pilnīgas šifrēšanas atslēgas visās jūsu sesijās, padarot šifrēto tērzēšanas vēsturi neizlasāmu. Pirms paroles atiestatīšanas iestatiet atslēgu dublēšanu vai eksportējiet istabas atslēgas no citas sesijas. + Mainot paroli, tiks atiestatītas visas pilnīgas šifrēšanas atslēgas visās jūsu sesijās, padarot šifrētās sarakstes vēsturi neizlasāmu. Pirms paroles atiestatīšanas iestatiet atslēgu dublēšanu vai eksportējiet istabas atslēgas no citas sesijas. Uzmanību! Jauna parole Epasts Tālāk - Apstiprinājuma vēstule tiks nosūtīta uz tavu epasta adresi, lai apstiprinātu paroles nomaiņu. + Apstiprinājuma vēstule tiks nosūtīta uz jūsu epasta adresi, lai apstiprinātu paroles nomaiņu. Pierakstīties Reģistrēties Turpināt - Citi + Cits Iestatījumi Izslēgt skaņu Tikai pieminējumi @@ -1111,7 +1111,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verificēts! Algoritms Versija - Droša reze + Droša rezerves kopija Vai tiešām to vēlaties\? Dalīties Gatavs @@ -1122,7 +1122,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Deaktivizēt kontu Nomaina jūsu parādāmo vārdu Padzen lietotāju ar norādīto id - Atstāt istabu + Atstāj istabu Uzaicina lietotāju ar norādīto id uz pašreizējo istabu Atceļ operatora statusu lietotājam ar norādīto Id Definē lietotāja statusu @@ -1149,7 +1149,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Deaktivizēt kontu Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem, dublējot šifrēšanas atslēgas savā serverī. Iestatīt drošu rezerves dublēšanu - Droša reze + Droša rezerves kopija Sūtīt paziņojumus par rakstīšanu Normāls Startēt pie ierīces ielādes @@ -1174,7 +1174,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Mainīt istabas avataru Apziņot visus Dzēst citu sūtītas ziņas - Pieejas liegumi lietotājiem + Liegt pieeju lietotājiem Padzīt lietotājus Mainīt iestatījumus Uzaicināt lietotājus @@ -1244,9 +1244,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verificēt ierīci Sistēmas noklusējuma Pielāgots (%1$d) iekš %2$s - Noklusējums iekš %1$s - Moderators iekš %1$s - Administrators iekš %1$s + %1$s noklusējums + %1$s moderators + %1$s administrators Pielāgots Moderators Administrators @@ -1261,4 +1261,516 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %d sekundes %d sekundes + Šī sesija ir uzticama drošai ziņojumapmaiņai, jo %1$s (%2$s) to verificēja: + Verificējiet šo sesiju, lai atzīmētu to kā uzticamu un piešķirtu piekļuvi šifrētām ziņām. Ja neesat pierakstījies šajā sesijā, jūsu konts var būt kompromitēts: + Šī sesija ir uzticama drošai ziņojumapmaiņai, jo jūs to verificējāt: + Izrakstīties no šīs sesijas + Pārvaldīt sesijas + Parādīt visas sesijas + Aktīvās sesijas + Salīdziniet kodu ar to, kas parādīts otra lietotāja ekrānā. + Salīdziniet unikālās emocijzīmes, pārliecinoties tās ir vienādā secībā. + Lai būtu droši, dariet to klātienē vai izmantojiet citu komunikācijas veidu. + Lai būtu droši, verificējiet %s ar vienreizēja koda palīdzību. + Citas istabas + Nesenās istabas + Verifikācijas slēdziens + Bota pogas + Aptauja + Uzlīme + Kāds no uzskaitītajiem var būt kompromitēts: +\n +\n - jūsu bāzes serveris +\n - bāzes serveris, kuram pieslēdzies verificējamais lietotājs +\n - jūsu vai otra lietotāja interneta pieslēgums +\n - jūsu vai otra lietotāja ierīce + Nav droša + Meklējiet zaļo vairogu, lai pārliecinātos, ka lietotājs ir uzticams. Visiem lietotājiem istabā jābūt uzticamiem, lai nodrošinātu, ka istaba ir droša. + Tie nesakrīt + Tie sakrīt + Neuzticama pieteikšanās + Izveido istabu… + Dažas rakstzīmes nav atļautas + Rāda tikai pirmos rezultātus, ierakstiet vairāk burtus… + Citas sesijas + Pašreizējā sesija + Papildu iestatījumi + Apskatīt visas manas sesijas + Jūsu matrix.to saite ir nepareizi veidota + Dzēst datus + Dzēst datus + Dzēst visus datus + Vai + Nelasītas ziņas + Jūs padarījāt šo pieejamu tikai ar ielūgumiem. + %1$s padarīja šo pieejamu tikai ar ielūgumiem. + Jūs padarījāt istabu pieejamu tikai ar ielūgumiem. + %1$s padarīja istabu pieejamu tikai ar ielūgumiem. + Jūs padarījāt istabu publiski pieejamu visiem, kas zina saiti. + %1$s padarīja istabu publiski pieejamu visiem, kas zina saiti. + Jūs neignorējat nevienu lietotāju + Ignorēt lietotāju + Šobrīd nav tīkla savienojuma + ZIŅOT + Ziņot par šo saturu + Pielāgots ziņojums… + Nepiemērots saturs + Tas ir spams + Šajā istabā nav neviena faila + %1$d no %2$d + %s izlasīja + %1$s un %2$s izlasīja + %1$s, %2$s un %3$s izlasīja + Sūtīt pielikumu + Piekrītiet identitāšu servera (%s) pakalpojumu sniegšanas noteikumiem, lai padarītu sevi atrodamu citiem, izmantojot epasta adresi vai tālruņa numuru. + Verifikācijas kods nav pareizs. + Teksta ziņojums ir nosūtīts uz %s. Lūdzu, verifikācijas kodu no ziņojuma. + Neizdevās pieslēgties identitāšu serverim + Konfigurēt identitāšu serveri + Atvienot identitāšu serveri + Identitāšu serveris + Pārskatīt noteikumus + Pievienojas istabai… + Ieteikumi + Kontakti + Nesenie + Filtrēt pēc nosaukuma vai ID… + Sāciet rakstīt, lai parādītos rezultāti + Nekas nav atrasts, lietojiet Pievienot ar Matrix ID, lai meklētu serverī. + Izveido istabu… + Pievienot ar QR kodu + Pievienot ar Matrix ID + Saite nokopēta starpliktuvē + (rediģēts) + Fails %1$s ir lejupielādēts! + Lejupielādē failu %1$s… + Sūta failu (%1$s / %2$s) + Šifrē failu… + Sūta sīktēlu (%1$s / %2$s) + Šifrē sīktēlu… + Balss un video + Eksperts + Preferences + Jūs jau skataties šo istabu! + Citi trešo pušu paziņojumi + Matrix SDK versija + Šīs istabas priekšskatījums nav pieejams. Vai vēlaties tai pievienoties\? + Šī istaba šobrīd nav pieejama. +\nMēģiniet vēlreiz vēlāk vai lūdziet istabas administratoru pārbaudīt, vai jums ir piekļuve. + Lūdzu, gaidiet… + Mainīt + Pēdējo reiz rediģēja %1$s %2$s + Jums vairs nav nelasītu ziņu + Nosūtīja jums uzaicinājumu + Verifikācijas process beidzās dēļ noilguma + Lietotājs atcēla verifikāciju + %s vēlas verificēt jūsu sesiju + Verifikācijas pieprasījums + Verifikācija atcelta. +\nIemesls: %s + Otra puse atcēla verifikāciju. +\n%s + Pieprasījums atcelts + Jūs veiksmīgi verificējāt šo sesiju. + Gaida, kamēr partneris apstiprinās… + Apskatīt pieprasījumu + Jūs saņēmāt ienākošu verifikācijas pieprasījumu. + Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem + Tas biju es + Neparedzēta kļūda + Izveidot frāzveida paroli + %d+ + +%d + %1$s: %2$s + %1$s: + Tikai par kļūdām + Par ziņām un kļūdām + Vienmēr + Atvainotie, notikusi kļūda + Nospiediet šeit, lai redzētu vecākas ziņas + Šī istabas ir citas sarakstes turpinājums + Sarakste turpinās šeit + Šī istaba ir aizvietota un vairs nav aktīva + Pārskatīt tagad + Lai turpinātu izmantot %1$s bāzes serveri, jums ir jāpārskata un jāpiekrīt noteikumiem un nosacījumiem. + Kluss + Neverificēta sesija pieprasa šifrēšanas atslēgas. +\nSesijas nosaukums: %1$s +\nRedzēta pēdējo reizi: %2$s +\nJa neesat pierakstījies citā sesijā, ignorējiet šo pieprasījumu. + Jauna sesija pieprasa šifrēšanas atslēgas. +\nSesijas nosaukums: %1$s +\nRedzēta pēdējo reizi: %2$s +\nJa neesat pierakstījies citā sesijā, ignorējiet šo pieprasījumu. + Lai turpinātu, jums ir jāpieņem šī pakalpojuma noteikumi. + Sūtit balss ziņas + Pārvaldīt integrācijas + Parametrs nav derīgs. + Iztrūks obligāts parametrs. + %1$s: %2$s %3$s + %1$s: %2$s + ** Neizdevās nosūtīt - atveriet istabu + Es + Jauns uzaicinājums + Jaunas ziņas + Jauns notikums + %1$s un %2$s + %1$s iekš %2$s un %3$s + Rakstiet te… + Atslēgas ir veiksmīgi eksportētas + Lūdzu, izveidojiet frāzveida paroli eksportēto atslēgu šifrēšanai. Jums būs jaievada to pašu frāzveida paroli, lai varētu importēt atslēgas. + Istabas versija + Pievienot lokālo adresi + Iestatiet šis istabas adreses, lai lietotāji var atrast šo istabu uz jūsu bāzes servera (%1$s) + Dzēst adresi \"%1$s\"\? + Galvenā adrese + Skatiet un pārvaldiet šīs istabas adreses un tās redzamību istabu katalogā. + Istabas adreses + Atskaņot aizvara skaņu + Izvēlēties + Izvēlēties + Noklusējuma kompresija + Papildinformācija: %s + Verificējot jūsu tālruņa numuru, radās kļūda. + Verificējot jūsu epasta adresi, radās kļūda. + Lai to izdarītu, iespējojiet ‘Atļaut integrācijas’ iestatījumos. + Datu plāna taupīšanas režīms izmanto filtru, lai klātbūtnes atjauninājumi un rakstīšanas paziņojumi tiek izfiltrēti. + Jā, es vēlos palīdzēt! + Priekšskatīt saites sarakstē, kad jūsu bāzes serveris atbalsta šo iespēju. + Integrācijas + Neizdevās atjaunināt iestatījumus. + Atvērt iestatījumus + Atjaunināt istabu + Sūtīt m.room.server_acl notikumus + Jums nav atļaujas atjaunināt lomas, kas nepieciešamas, lai mainītu dažādas istabas daļas + Skatiet un atjauniniet lomas, kas nepieciešamas, lai mainītu dažādas istabas daļas. + Atceļot pieejas liegumu, lietotājam atkal būs iespēja pievienoties istabai. + Atcelt pieejas liegumu lietotājam + Pieejas lieguma iemesls + Liegt pieeju lietotājam + lietotāja padzīšanas gadījumā tas tiks dzēsts no šis istabas. +\n +\nLai novērstu atkārtotu pievienošanos, tā vietā jums vajadzētu liegt pieeju. + Padzīšanas iemesls + Padzīt lietotāju + Vai tiešām vēlaties atcelt uzaicinājumu šim lietotājam\? + Atceļot ši lietotāja ignorēšanu, visas lietotāja ziņas atkal būs redzamas. + Ignorējot šo lietotāju noņems viņa ziņas istabās, kuras ir jums kopīgas. +\n +\nJūs varat atcelt šo darbību jebkurā brīdī vispārīgajos iestatījumos. + Pazemināt + Pazemināt sevi\? + Zvani + Pieprasījums nosūtīts + Atkārtoti pieprasīt šifrēšanas atslēgas no citām jūsu sesijām. + Šis tālruņa numurs jau ir definēts. + Sūtīt uzlīmi + Bezvadu austiņas + Austiņas + Skaļrunis + Istabu katalogs + Identitāšu serveris nav sakonfigurēts. + Jaunā vērtība + Atgriezties + Pārslēgt + Jūs nevarat uzsākt zvanu ar sevi, pagaidiet, kamēr dalībnieki akceptēs uzaicinājumu + Jūs nevarat uzsākt zvanu ar sevi + Trešo pušu licences + Sūtīt uzlīmi + Sakarā ar pilnīgu šifrēšanu, jums var būt nepieciešams sagaidīt ziņu no kāda, jo šifrēšanas atslēgas netika pareizi nosūtītas jums. + Gaida šo ziņu, tas var aizņemt ilgāku laiku + Jums nav piekļuves šai ziņai + + Parādīt ierīci, ar kuru jūs šobrīd varat veikt verifikāciju + Parādīt %d ierīces, ar kurām jūs šobrīd varat veikt verifikāciju + Parādīt %d ierīces, ar kurām jūs šobrīd varat veikt verifikāciju + + Jums būs jāatsāk bez vēstures, ziņām, uzticamām ierīcēm un uzticamiem lietotājiem + Ja jūs atiestatīsiet visu + Veiciet atiestatīšanu tikai tad, ja jums vairs nav nevienas citas ierīces, ar kuru verificēt šo ierīci. + Pilna atiestatīšana + Aizmirsāt vai pazaudējāt visas atkopšanās iespējas\? Atiestatiet visu + Izmantojiet savu %1$s vai savu %2$s, lai turpinātu. + Izmantojiet jaunāko ${app_name} citās savās ierīcēs: + Izmantojiet jaunāko ${app_name} citās savās ierīcēs, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} Android, vai kādu citu Matrix lietotni ar cross-signing atbalstu + Iestatiet jaunu konta paroli… + Šis ir sākums jūsu tiešās sarakstes vēsturei ar %s. + Šis ir šīs sarakstes pats sākums. + Šis ir pats %s sākums. + Jūs pievienojāties. + %s pievienojās. + Jūs izveidojāt un sakonfigurējāt istabu. + %s izveidoja un sakonfigurēja istabu. + Šajā istabā izmantotā šifrēšana netiek atbalstīta + Ziņas šajā istabā ir aizsargātas ar pilnīgu šifrēšanu. Uzziniet vairāk un verificējiet lietotājus viņu profilā. + Verifikācija atcelta + Jūsu konts varētu būt kompromitēts + Tas nebiju es + Izmantojiet šo sesiju jaunās sesijas verifikācijai, tādējādi dodot piekļuvi šifrētām ziņām. + Pieskarieties, lai pārskatītu un verificētu + Rediģēšanas iemesls + Iekļaut iemeslu + Dzēst… + Ja jūs nevarat piekļūt esošai sesijai + Izmantot atkopšanās frāzveida paroli vai atslēgu + Izmantojiet citu sesiju šis sesijas verifikācijai, tādējādi dodot piekļuvi šifrētām ziņām. + Citi lietotāji var tai neuzticēties + Iespējot šifrēšanu + Pēc iespējošanas šifrēšana istabai nevar tikt izslēgta. Šifrētā istabā sūtītas ziņas nav redzamas serverim, tikai istabas dalībniekiem. Šifrēšanas iespējošana var neļaut pareizi strādāt daudzus botiem un tiltiem. + Jums nav atļaujas, lai iespējotu šifrēšanu šajā istabā. + + Viena persona + %1$d cilvēki + %1$d cilvēki + + Papildu drošībai, verificējiet %s, pārbaudot vienreizēju kodu uz jūsu abu ierīcēm. +\n +\nMaksimālai drošībai, dariet to klātienē. + Gaida %s… + Verificēts %s + Verificē %s + QR koda attēls + Ja nevarat noskenēt kodu, veiciet verifikāciju, izmantojot unikālu emocijzīmju salīdzināšanu. + Verificēt ar emocijzīmēm + Verifikācija, izmantojot emocijzīmju salīdzināšanu + Ja jūs neatrodaties klātienē, salīdziniet emocijzīmes + Nevar skenēt + Skenēt viņu kodu + Skenējiet kodu ar otra lietotāja ierīci, lai droši verificētu viens otru + Jūs + Verificēt manuāli + Verificējiet šo sesiju + Verifikācijas pieprasījums + Verifikācija nosūtīta + %s akceptēja + Jūs atcēlāt + %s atcēla + Gaida… + Reaģēja ar %s + Bloķēt pievienošanos šai istabai ikvienam, kas nav daļa no %s + Iespējot šifrēšanu + Sākotnējā sinhronizācija… + Jūs izrakstījāties + Pierakstīties vēlreiz + Jūs izrakstījāties + Neizdevās atrast derīgu bāzes serveri. Lūdzu, pārbaudiet savu identifikatoru + Šis nav pareizs identifikators. Sagaidāmais formāts: \'@user:homeserver.org\' + Ja jūs nezināt savu paroli, dodieties atpakaļ, lai to atiestatītu. + Ja izveidojat kontu bāzes serverī, izmantojiet savu Matrix ID (paraugs: @user:domain.com) un paroli zemāk. + Pierakstīties ar Matrix ID + Pierakstīties ar Matrix ID + Alternatīvi, ja jums jau ir konts, un jūs zināt savu Matrix identifikatoru un paroli, varat izmantot šo metodi: + Uz šī bāzes servera darbojas pārāk veca versija. Aiciniet sava bāzes servera administratoru veikt atjauninājumus. Jūs varat turpināt, tomēr atsevišķas iespējas var nedarboties pareizi. + Uz šī bāzes servera darbojas pārāk veca versija, lai izveidotu savienojumu ar to. Aiciniet sava bāzes servera administratoru veikt atjaunināšanu. + Novecojis bāzes serveris + Ievadītais kods nav pareizs. Lūdzu, pārbaudiet. + Mēs tikko nosūtījām epastu uz %1$s. +\nLūdzu, noklikšķiniet uz saites epastā, lai turpinātu konta izveidi. + Lūdzu, pārbaudiet savu epastu + Pieņemt noteikumus, lai turpinātu + Lūdzu, veiciet CAPTCHA izaicinājumu + Izvēlēties pielāgotu bāzes serveri + Izvēlēties Element Matrix Services + Izvēlēties matrix.org + Jūsu konts vēl nav izveidots.. +\n +\nVai pārtraukt reģistrēšanos\? + Lūdzu, izmantojiet starptautisko formātu. + Iestatiet tālruņa numuru,lai pēc izvēles ļaut jums zināmiem cilvēkiem atrast. + Iestatiet tālruņa numuru + Tālāk + Epasts (izvēles) + Iestatiet epastu, lai atgūtu savu kontu. Vēlāk jūs varat pēc izvēles ļaut jums zināmiem cilvēkiem atrast sevi pēc epasta adreses. + Iestatīt epasta adresi + Jūsu parole vēl nav nomainīta. +\n +\nVai pārtraukt paroles nomaiņu\? + Gatavs! + Nospiediet saiti, lai apstiprinātu savu jauno paroli. Kad esat sekojis saitei, noklikšķiniet zemāk. + Apstiprinājuma epasts tika nosūtīts uz %1$s. + Pārbaudiet ienākošos epastus + Šis epasts nav piesaistīts nevienam kontam + Atiestatīt paroli uz %1$s + Šis epasts nav piesaistīts nevienam kontam. + Lietotnei neizdodas izveidot kontu uz šī bāzes servera. +\n +\nVai vēlaties reģistrēties, izmantojot tīmekļa klientu\? + Atvainojiet, šis serveris nepieņem jaunus kontus. + Lietotnei neizdodas pierakstīties uz šī bāzes servera. Bāzes serveris atbalsta sekojošos pierakstīšanās veidus: %1$s. +\n +\nVai vēlaties pierakstīties, izmantojot tīmekļa klientu\? + Radās kļūda, ielādējot lapu: %1$s (%2$d) + Ievadiet servera adresi, kuru vēlaties izmantot + Ievadiet Modular Element vai servera adresi, kuru vēlaties izmantot + Premium hostings organizācijām + Adrese + Element Matrix Services adrese + Attīrīt vēsturi + Turpināt, izmantojot SSO + Pierakstīties uz %1$s + Pieslēgšanās pielāgotam serverim + Pieslēgšanās Element Matrix Services + Pieslēgšanās %1$s + vienotā pierakstīšanās + Pierakstīties ar %s + Reģistrēties ar %s + Turpināt ar %s + Pielāgoti un papildu iestatījumi + Uzzināt vairāk + Premium hostings organizācijām + Pievienojieties bez maksas miljoniem lietotāju lielākajā publiskajā serverī + Tāpat kā ar epastu, kontiem ir sava mājvieta, lai gan jūs varat sazināties ar jebkuru citu + Izvēlieties serveri + Uzsākt + Paplašiniet un pielāgojiet savam ērtumam + Paturiet saraksti privātu ar šifrēšanas palīdzību + Sarakstieties ar cilvēkiem pa tiešo vai grupās + Tā ir jūsu sarakste. Tā pieder jums. + Paturiet ilgāk uz istabas, lai redzētu vairāk iespēju + Jūs neveicāt nekādas izmaiņas + %1$s neveica nekādas izmaiņas + Istabas iestatījumi + Pamest istabu + Izņemt no zemas prioritātes saraksta + Pievienot zemas prioritātes sarakstam + Izņemt no izlases + Pievienot izlasei + Uzlīme + Galerija + Audio + Kontakts + Fails + Pievienot attēlu no + QR kods + Nosaukums vai ID (#piemers:matrix.org) + Atvērt istabu katalogu + Sūtīt jaunu tiešo ziņu + Izveidot jaunu istabu + Nevarat atrast meklēto\? + Filtrēt sarakstes… + Istaba ir izveidota, bet daži ielūgumi nav nosūtīti šāda iemesla dēļ: +\n +\n%s + Pievienot šo istabu istabu katalogam + Jebkurš varēs pievienoties istabai + Izveidot jaunu istabu + Jūsu istabas parādīsies šeit. Pieskarieties + labajā apakšējā stūrī, lai atrastu esošās istabas vai izveidotu jaunu. + Atkārtot + Jūs neizmantojat nevienu identitāšu serveri + Rezerves kopiju nevarēja atšifrēt ar šo atkopšanās atslēgu: lūdzu, pārbaudiet, vai ievadījāt pareizo atkopšanās atslēgu. + Kalkulē atkopšanās atslēgu… + Frāzveida parole pārāk vāja + Nosūta doto ziņu ar sniegu + Nosūta doto ziņu ar konfeti + Atbastīta tikai šifrētās istabās + Nosūta ziņu vienkāršā tekstā, nepielietojot markdown + Izveido vienkāršu aptauju + Nosūta doto ziņu uzsvērti izkrāsotu varavīksnes krāsās + Nosūta doto ziņu izkrāsotu varavīksnes krāsās + Pievieno ¯\\_(ツ)_/¯ vienkārša teksta ziņas sākumā + Iespējo/atspējo markdown + Iestata istabas tematu + Pievienojas istabai ar norādīto aliasu + Atceļ pieejas liegumu lietotājam ar norādīto id + Komandai \"%s\" nepieciešami vairāk parametri vai arī kāds no parametriem ir nepareizs. + Vai tiešām vēlaties dzēst visas nenosūtītas ziņas šajā istabā\? + Dzēst nenosūtītās ziņas + Ziņas neizdevās nosūtīt + Vai vēlaties atcelt ziņu nosūtīšanu\? + Nosūtīta + Nosūta + Neizdevās autentificēties + Atmest izmaiņas + Te ir nesaglabātas izmaiņas. Atmest izmaiņas\? + Saite bija nepareizi veidota + QR kods nav noskenēts! + Nederīgs QR kods (nederīgs URI)! + Nevar atrast šo istabu. Pārliecinieties, ka tāda eksistē. + Brīdinājums! Pēdējais atlikušais mēģinājums pirms izrakstīšanās! + Atsaukt uzaicinājumu uz %1$s\? + Atsaukt uzaicinājumu + Meklēt kontaktus Matrix + Ielasa jūsu kontaktus… + Meklēt manos kontaktos + Telefongrāmata + Pievienot no manas telefongrāmatas + UZZINĀT VAIRĀK + SAPRATU + Esam priecīgi paziņot, ka mēs esam mainījuši nosaukumu! Jūsu lietotne ir atjaunināta un esat pierakstījies savā kontā. + Riot tagad saucas Element! + Gaida šifrēšanas vēsturi + Jūs veiksmīgi nomainījāt istabas iestatījumus + Loma + Iestatīt lomu + Atvienoties no identitāšu servera %s\? + Atvērt %s noteikumus + Dalieties ar šo kodu, lai cilvēki varētu noskenēt to, konta pievienošanai un sarakstes uzsākšanai. + Mans kods + Dalīties ar manu kodu + Skenēt QR kodu + Tas nav derīgs matrix QR kods + Uzaicinājums nosūtīts uz %1$s + Uzaicina lietotājus… + UZAICINĀT + Pievienot cilvēkus + Pievienot dalībniekus + Saite %1$s ved uz citu vietni: %2$s. +\n +\nVai tiešām vēlaties turpināt\? + Pārbaudiet šo saiti + Atzīmēt kā uzticamu + Interaktīvi verificēt ar emocijzīmēm + Verificējiet sesiju + Verificējiet jauno pierakstīšanos no sava konta: %1$s + Šifrēts ar neverificētu sesiju + sūta sniegu ❄️ + sūta konfeti 🎉 + %1$s (%2$s) + Ievadiet %s + Pieejams šifrēšanas atjauninājums + Ziņa… + Nepareizs lietotājvārds un/vai parole. Ievadītā parole sākas vai beidzas ar atstarpēm. Lūdzu, pārbaudiet to. + Gaida uz %s… + Gandrīz galā! Gaida apstiprinājumu… + Gandrīz galā! Vai otrā ierīcē redzams tas pats vairogs\? + "Temats: " + Pievienot tematu + Pabeigt + Ievadiet savu %s, lai turpinātu. + Apstiprināt %s + Konta parole + Verificējiet savas ierīces no iestatījumiem. + Kāds no uzskaitītajiem var būt kompromitēts: +\n +\n- jūsu parole +\n- jūsu bāzes serveris +\n- šī vai cita ierīce +\n- interneta savienojums kādai no ierīcēm +\n +\nMēs iesakām jums nekavējoties nomainīt paroli un atiestatīšanas atslēgu iestatījumos. + Atsvaidzināt + Vai vēlaties sūtīt šo pielikumu uz %1$s\? + Brīdinājums: + Jauna pierakstīšanās + Konta dati + Lidmašīnas režīms ir ieslēgts + Savienojums ar serveri ir zaudēts + Gandrīz galā! Vai %s redzams tas pats vairogs\? + Kamēr šis lietotājs nav padarījis šo sesiju uzticamu, ziņas uz un no tās ir marķētas ar brīdinājumiem. Alternatīvi, jūs varat manuāli verificēt šos sesiju. + %1$s (%2$s) pierakstījās, izmantojot jaunu sesiju: + Lūdzu, ievadiet frāzveida paroli + Frāzveida paroles nesakrīt + Piekļuve istabai + Pārvaldiet epasta adreses un tālruņu numurus, kas saistīti ar jūsu Matrix kontu + Epasti un tālruņa numuri + Paroles nesakrīt + Parole nav derīga + Atjaunināt paroli + Integrācija pārvaldnieks + Atļaut integrācijas + Deaktivizēt manu kontu + Sākotnējā sinhronizācija: +\nLejupielādē datus… + Sākotnējā sinhronizācija: +\nGaida servera atbildi… \ No newline at end of file diff --git a/vector/src/main/res/values-ml/strings.xml b/vector/src/main/res/values-ml/strings.xml index 867f5661c2..e11aaa7639 100644 --- a/vector/src/main/res/values-ml/strings.xml +++ b/vector/src/main/res/values-ml/strings.xml @@ -412,4 +412,232 @@ പുറത്തിറങ്ങുക പുറത്തിറങ്ങുക കീ ബാക്കപ്പ് ഉപയൊഗിക്കൂ + %1$s ഒരു സ്റ്റിക്കർ അയച്ചു. + നിങ്ങൾ ഒരു ചിത്രം അയച്ചു. + %1$s ഒരു ചിത്രം അയച്ചു. + %1$s: %2$s + നിങ്ങൾ %1$s-നെ(യെ) പുറത്താക്കി + %1$s %2$s-നെ(യെ) പുറത്താക്കി + നിങ്ങൾ ചേർന്നു + %1$s ചേർന്നു + നിങ്ങൾ മുറിയിൽ ചേർന്നു + %1$s മുറിയിൽ ചേർന്നു + %1$s നിങ്ങളെ ക്ഷണിച്ചു + നിങ്ങൾ %1$s-നെ(യെ) ക്ഷണിച്ചു + %1$s %2$s-നെ(യെ) ക്ഷണിച്ചു + നിങ്ങൾ മുറി സൃഷ്ടിച്ചു + %1$s മുറി സൃഷ്ടിച്ചു + നിങ്ങളുടെ ക്ഷണം + %s-ന്റെ ക്ഷണം + നിങ്ങൾ ഒരു സ്റ്റിക്കർ അയച്ചു. + %1$s സന്ദേശം നീക്കം ചെയ്തു [കാരണം: %2$s] + സന്ദേശം നീക്കം ചെയ്തു [കാരണം: %1$s] + സന്ദേശം %1$s നീക്കംചെയ്തു + സന്ദേശം നീക്കംചെയ്‌തു + നിങ്ങൾ മുറിയുടെ അവതാർ നീക്കംചെയ്‌തു + %1$s മുറിയുടെ അവതാർ നീക്കം ചെയ്‌തു + നിങ്ങൾ മുറിയുടെ വിഷയം നീക്കംചെയ്‌തു + %1$s മുറിയുടെ വിഷയം നീക്കം ചെയ്‌തു + നിങ്ങൾ മുറിയുടെ പേര് നീക്കംചെയ്‌തു + %1$s മുറിയുടെ പേര് നീക്കംചെയ്‌തു + (അവതാറും മാറ്റി) + VoIP കോൺഫറൻസ് പൂർത്തിയായി + VoIP കോൺഫറൻസ് ആരംഭിച്ചു + നിങ്ങൾ ഒരു VoIP കൊൺഫറൻസ് അഭ്യർത്ഥിച്ചു + %1$s ഒരു VoIP കോൺഫറൻസ് അഭ്യർത്ഥിച്ചു + 🎉 എല്ലാ സെർവറുകളും പങ്കെടുക്കുന്നതിൽ നിന്ന് വിലക്കി! ഈ മുറി ഇനി ഉപയോഗിക്കാനാവില്ല. + മാറ്റമൊന്നുമില്ല. + • ഐപി ലിറ്ററലുകളുമായി പൊരുത്തപ്പെടുന്ന സെർവർ ഇപ്പോൾ നിരോധിച്ചു. + • ഐപി ലിറ്ററലുകളുമായി പൊരുത്തപ്പെടുന്ന സെർവർ ഇപ്പോൾ അനുവദനീയമാണ്. + • അനുവദനീയ പട്ടികയിൽ നിന്നും %sമായി പൊരുത്തപ്പെടുന്ന സെർവർ നീക്കം ചെയ്തു. + • %sമായി പൊരുത്തപ്പെടുന്ന സെർവർ ഇപ്പോൾ അനുവദനീയമാണ്. + • %sമായി പൊരുത്തപ്പെടുന്ന സെർവർ നിരോധന പട്ടികയിൽ നിന്ന് നീക്കംചെയ്‌തു. + • %s മായി പൊരുത്തപ്പെടുന്ന സെർവർ ഇപ്പോൾ നിരോധിച്ചിരിക്കുന്നു. + ഈ റൂമിനായി നിങ്ങൾ സെർവർ ACL-കൾ മാറ്റി. + %s ഈ മുറിക്കായി സെർവർ ACL-കൾ മാറ്റി. + • ഐപി ലിറ്ററലുകളുമായി പൊരുത്തപ്പെടുന്ന സെർവർ അനുവദനീയമാണ്. + • ഐപി ലിറ്ററലുകളുമായി പൊരുത്തപ്പെടുന്ന സെർവർ നിരോധിച്ചിരിക്കുന്നു. + • %s മായി പൊരുത്തപ്പെടുന്ന സെർവർ നിരോധിച്ചിരിക്കുന്നു. + • %s മായി പൊരുത്തപ്പെടുന്ന സെർവർ അനുവദനീയമാണ്. + ഈ മുറിക്കായി നിങ്ങൾ സെർവർ ACL-കൾ സജ്ജമാക്കി. + %s ഈ മുറിക്കായി സെർവർ ACL-കൾ സജ്ജമാക്കി. + നിങ്ങൾ ഇവിടെ നവീകരിച്ചു. + %s ഇവിടെ നവീകരിച്ചു. + നിങ്ങൾ ഈ മുറി നവീകരിച്ചു. + %s ഈ മുറി നവീകരിച്ചു. + നിങ്ങൾ എൻഡ്-ടു-എൻഡ് എൻ‌ക്രിപ്ഷൻ ഓണാക്കി (%1$s) + %1$s എൻഡ്-ടു-എൻഡ് എൻ‌ക്രിപ്ഷൻ ഓണാക്കി (%2$s) + അജ്ഞാതം (%s). + ആർക്കും. + എല്ലാ മുറി അംഗങ്ങളും. + എല്ലാ മുറി അംഗങ്ങളും, അവർ ചേർന്ന സമയം മുതൽ. + എല്ലാ മുറി അംഗങ്ങളും, അവരെ ക്ഷണിച്ച സമയം മുതൽ. + ഭാവിയിലെ സന്ദേശങ്ങൾ %1$s ന് നിങ്ങൾ ദൃശ്യമാക്കി + %1$s ഭാവി സന്ദേശങ്ങൾ %2$s ന് ദൃശ്യമാക്കി + നിങ്ങൾ ഭാവിയിലെ മുറിയുടെ ചരിത്രം %1$s ന് ദൃശ്യമാക്കി + %1$s ഭാവിയിലെ മുറിയുടെ ചരിത്രം %2$s ന് ദൃശ്യമാക്കി + നിങ്ങൾ കോൾ അവസാനിപ്പിച്ചു. + %s കോൾ അവസാനിപ്പിച്ചു. + നിങ്ങൾ കോളിന് മറുപടി നൽകി. + %s കോളിന് മറുപടി നൽകി. + കോൾ സജ്ജീകരിക്കുന്നതിന് നിങ്ങൾ ഡാറ്റ അയച്ചു. + കോൾ സജ്ജീകരിക്കുന്നതിന് %s ഡാറ്റ അയച്ചു. + നിങ്ങൾ ഒരു വോയ്സ് കോൾ നടത്തി. + %s ഒരു വോയ്സ് കോൾ നടത്തി. + നിങ്ങൾ ഒരു വീഡിയോ കോൾ നടത്തി. + %s ഒരു വീഡിയോ കോൾ നടത്തി. + നിങ്ങൾ മുറിയുടെ പേര് ഇതിലേക്ക് മാറ്റി: %1$s + %1$s മുറിയുടെ പേര് ഇതിലേക്ക് മാറ്റി: %2$s + നിങ്ങൾ മുറിയുടെ അവതാർ മാറ്റി + %1$s മുറിയുടെ അവതാർ മാറ്റി + നിങ്ങൾ വിഷയം ഇതിലേക്ക് മാറ്റി: %1$s + %1$s വിഷയം ഇതിലേക്ക് മാറ്റി: %2$s + നിങ്ങൾ നിങ്ങളുടെ പ്രദർശന നാമം നീക്കം ചെയ്തു (ഇത് %1$s ആയിരുന്നു) + %1$s അവരുടെ പ്രദർശന നാമം നീക്കം ചെയ്തു (ഇത് %2$s ആയിരുന്നു) + നിങ്ങൾ നിങ്ങളുടെ പ്രദർശന നാമം %1$sൽ നിന്നും %2$s ലേക്ക് മാറ്റി + %1$s അവരുടെ പ്രദർശന നാമം %2$s ൽ നിന്നും %3$s ആക്കി മാറ്റി + നിങ്ങൾ നിങ്ങളുടെ പ്രദർശന നാമം %1$s ആയി സജ്ജമാക്കി + %1$s അവരുടെ പ്രദർശന നാമം %2$s ആയി സജ്ജമാക്കി + നിങ്ങൾ നിങ്ങളുടെ അവതാർ മാറ്റി + %1$s അവരുടെ അവതാർ മാറ്റി + നിങ്ങൾ %1$s ന്റെ ക്ഷണം പിൻവലിച്ചു + %1$s %2$s ന്റെ ക്ഷണം പിൻവലിച്ചു + നിങ്ങൾ %1$s നെ നിരോധിച്ചു + %1$s %2$s നെ നിരോധിച്ചു + നിങ്ങൾ %1$s ന്റെ നിരോധനം മാറ്റി + %1$s %2$s ന്റെ നിരോധനം മാറ്റി + നിങ്ങൾ ക്ഷണം നിരസിച്ചു + %1$s ക്ഷണം നിരസിച്ചു + നിങ്ങൾ മുറി വിട്ടു + %1$s മുറി വിട്ടു + നിങ്ങൾ മുറി വിട്ടു + %1$s മുറി വിട്ടു + നിങ്ങൾ ചർച്ച സൃഷ്ടിച്ചു + %1$s ചർച്ച സൃഷ്ടിച്ചു + %1$s ഈ മുറിയുടെ പ്രധാന വിലാസമായി %2$s സജ്ജമാക്കി. + നിങ്ങൾ %1$s ചേർത്ത് ഈ മുറിയുടെ വിലാസങ്ങളായിരുന്ന %2$s നീക്കം ചെയ്‌തു. + %1$s %2$s ചേർത്ത് ഈ മുറിയുടെ വിലാസങ്ങളായിരുന്ന %3$s നീക്കം ചെയ്‌തു. + + നിങ്ങൾ ഈ മുറിയുടെ വിലാസമായിരുന്ന %1$s നീക്കംചെയ്‌തു. + നിങ്ങൾ ഈ മുറിയുടെ വിലാസങ്ങളായിരുന്ന %1$s നീക്കംചെയ്‌തു. + + + %1$s ഈ മുറിയുടെ വിലാസമായിരുന്ന %2$s നീക്കംചെയ്‌തു. + %1$s ഈ മുറിയുടെ വിലാസങ്ങളായിരുന്ന %2$s നീക്കംചെയ്‌തു. + + + ഈ മുറിയുടെ വിലാസമായി നിങ്ങൾ %1$s ചേർത്തു. + ഈ മുറിയുടെ വിലാസങ്ങളായി നിങ്ങൾ %1$s ചേർത്തു. + + + ഈ മുറിയുടെ വിലാസമായി %1$s %2$s ചേർത്തു. + ഈ മുറിയുടെ വിലാസങ്ങളായി %1$s %2$s ചേർത്തു. + + %1$sന്റെ ക്ഷണം നിങ്ങൾ പിൻവലിച്ചു. കാരണം: %2$s + %1$s %2$sന്റെ ക്ഷണം പിൻവലിച്ചു. കാരണം: %3$s + %1$sനുള്ള ക്ഷണം നിങ്ങൾ സ്വീകരിച്ചു. കാരണം: %2$s + %1$s %2$sനുള്ള ക്ഷണം സ്വീകരിച്ചു. കാരണം: %3$s + %1$sന് മുറിയിൽ ചേരുന്നതിനുള്ള ക്ഷണം നിങ്ങൾ റദ്ദാക്കി. കാരണം: %2$s + %2$sന് മുറിയിൽ ചേരുന്നതിനുള്ള ക്ഷണം %1$s റദ്ദാക്കി. കാരണം: %3$s + മുറിയിൽ ചേരാൻ നിങ്ങൾ %1$sന് ഒരു ക്ഷണം അയച്ചു. കാരണം: %2$s + %1$s മുറിയിൽ ചേരാൻ %2$sന്ക്ഷണം അയച്ചു. കാരണം:%3$s + നിങ്ങൾ %1$sനെ വിലക്കി. കാരണം: %2$s + %1$s %2$s ന്റെ വിലക്ക് നീക്കി. കാരണം: %3$s + നിങ്ങൾ %1$sന്റെ വിലക്ക് നീക്കി. കാരണം: %2$s + %1$s %2$sന്റെ വിലക്ക് നീക്കി. കാരണം: %3$s + നിങ്ങൾ %1$sനെ പുറത്താക്കി. കാരണം:%2$s + %1$s %2$sനെ പുറത്താക്കി. കാരണം: %3$s + നിങ്ങൾ ക്ഷണം നിരസിച്ചു. കാരണം: %1$s + %1$s ക്ഷണം നിരസിച്ചു. കാരണം: %2$s + നിങ്ങൾ ഉപേക്ഷിച്ചു. കാരണം: %1$s + %1$s ഉപേക്ഷിച്ചു. കാരണം: %2$s + നിങ്ങൾ മുറി വിട്ടു. കാരണം: %1$s + %1$s മുറി വിട്ടു. കാരണം: %2$s + നിങ്ങൾ ചേർന്നു. കാരണം: %1$s + %1$s ചേർന്നു. കാരണം: %2$s + നിങ്ങൾ മുറിയിൽ ചേർന്നു. കാരണം: %1$s + %1$s മുറിയിൽ ചേർന്നു. കാരണം: %2$s + %1$s നിങ്ങളെ ക്ഷണിച്ചു. കാരണം: %2$s + നിങ്ങൾ %1$sനെ ക്ഷണിച്ചു. കാരണം: %2$s + %1$s %2$sനെ ക്ഷണിച്ചു. കാരണം: %3$s + നിങ്ങളുടെ ക്ഷണം. കാരണം: %1$s + %1$s ന്റെ ക്ഷണം. കാരണം: %2$s + അയയ്‌ക്കാനുള്ള ശ്രേണി മായ്‌ക്കുക + സന്ദേശം അയയ്ക്കുന്നു… + പ്രാരംഭ സമന്വയം: +\nഅക്കൗണ്ട് ഡാറ്റ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nജനസമൂഹങ്ങൾ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nഉപേക്ഷിച്ച മുറികൾ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nക്ഷണിക്കപ്പെട്ട മുറികൾ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nചേർന്ന മുറികൾ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nമുറികൾ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nക്രിപ്‌റ്റോ ഇറക്കുമതി ചെയ്യുന്നു + പ്രാരംഭ സമന്വയം: +\nഅക്കൗണ്ട് ഇറക്കുമതി ചെയ്യുന്നു… + പ്രാരംഭ സമന്വയം: +\nഡാറ്റ ഡൗൺലോഡുചെയ്യുന്നു… + പ്രാരംഭ സമന്വയം: +\nസെർവർ പ്രതികരണത്തിനായി കാത്തിരിക്കുന്നു… + ശൂന്യമായ മുറി ( %s ആയിരുന്നു) + ശൂന്യമായ മുറി + + %1$s ഉം വേറെ 1 ആളും + %1$s ഉം വേറെ %2$d പേരും + + + %1$s, %2$s, %3$s കൂടാതെ %4$d പേർ + %1$s, %2$s, %3$s കൂടാതെ %4$d പേരും + + %1$s, %2$s, %3$s പിന്നെ %4$sഉം + %1$s, %2$s പിന്നെ %3$sഉം + %1$s ഉം %2$s ഉം + മുറി ക്ഷണം + %sൽ നിന്നുമുള്ള ക്ഷണം + ഫോൺ നമ്പർ + ഈ - മെയിൽ വിലാസം + ഒരു ശൂന്യമായ മുറിയിൽ വീണ്ടും ചേരാൻ നിലവിൽ സാധ്യമല്ല. + മേട്രിക്സ് പിശക് + നെറ്റ്‌വർക്ക് പിശക് + ചിത്രം അപ്‌ലോഡുചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു + സന്ദേശം അയയ്‌ക്കാനായില്ല + പുനഃക്രമീകരിക്കാൻ കഴിഞ്ഞില്ല + അയച്ചയാളുടെ ഉപകരണം ഈ സന്ദേശത്തിനുള്ള കീകൾ ഞങ്ങൾക്ക് അയച്ചിട്ടില്ല. + ** ഡീക്രിപ്റ്റ് ചെയ്യാൻ കഴിഞ്ഞില്ല: %s ** + %1$s %2$s ന്റെ അധികാര നില മാറ്റി. + നിങ്ങൾ %1$s ന്റെ അധികാര നില മാറ്റി. + ഇച്ഛാനുസൃതം + ഇച്ഛാനുസൃതം (%1$d) + തനത് + മോഡറേറ്റർ + അഡ്മിൻ + നിങ്ങൾ വീഡിയോ കോൺഫറൻസ് പരിഷ്ക്കരിച്ചു + %1$s വീഡിയോ കോൺഫറൻസ് പരിഷ്ക്കരിച്ചു + നിങ്ങൾ വീഡിയോ കോൺഫറൻസ് അവസാനിപ്പിച്ചു + %1$s വീഡിയോ കോൺഫറൻസ് അവസാനിച്ചു + നിങ്ങൾ വീഡിയോ കോൺഫറൻസ് ആരംഭിച്ചു + %1$s വീഡിയോ കോൺഫറൻസ് ആരംഭിച്ചു + നിങ്ങൾ %1$s വിജറ്റ് പരിഷ്ക്കരിച്ചു + %1$s %2$s വിജറ്റ് പരിഷ്ക്കരിച്ചു + നിങ്ങൾ %1$s വിജറ്റ് നീക്കം ചെയ്തു + %1$s %2$s വിജറ്റ് നീക്കം ചെയ്തു + നിങ്ങൾ %1$s വിജറ്റ് ചേർത്തു + %1$s %2$s വിജറ്റ് ചേർത്തു + %1$s നുള്ള ക്ഷണം നിങ്ങൾ സ്വീകരിച്ചു + %1$s %2$sനുള്ള ക്ഷണം സ്വീകരിച്ചു + %1$sനുള്ള ക്ഷണം നിങ്ങൾ റദ്ദാക്കി + %1$s %2$sനുള്ള ക്ഷണം റദ്ദാക്കി + %1$sന് മുറിയിൽ ചേരുന്നതിനുള്ള ക്ഷണം നിങ്ങൾ റദ്ദാക്കി + %2$sന് മുറിയിൽ ചേരുന്നതിനുള്ള ക്ഷണം %1$s റദ്ദാക്കി + നിങ്ങൾ %1$s നെ ക്ഷണിച്ചു + %1$s %2$sനെ ക്ഷണിച്ചു + മുറിയിൽ ചേരാൻ നിങ്ങൾ %1$s ന് ഒരു ക്ഷണം അയച്ചു + %1$s മുറിയിൽ ചേരാൻ %2$s ന് ക്ഷണം അയച്ചു + നിങ്ങളുടെ പ്രൊഫൈൽ %1$s നവീകരിച്ചു + %1$s അവരുടെ പ്രൊഫൈൽ %2$s നവീകരിച്ചു \ No newline at end of file diff --git a/vector/src/main/res/values-nb-rNO/strings.xml b/vector/src/main/res/values-nb-rNO/strings.xml index a4fd61011d..1b4628c913 100644 --- a/vector/src/main/res/values-nb-rNO/strings.xml +++ b/vector/src/main/res/values-nb-rNO/strings.xml @@ -967,7 +967,7 @@ Forkaste endringer Les kvitteringsliste Be om krypteringsnøkler fra andre økter. - URL må starte med http[s]:// + URLen må starte med http[s]:// Du ser på varselet! Klikk på meg! Kunne ikke motta push. Løsningen kan være å installere applikasjonen på nytt. Legg til konto @@ -1027,7 +1027,7 @@ Start chat LAV PRIORITET FAVORITTER - DIREKTIV + KATALOG BLI MED Søking i krypterte rom støttes ikke ennå. FOLK @@ -1154,11 +1154,11 @@ Start ${app_name} på en annen enhet som kan dekryptere meldingen, slik at den kan sende nøklene til denne økten. Forespørsel sendt Nøkkelforespørsel sendt. - SSL-feil: identiteten til jevnaldrende er ikke bekreftet. - Kan ikke nå en hjemmeserver på denne URL-en, sjekk den + SSL Feil: Denne partnerns identitet har ikke blitt verifisert. + Kan ikke nå hjemmetjeneren på denne URLen, vennligst sjekk den Dette er ikke en gyldig adresse for en Matrix tjener - Denne URL-en er ikke tilgjengelig, sjekk den - Kan ikke registrere: e-post eierskap feil + Denne URLen kunne ikke nås, vennligst sjekk den + Klarte ikke registrere: e-posteierskapsfeil Fjern publiseringen Legg til Begynn å chatte @@ -1296,8 +1296,8 @@ \nKlikk på lenken den inneholder for å fortsette opprettelsen av kontoen. Den angitte koden er ikke riktig. Vennligst sjekk. - Det er sendt for mange forespørsler. Du kan prøve på nytt %1$d sekund… - Det er sendt for mange forespørsler. Du kan prøve på nytt %1$d sekunder… + Det er sendt for mange forespørsler. Du kan prøve på nytt om %1$d sekund… + Det er sendt for mange forespørsler. Du kan prøve på nytt om %1$d sekunder… Dette er ikke en gyldig brukeridentifikator. Forventet format: \'@bruker:homeserver.org\' Logg på for å gjenopprette krypteringsnøkler som er lagret eksklusivt på denne enheten. Du trenger dem for å lese alle dine sikre meldinger på hvilken som helst enhet. @@ -1564,15 +1564,15 @@ Filtrer utestengte brukere Endre widgets Aktiver analyse for å hjelpe med å forbedre ${app_name}. - Varsel Personvern - Inkluderer avatar og visningsnavnendringer. + Varselpersonvern + Inkluderer avatar- og visningsnavnendringer. Vis kontohendelser Invitasjoner, spark og utestengelser er ikke påvirket. Vis delta og forlate arrangementer Inkluderer invitasjoner/delta/forlot/spark/utesteng hendelser og avatar/visningsnavnendringer. Vis chateffekter - Vis statens medlemsarrangementer - Vis tidsstempler i 12-timers format + Vis statushendelser angående rommedlemmer + Vis tidsstempler i 12-timersformat Markdown formatering Forhåndsvis lenker i chatten når hjemmeserveren din støtter denne funksjonen. Forhåndsvisning av innebygd URL @@ -1581,14 +1581,14 @@ Hjemmeskjerm Varslingsmål Administrer kryptografinøkler - Bruk en Integration Manager til å administrere roboter, broer, widgets og klistremerkepakker. -\nIntegration Managers mottar konfigurasjonsdata, og kan endre moduler, sende rominvitasjoner og angi strømnivåer på dine vegne. + Bruk en integrasjonshåndterer til å administrere botter, broer, widgets og klistremerkepakker. +\nIntegrasjonshåndterere mottar konfigurasjonsdata, og kan endre moduler, sende rominvitasjoner og angi maktnivåer på dine vegne. Forsinkelse mellom hver synkronisering %s \nSynkroniseringen kan bli utsatt avhengig av ressursene (batteriet) eller enhetens tilstand (hvilemodus). Foretrukket synkroniseringsintervall - Tidsavbrudd for synkronisering forespørsel - Aktiver synkronisering i bakgrunnen + Tidsavbrudd for synkroniseringsforespørsel + Aktiver bakgrunnssynkronisering Du vil ikke bli varslet om innkommende meldinger når appen er i bakgrunnen. ${app_name} vil synkroniseres i bakgrunnen med jevne mellomrom på presis tid (konfigurerbar). \nDette vil påvirke radio og batteribruk, det vises en permanent melding om at ${app_name} lytter etter hendelser. @@ -1599,15 +1599,15 @@ Msgs som inneholder visningsnavnet mitt Konfigurer stille varsler Konfigurer anropsvarsler - Konfigurer lyd varsler - • Varsler vil <b>ikke vise meldingsinnhold<b> + Konfigurer høylytte varsler + • Varsler vil ikke vise meldingsinnhold Ignorer optimalisering Aktiver Start ved oppstart Tjenesten starter når enheten startes på nytt. Tjenesten kunne ikke startes på nytt - Tjenesten stoppet og startet på nytt automatisk. - Varslingstjeneste automatisk omstart - Start Tjeneste + Tjenesten ble stoppet og startet på nytt automatisk. + Automatisk omstart av varslingstjenesten + Start tjeneste Varslingstjenesten kjører ikke. \nPrøv å starte programmet på nytt. Varslingstjenesten kjører. diff --git a/vector/src/main/res/values-nb/strings.xml b/vector/src/main/res/values-nb/strings.xml deleted file mode 100644 index 3a7caefc55..0000000000 --- a/vector/src/main/res/values-nb/strings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Du opprettet diskusjonen - Du opprettet rommet - Invitasjonen din - Du sendte et klistremerke. - %1$s sendte et klistremerke. - Du sendte et bilde. - %1$s sendte et bilde. - %1$s: %2$s - %1$s skapte rommet - \ No newline at end of file diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index d346cbff34..b46bacebca 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -2675,4 +2675,70 @@ Pausar Retomar Voltar + Nível de confiança padrão + Selecionado + Vídeo + Algumas mensagens não foram enviadas + Remover foto de perfil + Mudar foto de perfil + Imagem + Importar chave do arquivo + Abrir widgets + Captura de tela + O limite é desconhecido. + O seu servidor local aceita anexos (arquivos, mídia, etc) com tamanhos de até %s. + Versão do servidor + Nome do servidor + Configurações da sala + Sair da chamada atual e mudar para a outra\? + Versão da sala + Mostrar todas as salas na lista de salas, incluindo as salas com conteúdo sensível. + Mostrar salas com conteúdo sensível + Lista de salas + Novo valor + Alterar + Sincronização inicial: +\nBaixando dados… + Sincronização inicial: +\nAguardando resposta do servidor… + Nível de confiança: confiável + Nível de confiança: alerta + Deseja mesmo excluir todas as mensagens não enviadas nesta sala\? + Excluir as mensagens não enviadas + Falha ao enviar as mensagens + Quer cancelar o envio da mensagem\? + Excluir todas as mensagens com falha + Falhou + Enviado + Enviando + Conteúdo do evento + Evento do estado enviado! + Evento enviado! + Evento malformado + Tipo de mensagem ausente + Nenhum conteúdo + Conteúdo do evento + Chave do estado + Tipo + Enviar evento de estado personalizado + Editar conteúdo + Eventos do estado + Enviar evento do estado + Enviar evento personalizado + Explorar o estado da sala + Ferramentas de desenvolvimento + Ver confirmações de leitura + Não notificar + Notificar sem som + Notificar com som + Mensagem não enviada devido a um erro + Verificado + Fechar o selecionador de emojis + Abrir o selecionador de emojis + Esta sala tem rascunho não enviado + + %d entrada + %d entradas + + Limite do envio de arquivo do servidor \ No newline at end of file diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index e401e856ce..1ed1b17734 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -2752,4 +2752,72 @@ Нет учётных данных, неправильная учётная запись пользователя и/или пароль Вернуть + Вы уверены, что хотите удалить все неотправленные сообщения в этой комнате\? + Удалить неотправленные сообщения + Сообщения не удалось отправить + Вы хотите отменить отправку сообщения\? + Удалить все неудачные сообщения + Не удалось + Отправлено + Отправка + Содержание события + Событие статуса отправлено! + Событие отправлено! + Неисправное событие + Отсутствует тип сообщения + Нет содержания + Содержание события + Ключ статуса + Тип + Отправить пользовательское событие статуса + Редактировать содержание + События статуса + Исследовать статус комнаты + Отправить событие статуса + Отправить пользовательское событие + Инструменты для разработчиков + См. подтверждение получения + Не уведомлять + Уведомить без звука + Уведомить со звуком + Сообщение не отправлено из-за ошибки + Проверено + Закрыть выбор эмодзи + Открыть выбор эмодзи + Доверенный уровень доверия + Предупреждающий уровень доверия + Уровень доверия по умолчанию + Выбрано + Видео + В этой комнате есть неотправленный черновик + Некоторые сообщения не были отправлены + Удалить аватар + Сменить аватар + Изображение + Импорт ключа из файла + Открытые виджеты + Скриншот + + %d запись + %d записи + %d записей + %d записей + + Лимит неизвестен. + Ваш домашний сервер принимает вложения (файлы, медиа и т.д.) размером до %s. + Лимит загрузки файла сервера + Версия сервера + Название сервера + Настройки комнаты + Покинуть текущую конференцию и перейти к другой\? + Версия комнаты + Показать все команты в списке комнат, в том числе с чувствительным содержанием. + Показать комнаты с чувствительным содержанием + Список комнат + Новое значение + Сменить + Начальная синхронизация: +\nЗагрузка данных… + Начальная синхронизация: +\nОжидание ответа сервера… \ No newline at end of file diff --git a/vector/src/main/res/values-sr/strings.xml b/vector/src/main/res/values-sr/strings.xml index 2ce03e4a54..065fd3154b 100644 --- a/vector/src/main/res/values-sr/strings.xml +++ b/vector/src/main/res/values-sr/strings.xml @@ -15,7 +15,7 @@ Празна соба (била је %s) Празна соба - %1$s и још %2$d + %1$s и још 1 %1$s и још %2$d %1$s и још %2$d @@ -450,4 +450,312 @@ Ресет Одбаци Паузирај + Неуспешно иницијализовање камере + Веза није успела + Дописник није одговорио. + Поставили сте позив на чекање + %s је ставио позив на чекање + Стави на чекање + Резиме + Врати се на позив + Активни позив (%s) + Видео позив у току… + Позив у току… + Улазни гласовни позив + Улазни видео позив + Улазни позив + Позив… + Позив завршен + Прикључивање позива… + Позив прикључен + Позив + Одабери мелодију за позиве: + Мелодија долазног позива + Дозволи сервер за повратни позив + Користити звоно од ${app_name} за долазне позиве + Затражити потврду пре него што се започнете позив + Спречити случајни позив + Позиви + Тема собе + Име собе + Данас + Јуче + %1$dм %2$dс + %d с + Поништи отпремање\? + Поништи преузимање\? + Мало + Средње + Велико + Оригинално + Погрешно корисничко име/лозинка + SSL грешка. + SSL грешка: Вршњачки идентитет није верификован. + Ово није валидна адреса Matrix сервера + Ова УРЛ адреса није доступна, молимо вас да проверите + Унети важећи URL + Немогућа регистрација: погрешна провера власништва имејла + Немогућност регистрације + Немогућност регистрације: мрежна грешка + Немогућност пријављивања + Није могуће пријавити се: мрежна грешка + URL треба да почне са http[s]:// + Прегледајте и прихватите политику овог сервера: + Ваша лозинка је ресетована. +\n +\nОдјављени сте од свих сесија и више нећете примати пуш обавештења. Да бисте поново омогућили обавештења, поново се пријавите на сваки уређај. + Послат је имејл на %s. Једном када кликнете на везу из поруке, кликните доле. + Регистрација са имејлом и телефонски број одједном није подржана док АПИ не постоји. Биће узети у обзир само телефонски број. +\n +\nМожете да додате свој имејл на свој профил у подешавањима. + Подесите е-пошту за опоравак рачуна. После користите имејл или телефон како би вас открити људи који вас познају. + Подесите е-пошту за опоравак рачуна. После користите имејл или телефон како би вас открити људи који вас познају. + Подесите имејл за опоравак рачуна, а касније како би вас открити људи који вас познају. + Попуните телефонски број како би вас открити људи који вас познају. + Тренутно немате ни један пакет стикера. +\n +\nДодати сада неки\? + Није успело успостављање везе у реалном времену. +\nЗамолите администратора вашег домаћег сервера да конфигурише TURN сервер како би позиви поуздано радили. + Опишите грешку. Шта сте урадили\? Шта сте очекивали да се догоди\? Шта се заправо догодило\? + Укључите историју размене кључева + Састанци користе Jitsi безбедносне и допунске политике. Сви људи тренутно у соби видеће позив да се придруже састанку. + Није успело верификовати имејл адресу: обавезно кликните на линк из примљеног имејла + Мора се унети нова лозинка. + Мора се унети имејл адреса коришћена са вашим налогом. + Да бисте ресетовали лозинку, унесите имејл адресу коришћену са налогом: + Проверио сам своју имејл адресу + Сервер идентитета: + Кућни сервер: + Корисничко име већ употребљено + Овај кућни сервер жели да се увери да нисте робот + Проверити Ваш имејл да би наставили регистрацију + Користите друге опције сервера (напредно) + Заборавили сте лозинку\? + Лозинке нису исте + Неважећи жетон + Недостаје имејл адреса или број телефона + Недостаје број телефона + Недостаје имејл адреса + Овај број телефона је већ коришћен. + Ова имејл адреса је већ коришћена. + Ово не изгледа као важећи број телефона + Ово не изгледа као важећа имејл адреса + Недостаје лозинка + Лозинка премала (6 минимум) + Корисничка имена могу садржати само слова, бројеве, тачке, цртице и подвлаке + Нетачно корисничко име и/или лозинка + Потврдите нову лозинку + Поновити лозинку + Број трелефона (опционо) + Број телефона + Имејл адреса (опционо) + Имејл адреса + Нажалост, ни једна спољна апликација није нађена да би испунила ову акцију. + наставити са… + Послати датотеке + Упали HD + Угаси HD + Позади + Испред + Пребаците камеру + Бежичне слушалице + Слушалице + Звучници + Телефон + Изаберите звучни уређај + ${app_name} Позив није успео + Не питај ме више + Пробајте да користите %s + Позив није успео због погрешне конфигурације сервера + Да ли сте сигурни да желите да започнете видео позив\? + Да ли сте сигурни да желите да започнете гласовни позив\? + Да ли сте сигурни да желите да започнете ћаскање са %s\? + Пошаљи гласовну поруку + Извештај о грешци није успео да се пошаље (%s) + Извештај о грешци је успешно послан + Апликација се последњи пут срушила. Да ли желите да отворите екран за пријаву грешке\? + Изгледа да мрдате телефон из фрустрације. Да ли желите да отворите екран за извештавање о грешкама\? + Ако је могуће, молим вас напишите опис на енглеском језику. + Прикажи све собе у директорију собе, укључујући собе са експлицитним садржајем. + Прикажи собе са експлицитним садржајем + Директоријум собе + Филтрирајте имена заједнице + Филтрирати имена собе + Филтрирати особе + Филтрирати фаворите + Филтрирати имена собе + Нова вредност + Вратити се + Променити + Не можете да поставите позив са собом, причекајте да учесници прихвате позивницу + Не можете да поставите позив са собом + Не може да се започне позив + Конференција је већ у току! + Немате дозволу за започињање позива + Немате дозволу за започињање позива у овој соби + Немате дозволу за започињање конференцијског позива + Немате дозволу за започињање конференцијског позива у овој соби + Потребна вам је дозвола за позивање да започнете конференцију у овој соби + Због недостајућих дозвола, ова акција није могућа. + Због недостајућих дозвола, неке функције могу недостајати… + Не може се покренути позив, покушајте касније + Конференцијски позив у току. +\nПридружи се као %1$s или %2$s + ТрајнаВеза + Почетна синхронизација: +\nПреузимање података… + Почетна синхронизација: +\nЧека се одговор сервера… + Променити дозволе + Променити име собе + Променити видљивост историје + Активирати шифровање собе + Променити главну адресу собе + Променити аватар собе + Променити widget-е + Обавестити све + Уклонити поруке које су други послали + Забранити кориснике + Протерати кориснике + Променити подешавања + Позвати чланове + Слати поруке + Подразумевана улога + Дозволе + Дозволе собе + Сигурно уклонити позив овој корисника\? + Откажи позив + Покажи све поруке овог корисника + Не игнорисањем овог корисника ће поново приказати све његове поруке. + Не игнориши више корисника + Игнорисај + Игнорирање овог корисника уклониће своје поруке из соба које делите. +\n +\nОву акцију можете поништити у било које време у општим подешавањима. + Игнорирај корисника + Снизити права + Нећете моћи да поништите ову промену пошто снизујете ваша права, ако сте последњи привилеговани корисник у соби, неће бити могуће повратити привилегије. + Ауто снизити права\? + Покажи списак сесије + Спомени + Идентификатор, име или имејл + Постави као администратор + Постави као модератор + Рисетуј на нормалан корисник + Изтерај + Уклони забрану + Забрани + Уклони из ове собе + Напусти ову собу + Поништи позив + Позови + СЕСИЈЕ + Приватни разговори + ПОЗИВ + АДМИНИСТРАТОРСКЕ АЛАТКЕ + %1$s пре %2$s + Сада %1$s + Неактиван + Ван везе + На вези + Креирај + Сигурно уклонити %s из овог ћаскања\? + Ова соба није јавна. Нећете моћи поново да се придружите без позива. + Стварно напустити ову собу\? + Напусти собу + + %dд + %dд + %dд + + + %dх + %dх + %dх + + + %dм + %dм + %dм + + + %dс + %dс + %dс + + 1 члан + + %d члан + %d члана + %d чланова + + + %d активан члан + %d активна члана + %d активних чланова + + Додај члана + Ново ћаскање + Додајте сервер идентитета у своја подешавања да бисте извршили ову акцију. + Ово је преглед ове собе. Интеракције собе су онемогућене. + једна соба + Покушавате да приступите %s. Да ли желите да се придружите да бисте учествовали у дискусији\? + %s Вас је позвао да се придружите овој соби + Ићи на прву непрочитану поруку. + Синхронизација… + Отвори заглавље + Листа чланова + Одбаци + Преглед + Придружи се + Избриши + Настави + Послати одговор (НЕшифрован)… + Послати шифрован дговор… + Послати поруку (НЕшифровану)… + Послати шифровану поруку… + %1$s & %2$s & други пишу… + %1$s & %2$s пишу… + %s пише… + Тражи + Е-пошта или Matrix ИД + Само Matrix кориснике + КОРИСНИЧКИ ДИРЕКТОРИЈУМ (%s) + ЛОКАЛНИ КОНТАКТИ (%d) + %1$s %2$s + %1$s и %2$s + "%1$s, " + Разлог + Склањање забране корисника ће му омогућити да се поново придружи соби. + Забрањен корисник ће бити избачен из ове собе и спречити ига да се поново придружи. + Склони забрану корисника + Разлог забране + Забрани корисника + Разлог одбацивања + Одбацити корисник + НЕ + ДА + Сачувати у преузимања\? + Сачувано + Дозволите приступ вашим контактима. + Да бисте скенирали QR кôд, морате дозволити приступ камеру. + Извињавам се. Акција није извршена, због недостајућих дозвола + " +\n +\nМолимо вас да дозволите приступ у следећем прозору да бисте могли да урадите позив." + " +\n +\nМолимо вас да дозволите приступ у следећем прозору да бисте могли да урадите позив." + Информација + Не може да се сними видео + Узми слику или видео + Позив одговорен на друго место + Пошаљи као + Листа група + Листа потврђивања за читање + Послат је захтев + Послат је захтев за кључ. + Нисте још кликнули у везу из послате е-поште + Ово корисничко име је већ коришћено \ No newline at end of file diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 87f7144994..31ac8d13c6 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -2661,4 +2661,15 @@ Skicka anpassad rumshändelse Utforska rumsläge Utvecklingsverktyg + Visa alla rum i rumskatalogen, inklusive rum med stötande innehåll. + Visa rum med stötande innehåll + Är du säker på att du vill radera alla oskickade meddelanden i det här rummet\? + Radera oskickade meddelanden + Meddelanden misslyckades att skickas + Vill du avbryta sändning av meddelanden\? + Radera alla misslyckade meddelanden + Misslyckad + Skickad + Skickar + Rumskatalog \ No newline at end of file diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml index 13d4a7c313..4739830ea4 100644 --- a/vector/src/main/res/values-tr/strings.xml +++ b/vector/src/main/res/values-tr/strings.xml @@ -1712,4 +1712,108 @@ %s aramayı bekletti Beklet Devam et + Avatarını değiştirdin + %1$s avatarını değiştirdi + %1$s kişisinin davetini geri çektin + %1$s, %2$s kişisinin davetini geri çekti + %1$s kişisini banladınız + %1$s, %2$s kişisini banladı + Daveti reddettin + %1$s daveti reddetti + Odadan ayrıldın + %1$s odadan ayrıldı + Odadan ayrıldın + %1$s odadan ayrıldı + Katıldın + %1$s katıldı + Odaya katıldın + %1$s odaya katıldı + %1$s kişisi seni davet etti + %1$s kişisini davet ettin + %1$s, %2$s kişisini davet etti + Tartışmayı oluşturdun + %1$s tartışmayı oluşturdu + Odayı oluşturdun + %1$s odayı oluşturdu + Senin davetin + %s\'nin daveti + Bir çıkartma gönderdin. + %1$s bir çıkartma gönderdi. + Bir fotoğraf gönderdin. + %1$s bir fotoğraf gönderdi. + %1$s%2$s + %1$s kullanıcısı görünen ismini kaldırdı (önceden şuydu: %2$s) + %1$s görünen adınızı şuna değiştiniz: %2$s + %1$s , %2$s görünen adını şununla değişti: %3$s + Görünen adınızı şuna değiştiniz: %1$s + %1$s görünen adını şuna değişti: %2$s + %1$s kişisini davet ettiniz + %1$s, %2$s kişisini davet etti + Mesaj %1$s tarafından silindi + Mesaj silindi + Oda avatarını kaldırdınız + %1$s oda avatarını kaldırdı + Oda konusunu kaldırdınız + %1$s oda konusunu kaldırdı + Oda ismini kaldırdınız + tüm oda üyeleri. + Aramayı sonlandırdınız. + %s aramayı sonlandırdı. + Aramayı cevapladınız. + %s aramayı cevapladı. + Bir sesli arama başlattınız. + %s bir sesli arama başlattı. + Görüntülü arama başlattınız. + %s bir görüntülü arama başlattı. + Oda ismini şuna değiştirdiniz: %1$s + %1$s oda ismini şuna değiştirdi: %2$s + Oda avatarını değiştirdiniz + %1$s oda avatarını değiştirdi + Konuyu şuna değiştirdiniz: %1$s + %1$s konuyu şuna değiştirdi: %2$s + %1$s adlı kişinin banını kaldırdı + %1$s, %2$s adlı kişinin banını kaldırdı + %1$s adlı kullanıcıyı attı + %1$s, %2$s adlı kullanıcıyı attı + Oda ayarları + Şifreyi göster + Şu zamanda okundu: + Mevcut konferanstan ayrıl ve bir diğerine git\? + Oda sürümü + Mesaj gönderiliyor… + Boş oda + %1$s, %2$s ve %3$s + Özel + %1$s widgetını kaldırdınız + %1$s widgetını eklediniz + Profilinizi güncellediniz %1$s + Oda daveti + Telefon numarası + E-posta adresi + Matrix hatası + Ağ hatası + Görüntü yüklenemedi + Mesaj gönderilemedi + Varsayılan + Video konferansı düzenlediniz + Video konferans %1$s tarafından düzenlendi + Video konferansı sonlandırdınız + Video konferans %1$s tarafından sonlandırıldı + Video konferansı başlattınız + %1$s kişisinin davetini kabul ettiniz + %1$s, %2$s kişisinin davetini kabul etti + %1$s kişisinin davetini geri aldınız + %1$s, %2$s kişisinin davetini geri aldı + %1$s Kişisinin odaya katılma davetini geri aldınız + %1$s, %2$s kişisinin odaya katılma davetini geri aldı + Odaya katılması için %1$s kişisine davet gönderdiniz + %1$s, odaya katılması için %2$s kişisine davet gönderdi + Mesaj %1$s tarafından kaldırıldı [sebep:%2$s] + Mesaj kaldırıldı [sebep: %1$s] + %1$s oda ismini kaldırdı + (avatar da değiştirildi) + Değişiklik yok. + Bu odayı geliştirdiniz. + %s bu odayı geliştirdi. + Uçtan uca şifrelemeyi açtınız (%1$s) \ No newline at end of file diff --git a/vector/src/main/res/values-tzm/strings.xml b/vector/src/main/res/values-tzm/strings.xml index befe8dd878..b8688a3c6b 100644 --- a/vector/src/main/res/values-tzm/strings.xml +++ b/vector/src/main/res/values-tzm/strings.xml @@ -31,4 +31,59 @@ Tisɣal Tisɣal Ssenfel Tisɣal + Rar + Talgoritmet + Tansa + Abda + Dɣer + Dɣer + Azen + Tuzinin + LKM + IFUYLA + Rzu + Ifuyla + Rzu + "%1$s, " + ƔER + + %das + %das + + Agey + Lkem + UHU + YAH + Aɣuri… + Ɣer + Assa + Atilifun + Rzu + Ɣer + Tineɣmisin + Azgal + Rgel + Ṛẓem + Tigawin + Ffeɣ + Agey + Ɣer + neɣ + Avidyu + Ɣer + Kkes + Sfeḍ + Ssiwel + Bḍu + Agem + Ssifeḍ + Azen + Ffel + Ḥḍu + Sser + WAX + Azdam… + Tuzend yat twellaft. + yuzen %1$s yat twellafet. + %1$s: %2$s \ No newline at end of file diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index 09aafac703..4d8cd05f6e 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -46,7 +46,7 @@ %1$s приймає запрошення до %2$s ** Неможливо розшифрувати: %s ** Пристрій відправника не надіслав нам ключ для цього повідомлення. - Неможливо відредагувати + Не вдається редагувати Не вдалося надіслати повідомлення Не вдалося завантажити зображення Помилка мережі @@ -94,10 +94,10 @@ Типово Модератор Адміністратор - Ви вилучили %1$s віджет - %1$s вилучає %2$s віджет - Ви додали %1$s віджет - %1$s додає %2$s віджет + Ви вилучили %1$s знадіб + %1$s вилучає %2$s знадіб + Ви додали %1$s знадіб + %1$s додає %2$s знадіб Ви прийняли запрошення до %1$s Ви надіслали запрошення для %1$s приєднатися до кімнати Ви оновили свій профіль %1$s @@ -158,8 +158,8 @@ Порожня кімната (була %s) Власний Власний (%1$d) - Ви змінили віджет %1$s - %1$s змінює віджет %2$s + Ви змінили знадіб %1$s + %1$s змінює знадіб %2$s Ви оновили кімнату. Ви зробили майбутню історію кімнати видимою для %1$s Ви зробили майбутні повідомлення видимими для %1$s @@ -194,7 +194,7 @@ Надіслати Надіслати ще раз Прибрати - Цитата + Цитувати Поділитися Пізніше Переслати @@ -246,7 +246,7 @@ Пошук кімнат Запрошення - Низький пріоритет + Неважливі Бесіди Локальні контакти @@ -285,7 +285,7 @@ Увійти Вийти URL сервера - URL сервера аутентифікації + URL сервера ідентифікації Пошук Почати новий чат Здійснити голосовий виклик @@ -328,7 +328,7 @@ Цей сервер хоче переконатися, що ви не робот Логін вже використовується Сервер: - Сервер аутентифікації: + Сервер ідентифікації: Я перевірив(ла) свою email адресу Для скидання паролю введіть email прив\'язаний до облікового запису: Необхідно ввести email прив\'язаний до вашого облікового запису\'. @@ -426,8 +426,8 @@ 1 учасник Залишити кімнату - Ви впевнені, що хочете залишити кімнату? - Ви впевнені, що хочете вилучити %s з цієї кімнати? + Ви впевнені, що бажаєте залишити кімнату\? + Ви впевнені, що бажаєте вилучити %s з цієї кімнати\? Створити Online Offline @@ -435,7 +435,7 @@ АДМІНІСТРУВАННЯ ВИКЛИК ПРЯМІ ЧАТИ - ПРИСТРОЇ + СЕАНСИ Запросити Залишити цю кімнату Вилучити з цієї кімнати @@ -444,13 +444,13 @@ Зробити звичайним користувачем Зробити модератором Зробити адміністратором - Ігнорувати користувача + Нехтувати Перестати ігнорувати ID користувача, ім\'я або email Згадати Показати Список Пристроїв Ви не зможете скасувати цю дію, оскільки надаєте користувачу той же рівень доступу, що й у вас.\nВи впевнені? - "Впевнені, що хочете запросити %s до цієї кімнати?" + Ви впевнені, що бажаєте запросити %s до цієї кімнати\? Запросити за ID Локальні Контакти (%d) @@ -468,7 +468,7 @@ Надіслати повідомлення (нешифроване)… Зв\'язок із сервером втрачено. Повідомлення не надіслані. %1$s або %2$s зараз? - Повідомлення не надіслані через присутність невідомих пристроїв. %1$s або %2$s зараз? + Повідомлення не надіслані через присутність невідомих сеансів. %1$s або %2$s зараз\? Повторити надсилання скасувати все Надіслати ненадіслані повідомлення знову @@ -515,7 +515,7 @@ КАТАЛОГ ОБРАНІ КІМНАТИ - НИЗЬКИЙ ПРІОРИТЕТ + НЕВАЖЛИВІ ЗАПРОШЕННЯ Почати чат Створити кімнату @@ -575,7 +575,7 @@ Зберігати медіа Налаштування користувача Сповіщення - Ігноровані користувачі + Нехтувані користувачі Інше Розширені Криптографія @@ -586,7 +586,7 @@ Домашній екран Закріплювати кімнати з пропущеними сповіщеннями Закріплювати кімнати з новими повідомленнями - Пристрої + Сеанси Показувати час надсилання для всіх повідомлень Показувати час надсилання у 12-годинному форматі @@ -604,7 +604,7 @@ Надіслати Залоговано як Cервер - Сервер Аутентифікації + Сервер ідентифікації Інтерфейс користувача Мова Оберіть мову @@ -623,8 +623,8 @@ Показувати всі повідомлення %s\? \n \nЗауважте, що це перезавантажить застосунок та може тривати деякий час. - Ви справді бажаєте видалити цю ціль сповіщень? - Справді бажаєте видалити %1$s %2$s? + Ви впевнені, що бажаєте видалити цю ціль сповіщень\? + Ви впевнені, що бажаєте видалити %1$s %2$s\? Оберіть країну Країна Будь ласка, оберіть країну @@ -649,13 +649,13 @@ Позначено як: Улюблені - Низький пріоритет + Неважливі Жодного Доступ та видимість Показувати кімнату при пошуку Доступ - Читабельність історії + Прочитність історії Хто може читати історію повідомлень? Хто має доступ до кімнати? @@ -679,8 +679,8 @@ Наскрізне шифрування Наскрізне шифрування увімкнено Вийдіть з облікового запису, щоб отримати змогу увімкнути шифрування. - Шифрувати лише до перевірених пристроїв - Ніколи не надсилати шифровані повідомлення з цього пристрою неперевіреним пристроям у цій кімнаті. + Шифрувати лише до звірених сеансів + Ніколи не надсилати зашифровані повідомлення з цього сеансу незвіреним сеансам у цій кімнаті. Кімната не має локальної адреси Нова адреса (e.g #foo:matrix.org") @@ -728,10 +728,10 @@ Імпортувати ключі кімнати Імпортувати ключі з локального файлу Імпортувати - Шифрувати лише для перевірених пристроїв - Ніколи не надсилати шифровані повідомлення з цього пристрою неперевіреним пристроям. - НЕ перевірено - Перевірено + Шифрувати лише для звірених сеансів + Ніколи не надсилати зашифровані повідомлення з цього сеансу на незвірені сеанси. + НЕ звірено + Звірено У чорному списку невідомий пристрій порожньо @@ -746,8 +746,12 @@ У майбутньому цей процес верифікації стане більш складним. Я підтверджую, що ключі співпадають - Кімната містить невідомі пристрої - Кімната містить неперевірені невідомі пристрої.\nThis means there is no guarantee that the devices belong to the users they claim to.\nWe recommend you go through the verification process for each device before continuing, but you can resend the message without verifying if you prefer.\n\nUnknown devices: + Кімната містить невідомі сеанси + Кімната містить незвірені невідомі сеанси. +\nЦе означає відсутність будь-яких гарантій у тому, що сеанси належать тим користувачам, які заявляють про належність цих сеансів їм. +\nМи радимо вам звірити кожен сеанс перед тим, як продовжити, проте ви можете перенадіслати повідомлення без звірки, якщо цього бажаєте. +\n +\nНевідомі сеанси: Вибір каталогу кімнат Можливо сервер недоступний чи перевантажений @@ -767,12 +771,12 @@ Найбільший Величезний - Для керування віджетами у цій кімнаті потрібен дозвіл - Помилка створення віджету + Для керування знадобами у цій кімнаті потрібен дозвіл + Помилка створення знадобу Здійснювати конференц дзвінки через Jitsi - Справді бажаєте видалити віджет? + Ви впевнені, що бажаєте видалити знадіб з цієї кімнати\? - Не вдалося створити віджет. + Не вдалося створити знадіб. Не вдалося надіслати запит. Рівень доступу має бути більше 0. Ви не перебуваєте в цій кімнаті. @@ -795,10 +799,10 @@ Використовувати рідну камеру Щойно доданий вами пристрій \'%s\' править ключі шифрування. - Ваш неперевірений пристрій \'%s\' править ключі шифрування. + Ваш незвірений пристрій \'%s\' вимагає ключі шифрування. Почати перевірку Поділитись без перевірки - Знехтувати запитом + Знехтувати запит Помилка виконання команди Команду %s не розпізнано @@ -812,9 +816,9 @@ Спільноти Нема груп Струснути пристрій, щоб повідомити про помилку - Ви дійсно хочете почати новий чат з %s? - Ви впевнені, що бажаєте почати голосовий виклик? - Ви впевнені, що бажаєте почати відео виклик? + Ви впевнені, що бажаєте розпочати новий чат з %s\? + Ви впевнені, що бажаєте розпочати голосовий виклик\? + Ви впевнені, що бажаєте розпочати відео виклик\? Список груп %d зміна членства @@ -832,9 +836,9 @@ Додати ярлик на головний екран Приватність сповіщень Нормальний - Відправити наліпку - Відправити стікер - У вас зараз не має стікерів. + Надіслати наліпку + Надіслати наліпку + У вас поки що не має наліпок. \n \nДодати зараз\? @@ -865,12 +869,12 @@ Завантажити Говорити Очистити - Вдруге запитати ключі шифрування з інших ваших пристроїв. + Вдруге запитати ключі шифрування з інших ваших сеансів. Запит ключа відправлений. Запит відправлений Вібрація при згадуванні користувача - Деактивація аккаунта - Деактивувати мій аккаунт + Деактивація облікового запису + Деактивувати мій обліковий запис Конфіденційність сповіщень Надати дозвіл Вибрати другий варіант @@ -972,10 +976,10 @@ %1$s у %2$s - %d активний віджет - %d активні віджети - %d активних віджетів - + %d активний знадіб + %d активні знадоби + %d активних знадобів + %d активних знадобів Пропущено обов’язковий параметр. Недійсний параметр. @@ -1047,14 +1051,14 @@ Продовження розмови тут Ця кімната є продовженням іншої розмови Натисніть сюди, щоб побачити старіші повідомлення - Перевищено ресурсний ліміт + Перевищено ресурсне обмеження Зв’язатися з адміністратором зв’яжіться з Вашим адміністратором Цей сервер перевищив один із ресурсних лімітів, тому деякі користувачі не зможуть увійти в систему. - Цей сервер перевищив один із ресурсних лімітів. - Цей сервер досяг свого місячного ліміту активних користувачів, тому деякі з них не зможуть увійти в систему. - Цей сервер досяг свого місячного ліміту активних користувачів. - Будь ласка, %s для збільшення цього ліміту. + Цей домашній сервер досяг одного зі своїх ресурсних обмежень. + Цей сервер досяг свого місячного обмеження на активних користувачів, тому деякі з них не зможуть увійти в систему. + Цей сервер досяг свого місячного обмеження на активних користувачів. + Будь ласка, %s для збільшення цього обмеження. Будь ласка, %s для продовження використання цього сервісу. Поступове завантаження співрозмовників Підвищити продуктивність, завантажуючи співрозмовників лише при першому перегляді. @@ -1122,8 +1126,8 @@ Увімкнути HD Вимкнути HD Основна - Фронтальна - Переключити камеру + Передня + Перемкнути камеру Бездротова гарнітура Гарнітура Динамік @@ -1139,15 +1143,15 @@ Ви впевнені, що бажаєте вийти\? Покласти слухавку Відхилити - Прийняти + Відповісти Відмовити Огляд Знехтувати Перервати Готово Пропустити - Не вдалося видалити віджет - Не вдалося додати віджет + Не вдалося видалити знадіб + Не вдалося додати знадіб Ви не можете здійснити дзвінок із самим собою Почати аудіо-зустріч Почати відеозустріч @@ -1168,7 +1172,7 @@ Резервне копіювання ключів… Якщо вийти зараз, ви втратите свої зашифровані повідомлення Перевірка сеансу - Стікер + Наліпка Галерея Файл Додати зображення з @@ -1216,10 +1220,10 @@ Призначити роль Надіслати Номери телефонів - Email адреси + Електронні адреси Скасувати запрошення 🔐️ Приєднуйтесь до мене в ${app_name} - Привіт, поспілкуйся зі мною в ${app_name}: %s + Привіт! Спілкуймося в ${app_name}: %s Запросити друзів Всі спільноти Показувати заглушку на місці видалених повідомлень @@ -1243,7 +1247,7 @@ Надіслати електронні адреси та номери телефонів Надіслати електронні адреси та номери телефонів Керування електронними адресами та номерами телефонів, пов’язаними з вашим обліковим записом Matrix - Електорнні адреси та номери телефонів + Електронні адреси та номери телефонів Надіслати історію запитів спільного доступу до ключів Стрічка подій Непрочитані повідомлення @@ -1290,9 +1294,9 @@ Налаштування сповіщень викликів Налаштування гучних сповіщень Початкова синхронізація… - Видимі електронні адреси + Виявні електронні адреси Недійсна відповідь виявлення домашнього сервера - Налаштуйте свою видимість. + Налаштуйте свою виявність. Видимість Не вдалося встановити зв’язок у режимі реального часу. \nПопросіть адміністратора вашого домашнього сервера налаштувати сервер TURN для надійної роботи викликів. @@ -1316,7 +1320,7 @@ Дізнатись більше Безпека Безпека та приватність - Ми раді повідомити, що змінили назву! Ваш застосунок оновлено й ви ввійшли у свій обліковий запис. + Ми раді повідомити вас, що ми змінили назву! Ваш застосунок оновлено й ви увійшли у свій обліковий запис. Зміну параля ще не завершено. \n \nЗупинити змінювання пароля\? @@ -1325,7 +1329,7 @@ Резервне копіювання розпочато Неочікувана помилка Ключ відновлення - Створення ключа відновлення за допомогою парольної фрази, цей процес може тривати кілька секунд. + Створення відновлювального ключа за допомогою парольної фрази, цей процес може тривати кілька секунд. Поділитися ключем відновлення з… Будь ласка, створіть копію Стоп @@ -1341,9 +1345,9 @@ Зберегти ключ відновлення Я створив копію Готово - Зберігайте ключ відновлення десь дуже надійно, наприклад, у менеджері паролів (або сейфі) - Ваш ключ відновлення — це мережа безпеки — ви можете використовувати його для відновлення доступу до ваших зашифрованих повідомлень, якщо ви забудете свою парольну фразу. -\nТримайте ключ відновлення десь дуже надійно, наприклад, у менеджері паролів (або сейфі) + Тримайте відновлювальний ключ у якомусь дуже надійному місці, наприклад, у менеджері паролів (або сейфі) + Ваш відновлювальний ключ — це мережа безпеки — ви можете використовувати його для відновлення доступу до ваших зашифрованих повідомлень, якщо ви забудете свою парольну фразу. +\nТримайте відновлювальний ключ у якомусь дуже надійному місці, наприклад, у менеджері паролів (або сейфі) Створюється резервна копія ключів. Успішно ! (Додатково) Налаштування за допомогою ключа відновлення @@ -1371,7 +1375,7 @@ Запит на розподіл ключів Поділитися Перевірити - Неперевірений сеанс запитує ключі шифрування. + Незвірений сеанс запитує ключі шифрування. \nНазва сеансу: %1$s \nОстанні відвідини: %2$s \nЯкщо ви не ввійшли в інший сеанс, знехтуйте цим запитом. @@ -1380,7 +1384,7 @@ \nОстанні відвідини: %2$s \nЯкщо ви не ввійшли в інший сеанс, знехтуйте цим запитом. Для продовження потрібно прийняти Умови користування цією службою. - Немає активних віджетів + Немає активних знадобів Керувати інтеграціями Менеджер інтеграції не налаштовано. Читати захищені DRM засоби масової інформації @@ -1388,25 +1392,25 @@ Використовувати камеру Заблокувати все Дозволити - Цей віджет хоче використовувати такі ресурси: + Цей знадіб хоче використовувати такі ресурси: На жаль, конференц-дзвінки з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 6.0) Ідентифікатор кімнати - Ідентифікатор віджета + Ідентифікатор знадобу Ваша тема Ваш ідентифікатор користувача URL-адреса зображення профілю Ваше видиме ім\'я Скасувати доступ для мене Відкрити в браузері - Перезавантажити віджет - Не вдалося завантажити віджет. + Перезавантажити знадіб + Не вдалося завантажити знадіб. \n%s Використання може спричинити обмін даними з %s: За його використання може бути встановлено файли cookie та відбуватися обмін даними з %s: - Цей віджет додав: - Завантажити віджет - Віджет - Активні віджети + Цей знадіб додав: + Завантажити знадіб + Знадіб + Активні знадоби ПОДАННЯ %1$s: %2$s %3$s %1$s: %2$s @@ -1512,8 +1516,8 @@ Включає події запрошення/приєднання/виходу/видалення/заборони та зміни зображень профілю/видимих імен. Показати стан подій учасників кімнати Керування криптографічними ключами - Використовуйте Менеджер інтеграції для керування ботами, мостами, віджетами та пакетами наклейок. -\nМенеджери інтеграції отримують дані конфігурації та можуть змінювати віджети, надсилати запрошення до кімнати та надавати права від вашого імені. + Використовуйте Менеджер інтеграції для керування ботами, мостами, знадобами та пакунками наліпок. +\nМенеджери інтеграції отримують дані конфігурації та можуть змінювати знадоби, надсилати запрошення до кімнати та надавати права від вашого імені. Інтеграції %d секунда @@ -1607,14 +1611,14 @@ \nЩоб запобігти їх повторному приєднанню, замість цього слід заблокувати їх. Причина викидання Викинути користувача - Дійсно скасувати запрошення для цього користувача\? + Ви впевнені, що бажаєте скасувати запрошення для цього користувача\? Скасувати запрошення Зняття ігнорування з цього користувача знову покаже всі повідомлення від нього. - Не ігнорувати користувача - Ігнорування цього користувача призведе до видалення його повідомлень з кімнат, якими ви ділитесь. + Рознехтувати користувача + Нехтування цього користувача призведе до видалення його повідомлень з усіх кімнат, де ви обидва є учасниками. \n \nВи можете будь-коли змінити цю дію в загальних налаштуваннях. - Ігнорувати користувача + Нехтувати користувача Понизити Ви не зможете скасувати цю зміну, оскільки понижуєте свої права, якщо ви останній привілейований користувач у кімнаті, неможливо буде повернути собі привілеї. Понизитися\? @@ -1637,8 +1641,8 @@ Лише згадки Всі повідомлення Всі повідомлення (гучно) - Ігнорувати користувача - Перестати ігнорувати + Нехтувати користувача + Рознехтувати Підпис Алгоритм Версія @@ -1673,7 +1677,7 @@ Налаштування захисту Захистіть доступ PIN-кодом та біометричними даними. Захист доступу - Цей сеанс довірений для безпечного обміну повідомленнями, оскільки ви його підтвердили: + Цей сеанс довірений для безпечного обміну повідомленнями, оскільки ви його звірили: %d активний сеанс %d активні сеанси @@ -1687,7 +1691,7 @@ Потрібна повторна автентифікація Для виконання цієї дії ${app_name} вимагає ввести свої облікові дані. Якщо скасувати, ви не зможете читати зашифровані повідомлення на новому пристрої, а інші користувачі не довірятимуть йому - Якщо скасувати, ви не зможете читати зашифровані повідомлення на цьому пристрої, а інші користувачі довірятимуть йому + Якщо скасувати, ви не зможете читати зашифровані повідомлення на цьому пристрої, а інші користувачі не довірятимуть йому Керування сеансами Просимо зачекати… Перевірка входу @@ -1697,9 +1701,361 @@ Повідомлення тут не захищено наскрізним шифруванням. Увімкнути наскрізне шифрування… Повідомлення в цій кімнаті наскрізно зашифровані. - Перевірити цей сеанс + Звірити цей сеанс Перевірте цей сеанс, підтвердивши, що на екрані партнера з’являються такі цифри Перевірте цей сеанс, підтвердивши, що на екрані партнера з’являються ці емоджі - Перевірте цей сеанс, щоб позначити його надійним. Довірені сеанси партнерів дають вам додаткову впевненість під час використання наскрізно зашифрованих повідомлень. - Перевірте цей сеанс, щоб позначити його надійним та надати йому доступ до зашифрованих повідомлень. Якщо ви не входили в цей сеанс, ваш обліковий запис може бути зламано: + Звірте цей сеанс, щоб позначити його надійним. Довірені сеанси партнерів дають вам додаткову впевненість під час використання наскрізно зашифрованих повідомлень. + Звірте цей сеанс, щоб позначити його надійним та надати йому доступ до зашифрованих повідомлень. Якщо ви не входили в цей сеанс, ваш обліковий запис може бути зламано: + Назва або ID (#example:matrix.org) + Назва + Фільтрувати за іменем користувача або ID… + Це початок цієї розмови. + Назва кімнати + Показувати такі подробиці, як назви кімнат та вміст повідомлень. + Тема кімнати (необов\'язково) + Тема + Ви впевнені, що хочете вилучити (видалити) цю подію\? Зауважте, що, якщо ви видалите назву кімнати або зміните тему, це може скасувати зміну. + %s щоб люди знали, про що ця кімната. + Додати тему + "Тема: " + Назва кімнати + Тема + Відкрити умови %s + Завантаження інших доступних мов… + Інші доступні мови + Поточна мова + Поділіться цим кодом з людьми, щоб вони змогли сканувати його, щоб додати вас і почати спілкуватися в чаті. + Мій код + Поділитися моїм кодом + Сканувати QR-код + Ми не можемо запросити користувачів. Перевірте користувачів, яких ви хочете запросити та повторіть спробу. + + Запрошення надіслано для %1$s та ще одній особі + Запрошення надіслано для %1$s та ще %2$d особам + Запрошення надіслано для %1$s та ще %2$d осіб + Запрошення надіслано для %1$s та ще %2$d осіб + + + Одна особа + %1$d особи + %1$d осіб + %1$d осіб + + Більше + ДОКЛАДНІШЕ + Повідомлення в цій кімнаті захищені наскрізним шифруванням. Дізнайтеся більше та підтвердьте користувачів у їхньому профілі. + Змінювати тему + Оновлювати кімнату + Надсилати події m.room.server_acl + Змінювати дозволи + Змінювати назву кімнати + Змінювати видимість історії + Вмикати шифрування кімнати + Змінювати основну адресу кімнати + Змінювати аватар кімнати + Змінювати знадоби + Сповіщати всіх + Вилучати повідомлення, надіслані іншими + Блокувати користувачів + Викидати користувачів + Змінювати налаштування + Запрошувати користувачів + Надсилати повідомлення + Типова роль + У вас немає дозволу на оновлення ролей, необхідних для зміни різних частин кімнати + Виберіть ролі, необхідні для зміни різних частин кімнати + Дозволи + Перегляд та оновлення ролей, необхідних для зміни різних частин кімнати. + Залишити кімнату + Залишити кімнату + Залишити + Залишити поточну конференцію та перейти до іншої\? + Це не загальнодоступна кімната. Ви не зможете знову приєднатися без запрошення. + У вас немає дозволу на ввімкнення шифрування в цій кімнаті. + Дозволи кімнати + Ознайомтеся з непрочитаними повідомленнями тут + Ви впевнені, що бажаєте видалити усі не надіслані повідомлення з цієї кімнати\? + Посилання %1$s спрямовує вас на інший сайт: %2$s. +\n +\nВи впевнені, що бажаєте продовжити\? + Ви вийшли + Вилучити… + Наліпка + Використовувати ботів, мости, знадоби та пакунки наліпок + Зв\'язок із сервером втрачено + Не знайдено жодної правки + Історія правок + Рівень довіри + Не довірений + Довірений + Шукайте зелений щит аби переконатись, що користувач є довіреним. Довіртесь усім користувачам у кімнаті аби переконатись, що кімната є безпечною та захищеною. + Для максимальної безпеки використовуйте інші довірені засоби зв\'язку або робіть це під час особистої зустрічі. + Не довірений вхід + Звірення цього сеансу позначить його довіреним для вас і для партнера. + Задля максимальної безпеки ми радимо зробити це під час особистої зустрічі або з використанням інших довірених засобів зв\'язку. + Підтвердьте вашу тотожність, звіривши цей вхід з одного з ваших інших сеансів та надавши йому доступ до зашифрованих повідомлень. + Звірте усі свої сеанси, щоб переконатись у безпечності вашого облікового запису та повідомлень + Сеанси + Не вдалось отримати сеанси + Ви не маєте доступу до цього повідомлення, бо відправник не довіряє вашому сеансу + Позначити довіреним + Зашифроване незвіреним пристроєм + Цей сеанс є довіреним для безпечного обміну повідомленням тому що його було звірено %1$s (%2$s): + Звірено + Ваш новий сеанс тепер звірений. В нього є доступ до ваших зашифрованих повідомлень, а інші користувачі бачитимуть його, як довірений. + Звірено %s + Захищені повідомлення з цим користувачем є наскрізно зашифрованими та є непрочитними для сторонніх осіб. + Ви успішно звірили цей сеанс. + Звірено! + Резервна копія має недійсний підпис з незвіреного сеансу %s + Резервна копія має недійсний підпис зі звіреного сеансу %s + Резервна копія має дійсний підпис з незвіреного сеансу %s + Резервна копія має дійсний підпис зі звіреного сеансу %s. + СТВОРИТИ + На вміст надіслано скаргу + Надіслано скаргу, як на спам + Надіслано скаргу, як на неприйнятне + Забагато помилок, вам довелось вийти + Незашифроване + надсилає сніг ❄️ + надсилає конфетті 🎉 + Надсилає вказане повідомлення зі снігом + Надсилає вказане повідомлення з конфетті + + Показати пристрій, з якого ви можете звірити цей сеанс просто зараз + Показати %d пристрої, з яких ви можете звірити цей сеанс просто зараз + Показати %d пристроїв, з яких ви можете звірити цей сеанс просто зараз + Показати %d пристроїв, з яких ви можете звірити цей сеанс просто зараз + + Ви розпочнете знову, але без історії повідомлень, без довірених пристроїв та користувачів + Вдавайтесь до цього лише за умов відсутності жодного пристрою, з якого ви можете звірити поточний пристрій. + Якщо ви скинете все + Скинути все + Резервна копія не може бути дешифрована цією парольною фразою: переконайтесь, що відновлювальна парольна фраза зазначена правильно. + Не знаєте вашої відновлювальної парольної фрази\? Ви можете %s. + Відновлювальна парольна фраза + Забули або втратили усі можливості для відновлення\? Скинути все + Використати файл + Скористатись відновлювальними парольною фразою або ключем + Скористатись відновлювальними парольною фразою або ключем + Використовуйте найостаннішій ${app_name} на ваших інших пристроях, ${app_name} Web, ${app_name} для комп\'ютерів, ${app_name} iOS, ${app_name} для Android, або будь-який інший, здатний до перехресного підписування, Matrix-клієнт + Використовуйте найостаннішій ${app_name} на ваших інших пристроях: + Якщо ви не можете доступитись до чинного сеансу + Використайте чинний сеанс, щоб звірити цей сеанс, таким чином надавши йому доступ до зашифрованих повідомлень. + Підтвердьте вашу тотожність, звіривши цей вхід та надавши йому доступ до зашифрованих повідомлень. + Звірте новий вхід, що доступається до вашого облікового запису: %1$s + Звірте цей вхід + Очікування… + Торкніться, щоб переглянути та звірити + Новий вхід. Це були ви\? + Оновити + Скинути ключі + або будь-який інший, здатний до перехресного підписування, Matrix-клієнт + Перехресне підписування не ввімкнене + Перехресне підписування увімкнено. +\nКлючі не є довіреними + Перехресне підписування увімкнено +\nКлючі є довіреними. +\nЗакриті ключі невідомі + Перехресне підписування увімкнено +\nЗакриті ключі на пристрої. + Перехресне підписування + %s приєднується. + Шифрування увімкнено + Ви прийняли запрошення для %1$s. Причина: %2$s + %1$s приймає запрошення для %2$s. Причина: %3$s + Ви відкликали запрошення для %1$s приєднатись до кімнати. Причина: %2$s + %1$s відкликає запрошення для %2$s приєднатись до кімнати. Причина: %3$s + %1$s надсилає %2$s запрошення приєднатись до кімнати. Причина: %3$s + Ви надіслали %1$s запрошення приєднатись до кімнати. Причина: %2$s + Ви заблокували %1$s. Причина: %2$s + %1$s заблоковано %2$s. Причина: %3$s + Ви розблокували %1$s. Причина: %2$s + %1$s розблоковує %2$s. Причина: %3$s + Ви викинули %1$s. Причина: %2$s + %1$s викидає %2$s. Причина: %3$s + Ви берете участь в цьому виклику зараз + Це початок вашої історії листування з %s. + Повідомлення тут є наскрізно зашифрованими. +\n +\nВаші повідомлення захищені замками, тож лише ви та отримувачі мають унікальні ключі для їхнього відмикання. + Повідомлення тут є наскрізно зашифрованими. +\n +\nВаші повідомлення захищені замками, тож лише ви та отримувачі мають унікальні ключі для їхнього відмикання. + Це початок %s. + %1$s відхиляє цей виклик + Ви відхилили цей виклик %1$s + %1$s розпочинає виклик + Ви розпочали виклик + Швидкі реакції + Користувачі + Під час переадресації трапилась помилка + Переадресувати + З\'єднати + Перетелефонувати + Відновити + Клавіатура + Ви утримали виклик + %s утримали виклик + Утримати + + Призупинений виклик + %1$d призупинені виклики + %1$d призупинених викликів + %1$d призупинених викликів + + Поточний виклик (%1$s) + + 1 поточний виклик (%1$s) · 1 призупинений виклик + 1 поточний виклик (%1$s) · %2$d призупинені виклики + 1 поточний виклик (%1$s) · %2$d призупинених викликів + 1 поточний виклик (%1$s) · %2$d призупинених викликів + + Змінити мережу + Змінити + Додати за matrix ID + Push-сповіщення вимкнено + Не вдалось розблокувати користувача + Блокування від %1$s + Відкликати запрошення для %1$s\? + Відкликати запрошення + Введіть ваш %s, щоб продовжити. + Підтвердити %s + Згенерувати ключ повідомлення + Зазначити %s + Пароль облікового запису + Ключ повідомлення + Подобається + Гаразд + Реакції + Ви приєднались. + Запрошування користувачів… + Переглянути умови + Умови використання + Переглянути історію редагувань + Входження до кімнати… + Запрошення отримано від %s + Запрошення надіслано до %1$s та %2$s + Запрошення надіслано до %1$s + Згоду користувача не було надано. + Використовувати %1$s + Початкова синхронізація: +\nЗвантаження даних… + Початкова синхронізація: +\nОчікування відповіді сервера… + Запросити користувачів + ЗАПРОСИТИ + Розпочніть друкувати, щоб отримати результати + Нещодавні + Відомі користувачі + Пропозиції + Контакти + Пошук контактів у Matrix + Контакти + Отримання контактів… + QR-код не зіскановано! + QR-код не дійсний (недійсний URI)! + Цей QR-код не дійсний + Чекаємо на %s… + Майже все! Чекаємо на підтвердження… + Майже все! Чи показує інший пристрій такий самий щит\? + Ні + Так + Майже все! Чи показує %s такий самий щит\? + QR-код + Зображення QR-коду + QR-код + Пошук за іменем або ID + Додати за QR-кодом + Очікування + Зазначте адресу сервера ідентифікації + В іншому разі, ви можете зазначити адресу будь-якого іншого сервера ідентифікації + Ваш домашній сервер (%1$s) пропонує використовувати %2$s як ваш сервер ідентифікації + Спочатку погодьтесь з умовами користування сервером ідентифікації в налаштуваннях. + Спочатку налаштуйте сервер ідентифікації. + Цей сервер ідентифікації є застарілим. ${app_name} підтримує лише API V2. + Від\'єднатись від сервера ідентифікації %s\? + Погодьтесь з умовами використання сервера ідентифікації (%s), щоб дозволити вашу виявність за електронною адресою та номером телефону. + Ви не нехтуєте жодних користувачів + Скаргу на неприйнятний вміст було надіслано. +\n +\nЯкщо ви більше не бажаєте бачити жодного вмісту від цього користувача, ви можете знехтувати його, щоб приховати його повідомлення. + Скаргу на спам було надіслано. +\n +\nЯкщо ви більше не бажаєте бачити жодного вмісту від цього користувача, ви можете знехтувати його, щоб приховати його повідомлення. + Скаргу на вміст було надіслано. +\n +\nЯкщо ви більше не бажаєте бачити жодного вмісту від цього користувача, ви можете знехтувати його, щоб приховати його повідомлення. + ЗНЕХТУВАТИ КОРИСТУВАЧА + Ви зараз оприлюднюєте електронні адреси та номери телефонів для сервера ідентифікації %1$s. Вам необхідно буде перепід\'єднатись до %2$s, щоб припинити оприлюднення. + Увімкнути детальні звіти. + Розблокувати історію + Надіслані лютострусом детальні звіти допоможуть розробникам отримати більше інформації. Навіть якщо цю функцію увімкнено, застосунок не зберігає ані вмісту повідомлень, ані будь-якої іншої особистої інформації. + Користувач видалив подію через %1$s + Подію видалено користувачем + Інша причина… + Неприйнятний вміст + Спам + Поскаржитись на цей вміст + ПОСКАРЖИТИСЬ + Причина скарги + Зреагували: %s + Зреагувати + Завершити + Відповісти + Редагувати + Зрозуміло + Riot тепер називається Element! + Файл %1$s було звантажено! + Звантаження файлу %1$s… + Надсилання файлу (%1$s / %2$s) + Шифрування файлу… + Струс виявлено! + Потрясіть вашим пристроєм, щоб перевірити поріг чутливості + Поріг чутливості + Лютоcтрус + Режим розробника вмикає приховані функції та може зробити застосунок менш стабільним. Лише для розробників! + Відкликати згоду + Надати згоду + Обраний вами сервер ідентифікації не має жодних умов використання. Продовжуйте лише якщо довіряєте власнику сервісу + Сервер ідентифікації не має умов використання + Зазначте, будь ласка, адресу сервера ідентифікації + Неможливо під\'єднатись до сервера ідентифікації + Зазначте адресу сервера ідентифікації + Чи погоджуєтесь ви надіслати дані ваших контактів (номери телефонів та/або електронні адреси) на налаштований сервер ідентифікації(%1$s) задля виявлення відомих вам наявних контактів\? +\n +\nДля поліпшення приватності дані буде захешовано перед надсиланням. + Ласкаво просимо! + Повторити + Від\'єднання від вашого сервера ідентифікації означатиме, що ви не будете виявними для інших користувачів та не зможете запрошувати інших через електронну пошту або номер телефону. + Ви наразі не використовуєте жодного сервера ідентифікації. Для того, щоб виявляти інших та бути виявним для знайомих вам наявних контактів, налаштуйте такий сервер нижче. + Ви зараз використовуєте %1$s для того, щоб виявляти інших та бути виявним для знайомих вам наявних контактів. + Змінити сервер ідентифікації + Налаштувати сервер ідентифікації + Від\'єднати сервер ідентифікації + Сервер ідентифікації + Бути виявним для інших + Жодного сервера ідентифікації не налаштовано. Вам необхідно скинути ваш пароль. + Ви не використовуєте жодного сервера ідентифікації + Криптографічна інформація недоступна + Обмеження невідомі. + Ваш домашній сервер дозволяє надсилати файли розміром до %s. + Обмеження сервера на розмір файлів + Версія сервера + Назва сервера + Встановити новий пароль облікового запису… + Попередній перегляд відкритих кімнат поки що не підтримується в ${app_name} + (відредаговано) + Востаннє відредаговано %1$s %2$s + Цей виклик було завершено + Новий вхід + Увійти + Увійти + Увійти знову + Увійти з Matrix ID + Увійти з Matrix ID + Увійти знову + Увійти + Увійти до %1$s + Увійти через %s \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index e40e95da82..82ec8fc36d 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -25,8 +25,8 @@ 所有聊天室成员。 任何人。 未知(%s)。 - %1$s 开启了端到端加密(%2$s) - %1$s 请求了一次 VoIP 会议 + %1$s 开启了端对端加密(%2$s) + %1$s 请求了 VoIP 会议 VoIP 会议已开始 VoIP 会议已结束 (头像也被更改) @@ -43,7 +43,7 @@ 手机号码 %1$s 撤回了对 %2$s 的邀请 %1$s 让未来的聊天室历史记录对 %2$s 可见 - %1$s 更新了他的个人档案 %2$s + %1$s 更新了他的资料 %2$s %1$s 向 %2$s 发送了加入聊天室的邀请 %1$s 接受了 %2$s 的邀请 无法撤回 @@ -104,8 +104,8 @@ %1$s 为此聊天室移除了主地址。 %1$s 已允许访客加入聊天室。 %1$s 已禁止访客加入聊天室。 - %1$s 已开启端到端加密。 - %1$s 已开启端到端加密(无法识别的演算法 %2$s)。 + %1$s 已开启端对端加密。 + %1$s 已开启端对端加密(无法识别的演算法 %2$s)。 %1$s 创建了这个聊天室 您发送了一张图片。 您发送了一张贴纸。 @@ -134,23 +134,23 @@ 您接听了通话。 您结束了通话。 您已让未来的聊天室记录对 %1$s 可见 - 您开启了端到端加密(%1$s) + 您开启了端对端加密(%1$s) 您升级了此聊天室。 您请求了 VoIP 会议 您移除了聊天室名称 您移除了聊天室主题 %1$s 移除了聊天室头像 您移除了聊天室头像 - 您更新了您的个人档案 %1$s + 您更新了您的资料 %1$s 您向 %1$s 发送了加入聊天室的邀请 您已撤回了对 %1$s 加入聊天室的邀请 您接受了 %1$s 的邀请 - %1$s 添加了 %2$s 小部件 - 您添加了 %1$s 小部件 - %1$s 移除了 %2$s 小部件 - 您移除了 %1$s 小部件 - %1$s 修改了 %2$s 小部件 - 您修改了 %1$s 小部件 + %1$s 添加了 %2$s 挂件 + 您添加了 %1$s 挂件 + %1$s 移除了 %2$s 挂件 + 您移除了 %1$s 挂件 + %1$s 修改了 %2$s 挂件 + 您修改了 %1$s 挂件 管理员 审核员 默认 @@ -182,8 +182,8 @@ 您移除了此聊天室的主地址。 您已允许访客加入聊天室。 您已禁止访客加入聊天室。 - 您已开启端到端加密。 - 您已开启端到端加密(无法识别的算法 %1$s)。 + 您已开启端对端加密。 + 您已开启端对端加密(无法识别的算法 %1$s)。 您已离开。理由:%1$s %1$s 已离开。理由:%2$s 您已加入。理由:%1$s @@ -268,7 +268,7 @@ 进度(%s%%) 主服务器 URL 身份服务器 URL - 登入 + 登录 注册 提交 跳过 @@ -298,7 +298,7 @@ 主服务器: 身份服务器: 我已验证了我的电子邮箱地址 - 要重置您的密码,请输入与您的帐号关联的电子邮箱地址: + 要重置您的密码,请输入与您的账号关联的电子邮箱地址: 必须输入与您账号关联的电子邮箱地址。 必须输入新密码。 URL 必须以 http[s]:// 开头 @@ -395,11 +395,11 @@ %d 秒 通话已连接 通话正在连接… - 为了发送或保存附件,${app_name} 需要访问您的图片和视频库。 + 为发送或保存附件,${app_name} 需要权限以访问您的图片和视频库。 \n -\n请在接下来弹出的窗口中授权允许访问,以便应用能够从您的手机发送文件。 - 为了拍照或进行视频通话,${app_name} 需要访问您的相机。 - 为了进行语音通话,${app_name} 需要访问您的麦克风。 +\n请在接下来的弹出窗口中授权允许访问,以便从此设备中发送文件。 + ${app_name} 需要权限来访问您的相机,以拍摄照片或进行视频通话。 + ${app_name} 需要权限以访问您的麦克风来进行语音通话。 您试图访问聊天室 %s。您是否愿意加入这个聊天室? 管理工具 私聊 @@ -421,8 +421,8 @@ 本地联系人(%d 个) %1$s 和 %2$s 正在输入… %1$s 和 %2$s 及其他人正在输入… - 发送一条加密消息… - 发送一条消息(未加密)… + 发送加密消息… + 发送消息(未加密)… 与服务器的连接已断开。 消息未发送。现在 %1$s 还是 %2$s? 因为未知设备的存在,消息未发送。现在 %1$s 还是 %2$s? @@ -455,7 +455,7 @@ 隐私政策 手机号码 应用信息 - 启用这个账户的通知 + 启用这个账号的通知 启用这个设备的通知 来自私聊的消息 来自群聊的消息 @@ -497,7 +497,7 @@ 只有成员(从他们被邀请开始) 只有成员(从他们加入开始) 只有被邀请的人 - 除游客之外任何知道这个聊天室链接的人 + 任何知道这个聊天室链接的人,游客除外 任何知道这个聊天室链接的人,包括游客 被封禁的用户 高级 @@ -505,30 +505,30 @@ 地址 这些是实验性功能,可能会出现不可预料的错误。请谨慎使用。 端对端加密 - 您需要注销来启用加密。 - 在此聊天室中、使用本设备时,从不向未验证的设备发送加密消息。 + 您需要注销以启用加密。 + 对于当前会话,从在不此聊天室中向未验证的设备发送加密消息。 这个聊天室没有本地地址 - 新地址(如 #foo:matrix.org) + 新地址(例如 #foo:matrix.org) 别名格式无效 “%s” 不是有效的别名格式 这个聊天室已启用加密。 这个聊天室已禁用加密。 - 启用加密 -\n(警告:无法再禁用!) + 启用加密 +\n(警告:启用后无法禁用!) 目录 端对端加密信息 事件信息 用户 ID - 导出端到端聊天室密钥 + 导出端对端聊天室密钥 导出聊天室密钥 导出密钥到本地文件 导出 - 输入密码 - 确认密码 - 端到端聊天室密钥已经被保存到“%s”。 + 输入密语 + 确认密语 + 端对端聊天室密钥已经被保存到“%s”。 \n \n注意:如果应用被卸载,此文件可能将会被移除。 - 导入端到端聊天室密钥 + 导入端对端聊天室密钥 导入聊天室密钥 从本地文件导入密钥 仅向已验证的设备发送加密消息 @@ -557,9 +557,9 @@ 问题反馈发送失败(%s) 阅读 无效令牌 - api存在之前,不支持同时通过电子邮件和电话号码进行注册。我们只考虑电话号码。 + 在新的 API 出现之前,尚不支持同时使用电子邮件和电话号码注册,所以只有电话号码会被记录。 \n -\n您可以在设置中添加您的电子邮件到您的个人资料。 +\n在设置中,您可以在个人资料里添加您的电子邮件。 用户名已被使用 无法注册:电子邮箱所有权验证失败 无法识别指定的访问令牌 @@ -582,20 +582,20 @@ " \n \n请在接下来弹出的窗口中授权允许访问。" - ${app_name}需要许可才能访问您的摄像机和麦克风来执行视频通话。 + ${app_name} 需要权限以访问您的摄像机和麦克风来进行视频通话。 \n -\n请在接下来弹出的窗口中授权允许访问。 +\n请在接下来的弹出窗口中授权允许访问,以便进行通话。 对不起。因为权限不足,操作已取消 保存至下载? 移除 - 此邀请已发送至未与此账户关联的 %s。 -\n您可能希望用一个不同的账户登录,或者把这个电子邮箱加入到你的账户。 + 此邀请已发送至未与此账号关联的 %s。 +\n您可能希望用一个不同的账号登录,或者把这个电子邮箱加入到你的账号。 这是此聊天室的预览。与聊天室的交互已禁用。 通话 您将不能撤销这个修改,因为您正在让这个用户和您拥有相同的特权级别。 \n您确定吗? - 这可能意味着有人在恶意劫持您的通讯,或者您的手机不信任远程服务器的数字证书。 - 如果服务器管理员说这是正常的,请确保以下的指纹与管理员提供的指纹相符。 + 这可能意味着有人正在恶意劫持您的流量,或者您的手机不信任远程服务器提供的数字证书。 + 如果服务器管理员说这是预期的情况,请确保下面的指纹与管理员提供的指纹相匹配。 报告这个内容的原因 目录 邀请 @@ -644,17 +644,15 @@ 发送至 已读标签清单 发送为 - ${app_name} 需要访问您的通讯录,才能根据电子邮箱地址和手机号码查找其他 Matrix 用户。 - -请在接下来的弹出窗口中授权允许访问。 - ${app_name}可以检查您的通讯录,以根据电子邮件和电话号码找到其他Matrix用户。 + ${app_name} 可以检查您的通讯录,并基于他们的邮箱地址和电话号码,来查找其他 Matrix 用户。若您同意本应用以此目的访问您的通讯录,请在接下来的弹出窗口中授权允许访问。 + ${app_name} 可以检查您的通讯录,并基于他们的邮箱地址和电话号码,来查找其他 Matrix 用户。 \n -\n你同意为此目的分享你的通讯录吗\? +\n您是否同意本应用以此目的访问您的通讯录\? 空闲 仅 Matrix 用户 - 您的手机信任的证书已被更改。这非常反常。建议您不要接受此新证书。 - 证书已从以前受信任的更改为不受信任的证书。服务器可能已更新其证书。请联系管理员并核对服务器的指纹。 - 请仅在服务器管理员已经发布了与上述指纹相匹配的指纹的情况下接受该证书。 + 证书已从一个先前受您的设备信任的证书更改为另一个。这非常反常!建议您不要接受此新证书。 + 证书已从曾受信任的证书更改为不受信任的证书。服务器可能已更新其证书,请联系管理员并核对服务器的指纹。 + 请仅在服务器管理员发布了与上述指纹匹配的指纹的情况下接受该证书。 成员 ID 的格式不正确。应该是一个电子邮箱地址或 Matrix ID,如 “@localpart:domain” 联系人 @@ -671,13 +669,13 @@ 正在等待验证 代码 访问和可见性 - 聊天室访问 + 聊天室访问权限 实验室 端对端加密已激活 %s 已尝试在这个聊天室的时间线上加载一个特定的时间点,但无法找到它。 公开名称 为验证此设备是否可信,请通过其他方式(例如面对面交换或拨打电话)与其拥有者联系,并询问他们该设备的用户设置中的密钥是否与以下密钥匹配: - 如果它们不匹配,您通信的安全性可能会受到影响。 + 如果它们不匹配,您通讯的安全性可能会受到影响。 这个聊天室包含未经验证的未知设备。 \n这意味着无法保证该设备属于其声称的用户。 \n我们建议您在继续操作之前,先验证每个设备,但如果您愿意也可以不验证而重新发送消息。 @@ -760,12 +758,12 @@ 黑色主题 通知声音 使用12小时制显示时间戳 - 您需要权限来管理这个聊天室的小部件 - 创建小部件失败 + 您需要权限来管理这个聊天室的挂件 + 创建挂件失败 用 jitsi 创建会议通话 - 您确定要删除这个小部件吗? + 您确定要删除这个挂件吗? - 无法创建小部件。 + 无法创建挂件。 发送请求失败。 特权级别必须是正整数。 您不在这个聊天室。 @@ -841,8 +839,8 @@ %d 位成员 - 无效的社区 ID - “%s” 不是一个有效的社区 ID + 社区 ID 无效 + “%s” 不是有效的社区 ID %d 条未读消息 @@ -859,7 +857,7 @@ %d 个聊天室 - 已启用 %d 个小部件 + 已启用 %d 个挂件 社区名称 @@ -893,29 +891,33 @@ • 通知通过 Firebase Cloud Messaging 发送 • 通知只含有元数据 • 通知不会显示消息内容 - 新的社区ID(如 +foo:matrix.org) + 新的社区 ID(如 +foo:matrix.org) 社区管理员没有提供这个社区的具体描述。 标准 低隐私模式 - 停用账户 - 停用我的账户 + 停用账号 + 停用我的账号 发送统计分析数据 ${app_name} 会收集匿名统计数据来帮助我们改进程序。 请允许资料分析以帮助我们改进 ${app_name}。 是的,我愿意帮助! - 停用账户 - 这将使您的账户永远不再可用。您将不能登录,或使用相同的用户 ID 重新注册。您的账户将退出所有已加入的聊天室,身份服务器上的账户信息也会被删除。此操作是不可逆的。 停用您的账户不会默认忘记您发送的消息。如果您希望我们忘记您发送的消息,请勾选下面的选择框。 Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意味着您发送的消息不会被发给新注册或未注册的用户,但是已收到您的消息的注册用户依旧可以看到他们的副本。 - 请在我停用账户的同时忘记我发送的所有消息(警告:这将导致未来的用户看到残缺的对话) + 停用账号 + 这将使您的账号永远不再可用。您将无法登录,也不能使用相同的用户 ID 重新注册。您的账号将退出所有已加入的聊天室,您在身份服务器上的账号信息也会被删除。此操作是不可逆的。 +\n +\n停用您的账号不会默认忘记您已发送的消息。如果您希望我们忘记您发送的消息,请勾选下面的选择框。 +\n +\nMatrix 中的消息可见性类似于电子邮件。我们忘记您的消息意味着您发送的消息不会被发给新注册或未注册的用户,但是已收到您的消息的注册用户依旧可以看到这些消息的副本。 + 请在我停用账号的同时忘记我发送的所有消息(警告:这将导致未来的用户看到残缺的对话) 请输入您的密码以继续: - 停用账户 + 停用账号 发送贴纸 发送贴纸 您目前没有启用任何贴纸包。 \n \n要添加一些吗? - • 通知中的消息内容从 Matrix 主服务器直接安全的获取 + • 通知中的消息内容直接从 Matrix 主服务器安全地获取 • 通知含有消息与元数据 - 这个聊天室不会显示任何社区的徽章 + 此聊天室不会显示任何社区徽章 缺少所需的参数。 无效参数。 样例 ID @@ -936,20 +938,20 @@ 请输入您的密码。 发言 如果可能的话,请使用英文撰写问题描述。 - 发送加密的回复… + 发送加密回复… 发送回复(未加密)… 发送前预览媒体文件 使用回车键发送消息 显示动作 - 依照 ID 封禁用户 - 依照 ID 解禁用户 + 按照 ID 封禁用户 + 按照 ID 解禁用户 设置用户的权限等级 - 依照 ID 取消用户管理员权限 - 依照 ID 邀请用户进入当前聊天室 - 依照别名加入聊天室 + 按照 ID 取消用户管理员权限 + 按照 ID 邀请用户进入当前聊天室 + 按照别名加入聊天室 离开聊天室 设置聊天室主题 - 依照 ID 踢出用户 + 按照 ID 踢出用户 更改您显示的昵称 打开/关闭 markdown 修复 Matrix Apps 管理 @@ -1021,10 +1023,10 @@ 通知已在系统设置中禁用。 \n请检查系统设置。 打开设置 - 帐号设置。 - 您的帐号已启用通知。 - 您的账户已禁用通知。 -\n请检查账户设置。 + 账号设置。 + 您的账号已启用通知。 + 您的账号已禁用通知。 +\n请检查账号设置。 启用 设备设置。 已为此设备启用通知。 @@ -1056,7 +1058,7 @@ 启用开机时启动 检查后台限制 电池优化 - 若主服务器支持本功能,在聊天时预览链接内容。 + 若主服务器支持此功能,在聊天中预览链接内容。 发送正在输入通知 让聊天室中的其他用户知道您正在输入。 Markdown 格式化 @@ -1065,10 +1067,10 @@ 点击已阅回执以显示所有已经阅读过某条消息的用户。 显示加入与离开事件 邀请、移除与封禁事件不受影响。 - 显示账户变动事件 + 显示账号变动事件 包括头像与显示名称的变动。 后台连接 - ${app_name}需要保持低影响的后台连接,以便获得可靠的通知。 + ${app_name} 需要保持低影响的后台连接,以便获得可靠的通知。 \n下一个屏幕中,系统将提示您允许 ${app_name} 始终在后台运行,请点击“允许“。 授予权限 在验证您的电子邮件地址时发生了一个错误。 @@ -1089,8 +1091,8 @@ 您的主服务器尚未支持延迟加载聊天室成员,请稍候再试。 通过仅载入最近聊天中出现的聊天室成员来提升性能。 延迟加载聊天室成员 - 已停用 Markdown。 - 已启用 Markdown。 + Markdown 已禁用。 + Markdown 已启用。 视频通话中… 自动重启通知服务 服务被停止,并已自动重启。 @@ -1106,23 +1108,23 @@ ${app_name} 未被电池优化影响。 如果设备在未充电的情况下关屏静置一段时间,其将进入打盹模式(Doze)。这将阻止应用访问网络并延后其运行、同步、与响铃。 忽略电池优化 - 请输入用于加密密钥备份的密码。您在恢复此备份时需要使用此密码。 - 创建密码 - 密码必须对应 + 请输入用于加密被导出密钥的密语。恢复此备份时,必须输入相同的密语才能导入密钥。 + 创建密语 + 密语必须对应 指令 %s 需要更多参数,或者有些参数不正确。 没有可用的 Google Play Services APK。消息通知可能不能正常工作。 密钥备份 使用备份密钥 密钥备份尚未完成,请等待… - 如果您此时登出账号,您将会失去您的已加密信息 - 密钥备份进行中。如果您此时登出账号将无法再访问您的已加密信息。 - 您的所有设备都应当启用安全密钥备份以确保您不会失去您的已加密信息的访问权。 - 我不想要我的已加密信息 - 密钥备份中… + 如果您此时登出账号,您将会失去您的已加密消息 + 密钥备份进行中。如果您此时登出账号将无法再访问您的已加密消息。 + 您的所有设备都应当启用安全密钥备份以确保您不会失去您的已加密消息的访问权。 + 我不想要我的已加密消息 + 正在备份密钥… 使用备份密钥 确定吗? 备份 - 如果您在登出账号之前不备份密钥,您将失去您的已加密信息的访问权。 + 如果您在登出账号之前不备份密钥,您将失去您的已加密消息的访问权。 留下 跳过 完成 @@ -1148,34 +1150,34 @@ 选择指示灯颜色,震动,铃声… 加密密钥管理 省流量模式使用了特定的过滤器,所以状态更新和输入状态通知将会被过滤掉。 - 已加密信息恢复 + 恢复已加密消息 管理密钥备份 静音 请输入一个用户名。 - 请输入密码 - 密码太弱 - 如果您想要 ${app_name} 生成一个恢复密钥,请删除密码。 + 请输入密语 + 密语太弱了 + 如果您想要 ${app_name} 生成一个恢复密钥,请删除密语。 没有可用的 Matrix 会话 - 已加密信息永不丢失 + 永不丢失已加密消息 加密聊天室中的信息会被端对端加密以确保安全。只有您和拥有密钥的接收方可以读取这些信息。 \n \n安全地备份您的密钥以免丢失信息。 开始使用备份密钥 (高级) 手动导出密钥 - 使用密码以保护您的备份。 - 我们将会在主服务器上保存一份您的密钥的加密拷贝。设置一个密码来保护您的备份的安全。 + 使用密语保护您的备份。 + 我们将会在主服务器上为您的密钥保存一份加密拷贝。设置一个密语来保护您的备份的安全。 \n -\n为了最大的安全性,这个密码应当与您的账号密码不同。 - 设置密码 - 备份创建中 +\n为了最大的安全性,此密语应当与您的账号密码不同。 + 设置密语 + 正在创建备份 或者用一个恢复密钥来保护您的备份,将其保存到另一个安全的地方。 (高级)设置一个恢复密钥 成功! 正在备份您的密钥。 - 您的恢复密钥是一张安全网 - 如果您忘记了密码您可以利用它重获您的已加密信息的访问权。 -\n请将您的恢复密钥保存在一个非常安全的地方,比如密码管理器中 (或保险箱里) - 将您的恢复密钥保存在一个非常安全的地方,比如密码管理器中 (或保险箱里) + 您的恢复密钥是一张安全网——如果您忘记了密语,您可以利用它重获您的已加密消息的访问权。 +\n请将您的恢复密钥保存在一个非常安全的地方,比如密码管理器中(或保险箱里) + 将您的恢复密钥保存在一个非常安全的地方,比如密码管理器中(或保险箱里) 完成 我已经制作了一份拷贝 保存恢复密钥 @@ -1185,8 +1187,8 @@ \n \n警告:如果应用被卸载,此文件可能会被删除。 请制作一份拷贝 - 分享恢复密钥 … - 用密码来生成恢复密钥,此过程可能会花费几秒钟。 + 分享恢复密钥… + 正在使用密语来生成恢复密钥,此过程可能会花费几秒钟。 恢复密钥 意外错误 备份开始 @@ -1194,19 +1196,19 @@ 您确定吗? 如果您登出账号或者丢失此设备,您可能再也无法访问您的信息。 正在获取备份的版本 … - 使用恢复密码解锁您的已加密历史消息 + 使用恢复密语解锁您的已加密历史消息 使用您的恢复密钥 - 不知道您的恢复密码,您可以 %s 。 + 如果不知道您的恢复密语,您可以 %s。 使用恢复密钥解锁您的已加密历史消息 输入恢复密钥 - 信息恢复 + 消息恢复 丢失了恢复密钥?您可以在设置中新建一个。 - 无法使用此密码解密备份:请检查您输入的恢复密码是否正确。 + 无法使用此密语解密备份:请检查您输入的恢复密语是否正确。 网络错误:请检查您的网络连接并重试。 - 备份恢复中: - 恢复密钥计算中 … - 密钥下载中 … - 密钥导入中 … + 正在恢复备份: + 正在计算恢复密钥… + 正在下载密钥… + 正在导入密钥… 解锁历史 请输入恢复密钥 无法使用此恢复密钥解密备份:请检查您输入的恢复密钥是否正确。 @@ -1231,8 +1233,8 @@ 备份具有已验证设备 %s 的无效签名 备份具有未验证设备 %s 的无效签名 无法获得备份 (%s)的信任信息。 - 要在此设备上使用密钥备份,请立即使用密码或恢复密钥进行恢复。 - 备份删除中 … + 要在此会话中使用密钥备份,请立即使用密语或恢复密钥进行恢复。 + 正在删除备份… 备份(%s)删除失败 删除备份 要从此服务器中删除您备份的加密密钥吗?您将无法再使用恢复密钥来读取加密的历史消息。 @@ -1241,22 +1243,22 @@ \n \n如果您并未设置新的恢复方法,可能是有攻击者试图侵入您的账号。请立即更改您的账号密码并在设置中设定一个新的恢复方法。 那是我 - 永不丢失已加密信息 + 永不丢失已加密消息 开始使用备份密钥 - 永不丢失已加密信息 + 永不丢失已加密消息 使用备份密钥 新加密信息密钥 管理密钥备份 - 密钥备份中 … + 正在备份密钥… 所有密钥都已备份 - %d 个密钥备份中 … + 正在备份 %d 个密钥… 版本 算法 签名 忽略 - 以单点登录方式登入 + 以单点登录方式登录 无法连接到此 URL,请检查 您的设备使用了过时的 TLS 安全协议,容易受到攻击,为保证安全,您将无法进行连接 按回车发送消息 @@ -1374,17 +1376,17 @@ 拒绝 没有设置身份服务器。 服务器的错误配置导致通话失败 - 请要求您的家庭服务器 (%1$s) 的管理员配置 TURN 服务器,以使通话可靠地工作。 + 请要求您的主服务器 (%1$s) 的管理员配置 TURN 服务器,以使通话可靠地工作。 \n \n或者,您可以尝试使用 %2$s 的公共服务器,但这将不那么可靠,并且它将与该服务器共享您的 IP 地址。您也可以在“设置”中进行管理。 尝试使用 %s 不要再问我 - 设置用于帐户恢复的电子邮件,然后就可以让认识您的人选择性探索到您。 + 设置用于恢复账号的电子邮件,然后就可以让认识您的人选择性探索到您。 设定电话,然后就可以让认识您的人选择性探索到您。 - 设定电子邮件以供帐号复原。 然后就可以让认识您的人用电子邮件或电话选择性探索到您。 - 设定电子邮件以供帐号复原。 然后就可以让认识您的人用电子邮件或电话选择性探索到您。 + 设定电子邮件以供恢复账号。然后就可以让认识您的人用电子邮件或电话选择性探索到您。 + 设定电子邮件以供恢复账号。然后就可以让认识您的人用电子邮件或电话选择性探索到您。 這不是有效的 Matrix 服务器位置 - 无法在此 URL 找到家庭服务器,请检查 + 无法在此 URL 找到主服务器,请检查 允许后备呼叫协助服务器 播放 暂停 @@ -1394,7 +1396,7 @@ 通知 ${app_name} 呼叫失败 无法建立实时连接。 -\n请要求您的家庭服务器管理员配置 TURN 服务器以使通话可靠工作。 +\n请要求您的主服务器管理员配置 TURN 服务器以使通话可靠工作。 选择声音设备 电话 扬声器 @@ -1407,7 +1409,7 @@ 打开 HD SSL 错误:尚未验证对等端身份。 SSL 错误。 - 当您的家庭服务器未提供时将使用 %s 作为辅助(在通话时将分享您的 IP 地址) + 当您的主服务器未提供时将使用 %s 作为辅助(在通话时将分享您的 IP 地址) 活动通话 (%s) 返回通话 在您的设置中添加身份服务器以执行此操作。 @@ -1456,7 +1458,7 @@ 设置安全备份 重置安全备份 在此设备上设置 - 通过在您的服务器上备份加密密钥保障加密消息和数据的访问权。 + 通过在您的服务器上备份加密密钥,防止失去对加密信息和数据的访问。 为您已有的备份生成新的安全密钥或设置新的安全口令。 这将替换您的当前密钥或短语。 发现 @@ -1468,30 +1470,30 @@ %d 个封禁用户 - 公开名称(对通信参与者可见) - 会话的公开名称对通信的参与者可见 + 公开名称(对与您通讯的人可见) + 会话的公开名称对与您通讯的人可见 成功导出密钥 %1$s: %2$s %1$s: %2$s %3$s 查看 - 活动小部件 - 小部件 - 载入小部件 - 此小部件添加者: + 活动挂件 + 挂件 + 载入挂件 + 此挂件添加者: 使用它会设置 cookie 并与 %s 分享数据: 使用它会与 %s 分享数据: - 无法载入小部件。 + 无法载入挂件。 \n%s - 重载小部件 + 重载挂件 在浏览器中打开 撤消我的访问权限 您的昵称 您的头像 URL 您的用户 ID 您的主题 - 小部件 ID + 挂件 ID 聊天室 ID - 小部件想使用以下资源: + 挂件想使用以下资源: 允许 阻止全部 使用相机 @@ -1500,18 +1502,18 @@ 未配置集成管理器。 若要继续请接受服务条款。 恢复密钥已保存。 - 您的家庭服务器上已存在备份 + 您的主服务器上已存在备份 您似乎已在另一个会话中设置密钥备份。您想要将其替换为正在创建的吗? 安全备份 保护加密信息及数据的访问权 设置安全备份 由于无效或过期的凭据您已登出。 - 验证会话已将其标记为可信。当使用端到端加密消息时信任参与者的会话将给您额外的内心平静。 - 验证会话将标记其为可信,同时将您的会话对对方标记为可信。 + 验证此会话以将其标记为可信任。当使用端对端加密消息时,信任参与者的会话可以使您更加安心。 + 验证会话将标记其为可信任,同时将您的会话对对方标记为可信任。 通过确认以下表情符号出现在对方的屏幕上来验证此会话 通过确认屏幕上对方显示以下数字来验证此会话 您收到传入验证请求。 - 与此用户的安全消息端到端加密,无法被第三方读取。 + 与此用户的安全消息端对端加密,无法被第三方读取。 对方取消了验证。 \n%s 验证已取消。 @@ -1528,7 +1530,7 @@ 用户不匹配 您未使用身份服务器 未配置身份服务器,需要重置您的密码。 - 您似乎正在试图连接到另一个家庭服务器。您想要登出吗? + 您似乎正在试图连接到另一个主服务器。您想要登出吗? 加入一个聊天室开始使用应用。 您已经跟上了! 您没有未读消息 @@ -1551,7 +1553,7 @@ 将此聊天室发布到聊天室目录 获取信任信息时发生错误 获取密钥备份数据时发生错误 - 从文件 \"%1$s\" 导入端到端密钥。 + 从文件 \"%1$s\" 导入端对端密钥。 其他第三方通知 您已经在查看此聊天室! 注册令牌 @@ -1577,7 +1579,7 @@ 发送新私聊消息 查看聊天室目录 名称或 ID (#example:matrix.org) - 在时间线中启用滑动回复 + 启用在时间线中滑动回复 在主屏幕上添加未读通知选项卡。 链接已复制到剪贴板 通过 matrix ID 添加 @@ -1590,7 +1592,7 @@ 服务条款 审核条款 可被其他人发现 - 使用机器人,小部件和贴纸包 + 使用机器人,挂件和贴纸包 已读于 身份服务器 断开身份服务器 @@ -1617,7 +1619,7 @@ 同意身份服务器 (%s) 服务条款使您可以通过电子邮件地址或电话号码被发现。 启用详细日志。 当您发送 RageShake 时详细日志将帮助开发者提供更多日志。即使启用,应用也不会记录消息内容或任何其他私有数据。 - 接收您的家庭服务器条款和条件后请重试。 + 接收您的主服务器条款和条件后请重试。 服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。 请稍后再试。 发送附件 打开导航菜单 @@ -1645,10 +1647,10 @@ 贴纸 无法处理共享数据 媒体 - 此聊天室中无媒体 + 此聊天室中暂无媒体 文件 %1$s 于 %2$s - 此聊天室中无文件 + 此聊天室中暂无文件 垃圾信息 不合适的内容 自定义报告…… @@ -1668,9 +1670,9 @@ 此内容已报告为不合适。 \n \n如果您不希望再看到此用户的更多内容,您可以忽略他们以隐藏他们的消息。 - ${app_name} 需要权限在磁盘上保存您的端到端密钥。 + ${app_name} 需要权限以在磁盘上保存您的端对端密钥。 \n -\n请在下个弹窗中允许访问以便手动导出密钥。 +\n请在接下来的弹出窗口中授权允许访问,以便您手动导出密钥。 目前没有网络连接 忽略用户 全部消息(嘈杂) @@ -1696,7 +1698,7 @@ 扩展 & 自定义您的体验 开始吧 选择服务器 - 就像电子邮件,账户有一个家,尽管您可以与任何人聊天 + 就像电子邮件,账号有一个家,尽管您可以与任何人聊天 在最大的公共服务器上免费加入数百万用户 面向组织的高级托管 了解更多 @@ -1706,9 +1708,9 @@ 连接到 %1$s 连接到 Element Matrix 服务 连接到自定义服务器 - 登入 %1$s + 登录 %1$s 注册 - 登入 + 登录 使用单点登录继续 Element Matrix 服务地址 地址 @@ -1716,23 +1718,23 @@ 输入 Modular Element 或您想使用的服务器地址 输入您想使用的服务器的地址 载入页面时出错:%1$s (%2$d) - 应用无法登录到此家庭服务器。家庭服务器支持以下登录类型:%1$s。 + 应用无法登录到此主服务器。主服务器支持以下登录类型:%1$s。 \n \n您想要通过网页客户端登录吗? - 抱歉,此服务器不接受新账户。 - 应用无法在此服务器上创建账户。 -\n + 抱歉,此服务器不接受新账号。 + 应用无法在此服务器上创建账号。 +\n \n您想要通过网页客户端注册吗? - 电子邮件未关联到任何账户。 + 电子邮件未关联到任何账号。 在 %1$s 上重置密码 验证邮件将发送到您的收件箱以确认设置您的新密码。 下一个 电子邮件 新密码 注意! - 更改您的密码将重置所有会话上的端到端加密密钥,从而使加密聊天记录无法读取。在重设密码之前,请设置“密钥备份”或从另一个会话中导出聊天室密钥。 + 更改您的密码将重置所有会话上的端对端加密密钥,从而使加密聊天记录无法读取。在重设密码之前,请设置“密钥备份”或从另一个会话中导出聊天室密钥。 继续 - 电子邮件未链接到任何账户 + 电子邮件未链接到任何账号 检查您的收件箱 验证电子邮件已发送到 %1$s。 点击链接以确认您的新密码。跟随包含的链接验证后,请点击下方。 @@ -1746,7 +1748,7 @@ \n \n是否中止密码更改过程? 设置电子邮件地址 - 设置电子邮件用于恢复您的帐户。之后,您可以选择允许您认识的人通过电子邮件发现您。 + 设置电子邮件用于恢复您的账号。之后,您可以选择允许您认识的人通过电子邮件发现您。 电子邮件 电子邮件(可选) 下一个 @@ -1770,31 +1772,31 @@ 下一个 用户名已占用 注意 - 您的帐户尚未创建。 + 您的账号尚未创建。 \n \n是否中止注册过程? 选择 matrix.org 选择 Element Matrix Services - 选择自定义家庭服务器 + 选择自定义主服务器 请进行人机验证 接受条款以继续 请检查您的电子邮件 我们向 %1$s 发送了电子邮件。 -\n请点击其中包含的链接继续账户创建。 +\n请点击其中包含的链接继续账号创建。 输入的验证码不正确。请检查。 - 过时的家庭服务器 - 此家庭服务器运行的版本过旧以至于无法连接。要求您的家庭服务器管理员升级。 + 过时的主服务器 + 此主服务器运行的版本过旧以至于无法连接。要求您的主服务器管理员升级。 发送了太多请求。您可以在 %1$d 秒后重试… - 或者,如果您已经拥有账户并知道您的 Matrix 标识符和密码,您可以使用这种方式: + 或者,如果您已经拥有账号并知道您的 Matrix 标识符和密码,您可以使用这种方式: 使用 Matrix ID 登录 使用 Matrix ID 登录 - 如果您在家庭服务器上设置了账户,在下方使用您的 Matrix ID(例 @user:domain.com)和密码。 + 如果您在主服务器上设置了账号,在下方使用您的 Matrix ID(例 @user:domain.com)和密码。 Matrix ID 如果您不知道您的密码,返回并重置。 这不是一个有效的用户标识符。期望的格式:\'@user:homeserver.org\' - 无法找到有效的家庭服务器。请检查您的标识符 + 无法找到有效的主服务器。请检查您的标识符 您已登出 这可能由于多种原因: \n @@ -1803,25 +1805,25 @@ \n• 您已从其他会话删除了此会话。 \n \n• 您的服务器管理员出于安全原因已取消您的访问权限。 - 重新登入 + 重新登录 您已登出 - 登入 - 您的家庭服务器 (%1$s) 管理员将您从您的账户 %2$s (%3$s) 登出。 + 登录 + 您的主服务器 (%1$s) 管理员将您从您的账号 %2$s (%3$s) 登出。 登录以恢复仅存储在此设备上的加密密钥。 您需要使用它们在任何设备上阅读所有安全消息。 - 登入 + 登录 密码 清除个人数据 注意:您的个人数据(包括加密密钥)仍存储在此设备上。 \n -\n如果您不再使用此设备,或想登入另一个帐户,请清除它。 +\n如果您不再使用此设备,或想登录另一个账号,请清除它。 清除全部数据 清除数据 是否清除当前存储在此设备上的全部数据? -\n再次登入以访问您的帐户数据和消息。 - 除非您登入以恢复加密密钥,否则您将无法访问安全消息。 +\n再次登录以访问您的账号数据和消息。 + 除非您登录以恢复加密密钥,否则您将无法访问安全消息。 清除数据 当前会话用于用户 %1$s 而您提供了用户 %2$s 的凭证。${app_name} 不支持此功能。 -\n请先清除数据,然后重新登入另一个账户。 +\n请先清除数据,然后重新登录另一个账号。 您的 matrix.to 链接更是不正确 描述太短 初始同步… @@ -1841,19 +1843,19 @@ 发生意外错误时,${app_name} 可能更经常崩溃 在明文消息前添加 ¯\\_(ツ)_/¯ 启用加密 - 一旦启用,加密无法禁用。 + 加密一经启用,便无法禁用。 您的电子邮件域无权注册此服务器 - 不可信登入 + 未信任的登录 匹配 不匹配 确认下方独特表情以相同顺序出现在他们的屏幕上,以验证此用户。 - 为了获得最高的安全性,请使用其他可信通信方式或亲自确认。 - 寻找绿色盾牌以确保用户可信。信任聊天室中的所有用户以确保聊天室的安全。 + 为了获得最高的安全性,请使用其他可信任的通讯方式,或者当面确认。 + 寻找绿色盾牌以确保用户可信任。信任聊天室中的所有用户以确保聊天室的安全。 不安全 以下其中一项可能会受到威胁: \n -\n - 您的家庭服务器 -\n - 您验证的用户连接到的家庭服务器 +\n - 您的主服务器 +\n - 您验证的用户连接到的主服务器 \n - 您或其它用户的网络连接 \n - 您或其他用户的设备 视频。 @@ -1885,10 +1887,10 @@ 为了提高安全性,请通过检查两个设备上的一次性代码来验证 %s。 \n \n为了获得最大的安全性,请亲自进行。 - 此聊天室的消息不是端到端加密。 - 该聊天室的消息端到端加密。 + 此聊天室的消息未经端对端加密。 + 该聊天室的消息已被端对端加密。 \n -\n您的消息使用锁进行保护,只有您和接收者才能使用唯一的密钥解锁。 +\n您的消息受加密保护,并且只有您和消息接收者拥有唯一解密密钥。 安全 了解更多 更多 @@ -1902,16 +1904,16 @@ 离开聊天室 正在离开聊天室… 管理员 - 审查员 + 协管员 自定义 邀请 用户 %1$s 管理员 - %1$s 审查员 + %1$s 的协管员 %1$s 默认权限 %2$s 自定义权限 (%1$d) - ${app_name} 无法处理 \'%1$s\' 类型事件 - ${app_name} 无法处理 \'%1$s\' 类型消息 + ${app_name} 无法处理类型为 \'%1$s\' 的事件 + ${app_name} 无法处理类型为 \'%1$s\' 的消息 ${app_name} 在渲染 id 为 \'%1$s\' 的事件内容时遇到了一个问题 取消忽略 该会话无法与您的其他会话共享此验证。 @@ -1922,34 +1924,34 @@ 发送彩虹色给定表情 时间线 消息编辑器 - 启用端到端加密… - 一旦启用,加密无法禁用。 + 启用端对端加密… + 加密一经启用,便无法禁用。 是否启用加密? - 启用后,将无法禁用聊天室加密。服务器无法看到加密聊天室中发送的消息,只有聊天室的参与者才能看到。启用加密可能会阻止许多机器人和桥接正常工作。 + 聊天室加密一经启用,便无法禁用。在加密聊天室中,发送的消息无法被服务器看到,只能被聊天室的参与者看到。启用加密可能会使许多机器人和桥接无法正常运作。 启用加密 - 为保证安全,通过检查一次性代码验证 %s。 - 为保证安全,亲自或使用其他通信方式验证。 + 为保证安全,请核对一次性代码以验证 %s。 + 为保证安全,请当面验证,或者使用其他通讯方式验证。 比较独特表情,确保它们以相同顺序出现。 与其他用户设备上显示的代码比较。 - 与此用户的消息端到端加密,无法被第三方读取。 - 您的新会话已验证。它可以访问您的加密消息,其他用户会将其视为可信。 + 与此用户的消息端对端加密,无法被第三方读取。 + 您的新会话已验证。它可以访问您的加密消息,其他用户会将其视为可信任。 交叉签名 交叉签名已启用 \n设备上的私钥。 交叉签名已启用 -\n密钥可信。 +\n密钥可信任。 \n私钥未知 交叉签名已启用。 -\n密钥不可信 +\n密钥未信任 交叉签名未启用 - 您的服务器管理员已默认禁用私有聊天室和私聊消息端到端加密。 + 您的服务器管理员已默认禁用私有聊天室和私聊消息端对端加密。 激活会话 显示全部会话 管理会话 登出此会话 加密信息不可用 此会话对安全消息可信因为您已验证它: - 验证此会话以将其标记为可信,并授予其访问加密消息的权限。如果您未登录此会话,则您的帐户可能已被盗: + 验证此会话以将其标记为可信,并授予其访问加密消息的权限。如果您未登录此会话,则您的账号可能已被盗: %d 个活动会话 @@ -1962,10 +1964,10 @@ 注意 无法获取会话 会话 - 可信 - 不可信 - 此会话对加密消息可信,%1$s (%2$s) 已验证它: - %1$s (%2$s) 使用新会话登入: + 可信任 + 未信任 + 此会话可信任,可以用于收发加密消息,因为 %1$s(%2$s)已验证了它: + %1$s (%2$s) 使用新会话登录: 在此用户信任此会话之前,发送到该会话和从该会话发送的消息均标有警告。或者,您可以手动进行验证。 初始化交叉签名 重置密钥 @@ -1976,7 +1978,7 @@ 到服务器的连接已丢失 飞行模式已打开 开发工具 - 账户数据 + 账号数据 %d 票 @@ -1985,13 +1987,13 @@ 已选选项 创建简单调查 - 使用恢复短语或密钥 + 使用恢复密语或密钥 如果您无法访问已有会话 - 新登入 + 新登录 无法在存储中找到秘密 - 输入秘密存储密码 - 注意: - 您应仅在可信设备上访问秘密存储 + 输入秘密存储密语 + 警告: + 您应仅在可信任的设备上访问秘密存储 移除… 您想要发送此附件到 %1$s 吗? @@ -2010,29 +2012,29 @@ 轻按以审核和验证 使用此会话验证新的会话,授权访问加密消息。 这不是我 - 您的账户可能已被盗用 + 您的账号可能已被盗用 如果您取消,您将无法在此设备上读取加密消息,其他用户不会信任它 如果您取消,您将无法在新设备上读取加密消息,其他用户不会信任它 如果您现在取消将不会验证 %1$s (%2$s)。在他们的用户个人档案中重新开始。 以下其中一项可能有风险: \n \n- 您的密码 -\n- 您的家庭服务器 +\n- 您的主服务器 \n- 此设备或其它设备 \n- 设备使用的网络连接 \n \n我们推荐您在设置中立即更换您的密码和恢复密钥。 通过设置验证您的设备。 验证已取消 - 恢复密码 + 恢复密语 消息密钥 - 账户密码 - 设置一个 %s + 账号密码 + 设置 %s 生成消息密钥 确认 %s 输入您的 %s 以继续。 再次输入您的 %s 确认。 - 不要使用您的账户密码。 + 不要使用您的账号密码。 输入只有你知道的安全口令,用于保护服务器上的秘密。 这可能会花费数秒,请耐心等待。 设置恢复。 @@ -2042,7 +2044,7 @@ 完成 使用此 %1$s 作为安全网以防您忘记您的 %2$s。 发布创建的身份密钥 - 从密码生成安全密钥 + 从密语生成安全密钥 正在定义 SSSS 默认密钥 正在同步主密钥 正在同步用户密钥 @@ -2059,14 +2061,14 @@ 跳至已读回执 事件被聊天室管理员调整,理由:%1$s 密钥已是最新! - 保护与解锁已加密信息并信任%s。 + 保护与解锁已加密消息并信任 %s。 保存到优盘或者备份盘 复制到您的个人云存储 您无法在移动设备上执行此操作 - 设置恢复密码让您能够保护和解锁加密信息并信任设备。 + 设置恢复密语让您能够保护和解锁加密信息并信任设备。 \n \n如果您不希望设置文本密码,那么生成密钥亦可。 - 设置恢复密码让您能够保护和解锁加密信息并信任设备。 + 设置恢复密语让您能够保护和解锁加密信息并信任设备。 如果您现在取消,那么当您失去登录权限时也会丢失加密的信息和数据。 \n \n您也可以通过设置菜单来建立保护备份以及管理您的密钥。 @@ -2089,7 +2091,7 @@ 按事件设置通知重要性 以纯文本形式发送消息,而不将其解释为 markdown 用户名和/或密码不正确。输入的密码以空格开头或结尾,请检查。 - 此帐户已停用。 + 此账号已停用。 消息… 加密升级可用 启用交叉签名 @@ -2097,27 +2099,27 @@ 输入您的 %s 以继续 使用文件 输入 %s - 恢复密码 + 恢复密语 无效的恢复密钥 请输入恢复密钥 检查备份密钥 检查备份密钥 (%s) 获取曲线密钥 - 从密码生成 SSSS 密钥 - 从密码生成 SSSS 密钥 (%s) + 从密语生成 SSSS 密钥 + 从密语生成 SSSS 密钥(%s) 从恢复密钥生成 SSSS 密钥 正在在 SSSS 中保存密钥备份秘密 %1$s (%2$s) - 输入您的密钥备份密码以继续。 + 输入您的密钥备份密语以继续。 使用您的密钥备份恢复密钥 - 不知道您的密钥备份密码,您可以 %s。 + 不知道您的密钥备份密语,您可以 %s。 密钥备份恢复密钥 阻止应用内屏幕截图 启用此设置添加 FLAG_SECURE 到所有活动。重启应用使更改生效。 媒体文件已添加到相册 无法添加媒体文件到相册 无法保存媒体文件 - 选择新的账户密码… + 选择新的账号密码… 在你的其他设备上使用最新的${app_name} 网页版、${app_name} 桌面版、${app_name} iOS 版、${app_name} 安卓版,或其他能够交叉签名的 Matrix 客户端 ${app_name} Web \n${app_name} Desktop @@ -2134,14 +2136,14 @@ 访问安全存储失败 未加密 由未验证设备加密 - 查看您的登入位置 - 验证您的全部会话确保您的账户和消息安全 - 验证访问您的账户的新登录:%1$s + 查看您的登录位置 + 验证您的全部会话确保您的账号和消息安全 + 验证访问您的账号的新登录:%1$s 使用文本手动验证 验证登录 使用表情交互式验证 通过从您的其他会话验证此登录确认您的身份,授权它访问您的加密消息。 - 标记为可信 + 标记为信任 请选择用户名。 请选择密码。 仔细检查此链接 @@ -2165,13 +2167,13 @@ 打开 %s 条款 是否从身份服务器 %s 断开? 身份服务器已过期。${app_name} 仅支持 API V2。 - 无法执行此操作。家庭服务器已过期。 + 无法执行此操作。主服务器已过期。 请先配置身份服务器。 请先在设置中接受身份服务器的条款。 为了您的隐私,${app_name} 仅支持发送用户电子邮件和电话号码的哈希值。 关联失败。 - 此标识符无当前关联。 - 您的家庭服务器 (%1$s) 建议使用 %2$s 作为您的身份服务器 + 目前与此标识符没有关联。 + 您的主服务器(%1$s)建议使用 %2$s 作为您的身份服务器 使用 %1$s 或者,您可以输入任何其他身份服务器 URL 输入身份服务器 URL @@ -2185,14 +2187,14 @@ 启动相机 设置安全备份 安全备份 - 通过在您的服务器上备份加密密钥防止丢失对加密消息和数据的访问。 + 通过在您的服务器上备份加密密钥,防止失去对加密信息和数据的访问。 设置 使用安全密钥 生成安全密钥存储在安全的地方如密码管理器或保险箱。 使用安全口令 输入仅有您知道的安全口令,生成备份用的密钥。 保存您的安全密钥 - 将您的安全密钥存储在安全的地方如密码管理器或保险箱。 + 将您的安全密钥存储在安全的地方,例如密码管理器或保险箱。 设置安全口令 输入只有您知道的安全口令,用于保护您的服务器上的秘密。 安全口令 @@ -2205,19 +2207,19 @@ 您无法访问此消息 正在等待此消息,可能会花费一些时间 无法解密 - 由于端到端加密,您可能需要等待某人的消息到达因为加密密钥未正确发送给您。 + 由于端对端加密,您可能需要等待某人的消息到达因为加密密钥未正确发送给您。 您无法访问此消息因为您已屏蔽此发送者 - 您无法访问此消息因为您的会话不被发送者信任 + 您无法访问此消息,因为您的会话不被发送者信任 您无法访问此消息因为发送者有意不发送密钥 正在等待加密历史 - Riot 现在是 Element! - 我们兴奋地宣布我们改名了!您的应用已经是最新的并且您已登入您的帐户。 + Riot 现已成为 Element! + 我们很高兴地宣布我们改名了!您的应用已经更新到最新版本,并且您已登录您的账号。 明白了 了解更多 将恢复密钥保存到 - 从我的电话簿添加 - 您的电话簿是空的 - 电话簿 + 从我的通讯录添加 + 您的通讯录是空的 + 通讯录 搜索我的联系人 正在获取您的联系人… 您的通讯录是空的 @@ -2247,14 +2249,14 @@ 发起音频会议 会议使用 Jitsi 安全与许可政策。您的会议进行期间当前聊天室内的所有人将看到加入邀请。 您无法呼叫您自己 - 您无法呼叫您自己,请等待参与者接受邀请 - 添加小部件失败 - 移除小部件失败 + 您无法与自己通话,请等待参与者接受邀请 + 添加挂件失败 + 移除挂件失败 成功导入 %1$d/%2$d 个密钥。 管理集成 - 无活动小部件 + 无活动挂件 聊天室已创建,但由于以下原因一些邀请尚未发送: \n \n%s @@ -2267,14 +2269,14 @@ 注意!登出前最后一次尝试! 错误次数过多,您已被登出 此电话号码已定义。 - 您的帐户尚未添加电话号码 + 您的账号尚未添加电话号码 电子邮件地址 - 您的账户尚未添加电子邮件 + 您的账号尚未添加电子邮件 电话号码 移除 %s? 请确认您已点击我们向您发送的电子邮件中的链接。 电子邮件和电话号码 - 管理链接到您的 Matrix 账户的电子邮件和电话号码 + 管理链接到您的 Matrix 账号的电子邮件和电话号码 代码 请使用国际格式(电话号码必须以 ‘+’ 开始) 通过验证此登录确认您的身份,授权它访问加密信息。 @@ -2290,7 +2292,7 @@ 机器人按钮 回应:%s 验证结果 - 是否删除类型 %1$s 的账户数据? + 是否删除类型 %1$s 的账号数据? \n \n小心使用,它可能导致意外行为。 链接格式不正确 @@ -2313,17 +2315,17 @@ 如果您重置一切 仅当没有其他设备可用来验证此设备时,才执行此操作。 重置一切 - 忘记或丢失了所有的回复选项?重置一切 + 忘记或丢失了所有的恢复选项?重置一切 您已加入。 %s 已加入。 - 此聊天室的消息是端到端加密的。 + 此聊天室的消息是端对端加密的。 离开 设置 - 此处的消息是端到端加密的, + 此处的消息已被端对端加密。 \n -\n您的消息已用锁保护并且只有您和收件人拥有唯一的钥匙解锁它们。 - 此处的消息不是端到端加密。 - 此家庭服务器正在运行较旧版本。要求您的家庭服务器管理员升级。您可以继续,但一些功能可能无法正确工作。 +\n您的消息受加密保护,并且只有您和消息接收者拥有唯一解密密钥。 + 此处的消息未经端对端加密。 + 此主服务器正在运行较旧版本。要求您的主服务器管理员升级。您可以继续,但一些功能可能无法正确工作。 您仅发出此邀请。 %1$s 仅发出此邀请。 在加密聊天室显示完整历史 @@ -2361,7 +2363,7 @@ 聊天室地址 查看和管理此聊天室的地址,以及它在聊天室目录中的可见性。 聊天室地址 - 聊天室访问 + 聊天室访问权限 改成谁都可以阅读历史只会应用于此聊天室未来的消息。已经存在的历史消息的可见性将不会改变。 发送密钥共享请求历史记录 取消发布 @@ -2423,7 +2425,8 @@ 聊天室话题(可选) 聊天室名称 此聊天室无法预览。您想加入吗? - 此聊天室当前不可访问。请稍候重试,或向聊天室管理员询问以检查您是否拥有访问权限。 + 目前无法访问此聊天室。 +\n请稍候重试,或询问聊天室管理员以确认您是否拥有访问权限。 无法获取当前聊天室目录可见性(%1$s)。 是否将此聊天室发布至 %1$s 的聊天室目录中? 取消发布此地址 @@ -2449,7 +2452,7 @@ 启用聊天室加密 更改聊天室主地址 更改聊天室头像 - 修改小部件 + 修改挂件 通知每个人 移除其他人发送的消息 封禁用户 @@ -2471,4 +2474,105 @@ 未验证,缺少有效验证凭证 返回 系统默认 + • 匹配 %s 的服务器已被屏蔽。 + • IP 地址匹配的服务器已被屏蔽。 + • IP 地址匹配的服务器已被允许。 + • 匹配 %s 的服务器已被屏蔽。 + • 匹配 %s 的服务器已被允许。 + 已勾选 + 已选中 + 活跃通话(%1$s) + + %1$d 暂停了通话 + + 需要重新验证 + 删除失败的消息 + 您确定要取消发送消息吗? + 消息发送失败 + 删除未发送的消息 + 您确定要删除此聊天室中所有未发送的消息吗? + 失败 + 状态事件已发送! + 事件已发送! + 事件格式错误 + 已发送 + 正在发送 + 事件内容 + 无内容 + 事件内容 + 类型 + 发送自定义状态事件 + 编辑内容 + 状态事件 + 发送状态事件 + 发送自定义事件 + 开发者工具 + 视频 + 验证失败 + 用户 + 回拨 + %1$s 发起了通话 + 您发起了通话 + + %d 个条目 + + 不是有效的 Matrix 二维码 + 扫描二维码 + 添加人员 + 邀请朋友 + 服务器版本 + 服务器名称 + 切换 + 初始化同步: +\n正在下载数据… + 初始化同步: +\n正在等待服务器响应… + 空聊天室(曾为 %s) + + %1$s,%2$s,%3$s 和 %4$d 位其他成员 + + %1$s,%2$s,%3$s 和 %4$s + %1$s,%2$s 和 %3$s + %1$s 发起了视频会议 + 您发起了视频会议 + %1$s 启用了视频会议 + 您启用了视频会议 + %1$s 修改了视频会议 + 您修改了视频会议 + 设置头像 + 聊天室设置 + 聊天室版本 + 放弃修改 + 拨号键盘 + Matrix 链接 + 转移 + 连接 + 删除头像 + 修改头像 + 图片 + 关闭 Emoji 选择器 + 打开 Emoji 选择器 + 可信信任等级 + 警告信任等级 + 默认信任等级 + 你修改了此聊天室的服务器访问控制列表。 + %s 修改了此聊天室的服务器访问控制列表。 + %s 为此聊天室设置了服务器访问控制列表。 + 您为此聊天室设置了服务器访问控制列表。 + 在 Matrix 上查找联系人 + 用户尚未同意条款。 + 分享此二维码,其他人扫描后即可添加您,并开始聊天。 + 我的二维码 + 分享我的二维码 + 消息类型缺失 + 检查聊天室状态 + 查看已读回执 + 关闭通知 + 无声通知 + 有声通知 + 此聊天室有未发送的草稿 + 有些消息未被发送 + 从文件中导入密钥 + 发生错误,消息未能发送 + 状态键 \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 842700b6eb..f9345b146f 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -2610,4 +2610,15 @@ \n正在下載資料…… 初始同步: \n正在等待伺服器回應…… + 您確定您想要刪除此聊天室中所有未傳送的訊息嗎? + 刪除未傳送的訊息 + 訊息傳送失敗 + 您想要取消傳送訊息嗎? + 刪除所有失敗的訊息 + 失敗 + 已傳送 + 正在傳送 + 顯示聊天室目錄中的所有聊天室,包含有明確內容的聊天室。 + 顯示帶有明確內容的聊天室 + 聊天室目錄 \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 6a3c60a021..1d39791ad8 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -88,9 +88,15 @@ + android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" + android:summary="@string/settings_show_join_leave_messages_summary" + android:title="@string/settings_show_join_leave_messages" /> + +