diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index d13e40248f..5ad39614b7 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -31,6 +31,7 @@ ssss sygnal threepid + unpublish unwedging diff --git a/AUTHORS.md b/AUTHORS.md index 4fb5b8c994..ad20133d83 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,7 +4,7 @@ A full developer contributors list can be found [here](https://github.com/vector Even if we try to be able to work on all the functionalities, we have more knowledge about what we have developed ourselves. -## Benoit: Android team leader +## [Benoit](https://github.com/bmarty): Android team leader [@benoit.marty:matrix.org](https://matrix.to/#/@benoit.marty:matrix.org) - Android team leader and project leader, Android developer, GitHub community manager. @@ -12,7 +12,7 @@ Even if we try to be able to work on all the functionalities, we have more knowl - Reviewing and polishing developed features, code quality manager, PRs reviewer, GitHub community manager. - Release manager on the Play Store -## François: Software architect +## [Ganfra](https://github.com/ganfra) (aka François): Software architect [@ganfra:matrix.org](https://matrix.to/#/@ganfra:matrix.org) - Software architect, Android developer @@ -20,12 +20,17 @@ Even if we try to be able to work on all the functionalities, we have more knowl - Work mainly on the global architecture of the project. - Specialist of the timeline, and lots of other features. -## Valere: Product manager, Android developer +## [Valere](https://github.com/BillCarsonFr): Product manager, Android developer [@valere35:matrix.org](https://matrix.to/#/@valere35:matrix.org) - Product manager, Android developer - Specialist on the crypto implementation. +## [Onuray](https://github.com/onurays): Android developer + +[@onurays:matrix.org](https://matrix.to/#/@onurays:matrix.org) +- Android developer + # Other contributors First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function. @@ -34,7 +39,7 @@ We do not forget all translators, for their work of translating Element into man Feel free to add your name below, when you contribute to the project! -Name | Matrix ID | GitHub ---------|---------------------|-------------------------------------- -gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower) - +Name | Matrix ID | GitHub +----------|-----------------------------|-------------------------------------- +gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower) +TR_SLimey | @tr_slimey:an-atom-in.space | [TR-SLimey](https://github.com/TR-SLimey) diff --git a/CHANGES.md b/CHANGES.md index 26418c75f1..b16a6690bc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,25 +1,73 @@ -Changes in Element 1.0.11 (2020-XX-XX) +Changes in Element 1.0.12 (2020-XX-XX) =================================================== Features ✨: - - + - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) + - Room setting: update join rules and guest access (#2442) Improvements 🙌: - - Open an existing DM instead of creating a new one (#2319) + - Add Setting Item to Change PIN (#2462) + - Improve room history visibility setting UX (#1579) Bugfix 🐛: - - Fix issue when updating the avatar of a room (new avatar vanishing) - - Discard change dialog displayed by mistake when avatar has been updated + - Double bottomsheet effect after verify with passphrase + - EditText cursor jumps to the start while typing fast (#2469) Translations 🗣: - SDK API changes ⚠️: - - AccountService now exposes suspendable function instead of using MatrixCallback (#2354). Note: We will incrementally migrate all the SDK API in a near future. + - Build 🧱: + - Upgrade some dependencies and Kotlin version + - Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable) + +Test: - +Other changes: + - Remove "Status.im" theme #2424 + +Changes in Element 1.0.11 (2020-11-27) +=================================================== + +Features ✨: + - Create DMs with users by scanning their QR code (#2025) + - Add Invite friends quick invite actions (#2348) + - Add friend by scanning QR code, show your code to friends (#2025) + +Improvements 🙌: + - New room creation tile with quick action (#2346) + - Open an existing DM instead of creating a new one (#2319) + - Use RoomMember instead of User in the context of a Room. + - Ask for explicit user consent to send their contact details to the identity server (#2375) + - Handle events of type "m.room.server_acl" (#890) + - Room creation form: add advanced section to disable federation (#1314) + - Move "Enable Encryption" from room setting screen to room profile screen (#2394) + - Home empty screens quick design update (#2347) + - Improve Invite user screen (seamless search for matrix ID) + +Bugfix 🐛: + - Fix crash on AttachmentViewer (#2365) + - Exclude yourself when decorating rooms which are direct or don't have more than 2 users (#2370) + - F-Droid version: ensure timeout of sync request can be more than 60 seconds (#2169) + - Fix issue when restoring draft after sharing (#2287) + - Fix issue when updating the avatar of a room (new avatar vanishing) + - Discard change dialog displayed by mistake when avatar has been updated + - Try to fix cropped image in timeline (#2126) + - Registration: annoying error message scares every new user when they add an email (#2391) + - Fix jitsi integration for those with non-vanilla dialler frameworks + - Update profile has no effect if user is in zero rooms + - Fix issues with matrix.to deep linking (#2349) + +SDK API changes ⚠️: + - AccountService now exposes suspendable function instead of using MatrixCallback (#2354). + Note: We will incrementally migrate all the SDK API in a near future (#2449) + +Test: + - Add `allScreensTest` to cover all screens of the app + Other changes: - Upgrade Realm dependency to 10.0.0 @@ -1033,5 +1081,8 @@ SDK API changes ⚠️: Build 🧱: - +Test: + - + Other changes: - diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 91ddd519df..59ba6c4500 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -66,7 +66,6 @@ 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.fragment:fragment:1.3.0-beta01" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation 'com.google.android.material:material:1.2.1' diff --git a/build.gradle b/build.gradle index 0c4b35b060..6dd61a720c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.10' - ext.kotlin_coroutines_version = "1.3.9" + ext.kotlin_version = '1.4.20' + ext.kotlin_coroutines_version = "1.4.1" repositories { google() jcenter() @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.google.gms:google-services:4.3.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' diff --git a/fastlane/metadata/android/ca/changelogs/40100100.txt b/fastlane/metadata/android/ca/changelogs/40100100.txt new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/40100100.txt @@ -0,0 +1 @@ +// TODO diff --git a/fastlane/metadata/android/ca/full_description.txt b/fastlane/metadata/android/ca/full_description.txt new file mode 100644 index 0000000000..b45a5488ea --- /dev/null +++ b/fastlane/metadata/android/ca/full_description.txt @@ -0,0 +1,30 @@ +Element és un nou tipus d'aplicació de missatgeria i col·laboració que: + +1. Et dóna a tu el control per preservar la teva privadesa +2. Et permet comunicar-te amb qualsevol persona de la xarxa Matrix i, fins i tot més enllà gràcies a integracions amb altres aplicacions com Slack +3. Et protegeix de la publicitat, l'obtenció no desitjada de dades i dels navegadors amb accés controlat +4. T'assegura a tu mitjançant l'encriptació d'extrem a extrem i amb signatures creuades per verificar els altres + +Element és completament diferent a les altres aplicacions de missatgeria i col·laboració ja que és descentralitzat i de codi obert. + +Element et deixa triar l'allotjament perquè disposis de privadesa, propietat i control de les teves dades i converses. Et dóna accés a una xarxa oberta perquè no et quedis únicament parlant amb els usuaris d'Element. + +Element pot fer tot això ja que opera sobre Matrix - l'estàndard per a les comunicacions obertes i descentralitzades. + +Element et dóna el control perquè et deixa escollir qui vols que allotgi les teves converses. Des de l'aplicació d'Element, pots triar l'allotjament de diferents maneres: + +1. Crea un compte gratuït al servidor públic de matrix.org allotjat pels desenvolupadors de Matrix o tria'n un entre els milers de servidors públics creats per voluntaris +2. Allotja tu mateix el teu compte en el teu propi servidor +3. Registra el compte en un servidor personalitzat subscrivint-te a la plataforma d'Element Matrix Services (EMS) + +Per què escollir Element? + +PROPIETAT DE LES TEVES DADES: Tu decideixes a on desar les teves dades i missatges. Tu les controles i n'ets el propietari, no una mega-corporació que s'aprofita de les teves dades o les cedeix a tercers. + +MISSATGERIA I COL·LABORACIÓ OBERTA: Pots parlar amb qualsevol que estigui a la xarxa Matrix, ja sigui amb Element o amb qualsevol altre aplicació Matrix, fins i tot encara que utilitzin sistemes de missatgeria diferents com Slack, IRC o XMPP. + +SUPER-SEGUR: Encriptació d'extrem a extrem real (només qui està conversant pot desxifrar els missatges), i amb signatures creuades per a verificar els dispositius dels participants en les converses. + +COMUNICACIÓ COMPLETA: Missatgeria, veu i video-trucades, compartició de fitxers, compartició de pantalla i un munt d'integracions, bots i ginys. Crea sales, comunitats, mantén-te en contacte i enllesteix el que et proposes. + +A TOT ARREU: Mantingues el contacte des de qualsevol lloc on siguis, amb un historial de missatges totalment sincronitzat entre tots els teus dispositius i també a la web: https://app.element.io. diff --git a/fastlane/metadata/android/ca/short_description.txt b/fastlane/metadata/android/ca/short_description.txt new file mode 100644 index 0000000000..1e842ec64e --- /dev/null +++ b/fastlane/metadata/android/ca/short_description.txt @@ -0,0 +1 @@ +Xat i VoIP segurs i descentralitzats. Protegeix les teves dades de tercers. diff --git a/fastlane/metadata/android/ca/title.txt b/fastlane/metadata/android/ca/title.txt new file mode 100644 index 0000000000..adc831006a --- /dev/null +++ b/fastlane/metadata/android/ca/title.txt @@ -0,0 +1 @@ +Element (anteriorment Riot.im) diff --git a/fastlane/metadata/android/de/changelogs/40100100.txt b/fastlane/metadata/android/de/changelogs/40100100.txt new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/40100100.txt @@ -0,0 +1 @@ +// TODO diff --git a/fastlane/metadata/android/en-US/changelogs/40100110.txt b/fastlane/metadata/android/en-US/changelogs/40100110.txt new file mode 100644 index 0000000000..e587003352 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100110.txt @@ -0,0 +1,2 @@ +This new version mainly contains user interface and user experience improvements. Now you can invite friends, and create DM very fast by scanning QR codes. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.11 \ No newline at end of file diff --git a/fastlane/metadata/android/es/changelogs/40100100.txt b/fastlane/metadata/android/es/changelogs/40100100.txt new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/40100100.txt @@ -0,0 +1 @@ +// TODO diff --git a/fastlane/metadata/android/es/full_description.txt b/fastlane/metadata/android/es/full_description.txt index 860a1f19c3..8c9915a735 100644 --- a/fastlane/metadata/android/es/full_description.txt +++ b/fastlane/metadata/android/es/full_description.txt @@ -1,30 +1,30 @@ Element es un nuevo tipo de aplicación de mensajería y colaboración que: -1. Le da el control para preservar su privacidad -2. Le permite comunicarse con cualquier persona en la red Matrix e incluso más allá al integrarse con aplicaciones como Slack. -3. Te protege de la publicidad, la minería de datos y los jardines vallados. -4. Lo protege a través del cifrado de un extremo a otro, con firma cruzada para verificar a otros +1. Te da el control para preservar su privacidad +2. Te permite comunicarse con cualquier persona en la red Matrix e incluso más allá al integrarse con aplicaciones como Slack +3. Te protege de la publicidad, la minería de datos y los jardines vallados +4. Te protege a través de encriptación de Extremo-a-Extremo, con firma cruzada para verificar a otros Element es completamente diferente de otras aplicaciones de mensajería y colaboración porque es descentralizado y de código abierto. -Element le permite autohospedarse, o elegir un host, para que tenga privacidad, propiedad y control de sus datos y conversaciones. Te da acceso a una red abierta; para que no se quede atascado hablando solo con otros usuarios de Element. Y es muy seguro. +Element te permite tener su propio servidor privado, o elegir uno público, para que tenga privacidad, posesión, y control de sus datos y conversaciones. Te da acceso a una red abierta; para que no se quede atrapado hablando solo con otros usuarios de Element. Y es muy seguro. Element puede hacer todo esto porque opera en Matrix, el estándar para la comunicación abierta y descentralizada. -Element te da el control permitiéndote elegir quién aloja tus conversaciones. Desde la aplicación Element, puede elegir hospedar de diferentes maneras: +Element te da el control permitiéndote elegir quién aloja tus conversaciones. Desde la aplicación Element, puedes elegir hospedar de diferentes maneras: -1. Obtenga una cuenta gratuita en el servidor público de matrix.org alojado por los desarrolladores de Matrix, o elija entre miles de servidores públicos alojados por voluntarios -2. Autohospede su cuenta ejecutando un servidor en su propio hardware -3. Regístrese para obtener una cuenta en un servidor personalizado simplemente suscribiéndose a la plataforma de alojamiento de Element Matrix Services +1. Obtén una cuenta gratuita en el servidor público de matrix.org alojado por los desarrolladores de Matrix, o elije entre miles de servidores públicos alojados por voluntarios +2. Autohospeda tu cuenta con un servidor en tu propio hardware +3. Regístrate para obtener una cuenta en un servidor personalizado simplemente suscribiéndote a la plataforma de alojamiento de Element Matrix Services ¿Por qué elegir Element? -POSEE SUS DATOS: Tú decides dónde guardar tus datos y mensajes. Usted es el propietario y lo controla, no algún MEGACORP que extraiga sus datos o dé acceso a terceros. +TOMA POSESIÓN DE TUS DATOS: Tú decides dónde guardar tus datos y mensajes. Tú eres el propietario y quien lo controla, no alguna MEGACORP que extrae tu datos o da acceso a terceros. -MENSAJERÍA ABIERTA Y COLABORACIÓN: Puede chatear con cualquier otra persona en la red de Matrix, ya sea que estén usando Element u otra aplicación de Matrix, e incluso si están usando un sistema de mensajería diferente como Slack, IRC o XMPP. +MENSAJERÍA ABIERTA Y COLABORACIÓN: Puede chatear con cualquier otra persona en la red de Matrix, tanto si usan Element u otra aplicación de Matrix, e incluso si están usando un sistema de mensajería diferente como Slack, IRC o XMPP. -SUPER SEGURO: Cifrado real de extremo a extremo (solo aquellos en la conversación pueden descifrar mensajes) y firma cruzada para verificar los dispositivos de los participantes de la conversación. +SUPER SEGURO: Encriptación de Extremo-a-Extremo real (solo aquellos en la conversación pueden descifrar mensajes) y firma cruzada para verificar los dispositivos de los participantes de la conversación. -COMUNICACIÓN COMPLETA: Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido de pantalla y un montón de integraciones, bots y widgets. Construya salas, comunidades, manténgase en contacto y haga las cosas. +COMUNICACIÓN COMPLETA: Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido de pantalla y un montón de integraciones, bots y widgets. Crea salas, comunidades, mantente en contacto y organízate con eficacia. -EN TODAS PARTES: Manténgase en contacto donde quiera que esté con un historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io. +EN TODAS PARTES: Mantente en contacto donde quiera que estés con un historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io. diff --git a/fastlane/metadata/android/es/short_description.txt b/fastlane/metadata/android/es/short_description.txt index 0562213351..473228e0df 100644 --- a/fastlane/metadata/android/es/short_description.txt +++ b/fastlane/metadata/android/es/short_description.txt @@ -1 +1 @@ -Chat y VoIP descentralizados seguros. Mantenga sus datos a salvo de terceros. +Chat y VoIP descentralizados y seguros. Mantén tus datos a salvo de terceros. diff --git a/fastlane/metadata/android/es/title.txt b/fastlane/metadata/android/es/title.txt index adc831006a..971e5cf146 100644 --- a/fastlane/metadata/android/es/title.txt +++ b/fastlane/metadata/android/es/title.txt @@ -1 +1 @@ -Element (anteriorment Riot.im) +Element (previamente Riot.im) diff --git a/fastlane/metadata/android/fa/changelogs/40100100.txt b/fastlane/metadata/android/fa/changelogs/40100100.txt new file mode 100644 index 0000000000..6123bfc7fc --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40100100.txt @@ -0,0 +1 @@ +// برای انجام diff --git a/fastlane/metadata/android/it/changelogs/40100100.txt b/fastlane/metadata/android/it/changelogs/40100100.txt new file mode 100644 index 0000000000..0c7cc8cc6c --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/40100100.txt @@ -0,0 +1 @@ +// DA FARE diff --git a/fastlane/metadata/android/nb/short_description.txt b/fastlane/metadata/android/nb/short_description.txt new file mode 100644 index 0000000000..b7cad4c849 --- /dev/null +++ b/fastlane/metadata/android/nb/short_description.txt @@ -0,0 +1 @@ +Sikker desentralisert chat & VoIP. Beskytt dataene dine fra tredjeparter. diff --git a/fastlane/metadata/android/nb/title.txt b/fastlane/metadata/android/nb/title.txt new file mode 100644 index 0000000000..aacee5be54 --- /dev/null +++ b/fastlane/metadata/android/nb/title.txt @@ -0,0 +1 @@ +Element (tidligere Riot.im) diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100100.txt b/fastlane/metadata/android/pt_BR/changelogs/40100100.txt new file mode 100644 index 0000000000..02cfd45a87 --- /dev/null +++ b/fastlane/metadata/android/pt_BR/changelogs/40100100.txt @@ -0,0 +1 @@ +// A FAZER diff --git a/fastlane/metadata/android/sv/changelogs/40100100.txt b/fastlane/metadata/android/sv/changelogs/40100100.txt new file mode 100644 index 0000000000..6da756aca9 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/40100100.txt @@ -0,0 +1 @@ +// ATT GÖRA diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt new file mode 100644 index 0000000000..3c21bcbeb6 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt @@ -0,0 +1 @@ +// 待辦事項 diff --git a/fastlane/metadata/android/zh_Hant/full_description.txt b/fastlane/metadata/android/zh_Hant/full_description.txt new file mode 100644 index 0000000000..2fdf6fa478 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/full_description.txt @@ -0,0 +1,30 @@ +Element 是一種新型態的即時通訊軟體與協作應用程式: + +1. 自己的隱私自己掌控 +2. 讓您與任何在 Matrix 網路中的人通訊,甚至可與如 Slack 等的應用程式整合 +3. 保護您免受廣告、資料採礦與圍牆花園的侵害 +4. 透過端到端加密保護您,並使用交叉簽章來驗證其他人 + +Element 是去中心化且開放原始碼的應用程式,因此與其他即時通訊與協作軟體完全不同。 + +Element 讓您可以自架(或是自行選擇服務提供者)所以您擁有您資料與對話的隱私、所有權與控制權。它讓您可以存取開放的網路;因此,您不僅可以與其他 Matrix 使用者聊天。而且非常安全。 + +Element 能作到這些事情是因為它在 Matrix 上執行,這是一個開放的去中心化通訊的標準。 + +Element 讓您選擇您要在哪裡託管您的對話來將控制權還給您。在 Element 應用程式中,您可以選擇其他方式來託管: + +1. 在由 Matrix 開發者架設的 matrix.org 公開伺服器上取得免費的帳號,或是從數千個由志願者所架設的公開伺服器中選擇 +2. 在您自己的硬體上自行架設伺服器並建立帳號 +3. 訂閱 Element Matrix 服務託管平台並在自訂伺服氣上註冊帳號 + +為何選擇 Element? + +擁有您的資料:您決定您的資料與訊息要放在哪裡。您擁有並控制它,而非某些科技巨頭會挖掘您的資料並將其售予第三方。 + +開放的即時通訊與協作:您可以與 Matrix 網路中的任何人聊天,不管他們是使用 Element 或其他 Matrix 應用程式都可以,或甚至是其他的訊息系統,如 Slack、IRC 或 XMPP 也都可以。 + +超級安全:即時的端到端加密(僅有參與對話的人可以解密訊息),以及交叉簽章以驗證對話參與者的裝置。 + +完整通訊:即時通訊、語音與視訊通話、檔案分享、畫面分享與超多的整合、機器人與小工具。建立聊天室、保持聯繫並完成工作。 + +無論您身在何處:無論您身在何處,都可以透過 https://app.element.io 來在所有裝置與網路上保持訊息歷史同步。 diff --git a/fastlane/metadata/android/zh_Hant/short_description.txt b/fastlane/metadata/android/zh_Hant/short_description.txt new file mode 100644 index 0000000000..23bb82c04e --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/short_description.txt @@ -0,0 +1 @@ +安全的去中心化聊天與 VoIP。確保您的資料不受第三方的影響。 diff --git a/fastlane/metadata/android/zh_Hant/title.txt b/fastlane/metadata/android/zh_Hant/title.txt new file mode 100644 index 0000000000..3be2260b73 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/title.txt @@ -0,0 +1 @@ +Element(曾名為 Riot.im) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 99d667ccdc..cdc95ef6eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 3d62758065..37f41d0a2a 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -36,9 +36,9 @@ android { dependencies { implementation project(":matrix-sdk-android") implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.2" diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt index f6dbe3d160..ec30a31f6d 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt @@ -21,34 +21,36 @@ import org.matrix.android.sdk.api.util.Cancelable import io.reactivex.Completable import io.reactivex.Single -fun singleBuilder(builder: (callback: MatrixCallback) -> Cancelable): Single = Single.create { - val callback: MatrixCallback = object : MatrixCallback { +fun singleBuilder(builder: (MatrixCallback) -> Cancelable): Single = Single.create { emitter -> + val callback = object : MatrixCallback { override fun onSuccess(data: T) { - it.onSuccess(data) + // Add `!!` to fix the warning: + // "Type mismatch: type parameter with nullable bounds is used T is used where T was expected. This warning will become an error soon" + emitter.onSuccess(data!!) } override fun onFailure(failure: Throwable) { - it.tryOnError(failure) + emitter.tryOnError(failure) } } val cancelable = builder(callback) - it.setCancellable { + emitter.setCancellable { cancelable.cancel() } } -fun completableBuilder(builder: (callback: MatrixCallback) -> Cancelable): Completable = Completable.create { - val callback: MatrixCallback = object : MatrixCallback { +fun completableBuilder(builder: (MatrixCallback) -> Cancelable): Completable = Completable.create { emitter -> + val callback = object : MatrixCallback { override fun onSuccess(data: T) { - it.onComplete() + emitter.onComplete() } override fun onFailure(failure: Throwable) { - it.tryOnError(failure) + emitter.tryOnError(failure) } } val cancelable = builder(callback) - it.setCancellable { + emitter.setCancellable { cancelable.cancel() } } 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 86f2d26808..bf4bcacc31 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 @@ -35,6 +35,8 @@ import org.matrix.android.sdk.api.util.toOptional import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules class RxRoom(private val room: Room) { @@ -127,18 +129,14 @@ class RxRoom(private val room: Room) { room.updateName(name, it) } - fun addRoomAlias(alias: String): Completable = completableBuilder { - room.addRoomAlias(alias, it) - } - - fun updateCanonicalAlias(alias: String): Completable = completableBuilder { - room.updateCanonicalAlias(alias, it) - } - fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder { room.updateHistoryReadability(readability, it) } + fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = completableBuilder { + room.updateJoinRule(joinRules, guestAccess, it) + } + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { room.updateAvatar(avatarUri, fileName, it) } 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 03df708c0c..0e5b88adb2 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 @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams 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.sync.SyncState @@ -92,6 +93,13 @@ class RxSession(private val session: Session) { } } + fun liveRoomMember(userId: String, roomId: String): Observable> { + return session.getRoomMemberLive(userId, roomId).asObservable() + .startWithCallable { + session.getRoomMember(userId, roomId).toOptional() + } + } + fun liveUsers(): Observable> { return session.getUsersLive().asObservable() } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 05408080ea..18b0410167 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -125,7 +125,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.appcompat:appcompat:1.2.0" - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" @@ -146,7 +145,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.3.0' + implementation 'androidx.exifinterface:exifinterface:1.3.1' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' 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 1a9165ade4..cbb22daf0f 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 @@ -68,8 +68,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { if (encryptedRoom) { val room = aliceSession.getRoom(roomId)!! - mTestHelper.doSync { - room.enableEncryption(callback = it) + mTestHelper.runBlockingTest { + room.enableEncryption() } } 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 7db159cd0b..ae300c936d 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 @@ -71,38 +71,27 @@ class SearchMessagesTest : InstrumentedTest { commonTestHelper.await(lock) lock = CountDownLatch(1) - aliceSession - .searchService() - .search( - searchTerm = "lore", - limit = 10, - includeProfile = true, - afterLimit = 0, - beforeLimit = 10, - orderByRecent = true, - nextBatch = null, - roomId = aliceRoomId, - 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() - } - } - ) - lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS) + 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) diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 5c03e8a855..630f6f1e29 100644 --- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -66,9 +66,9 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { } private fun logJson(formattedJson: String) { - val arr = formattedJson.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - for (s in arr) { - Timber.v(s) - } + formattedJson + .lines() + .dropLastWhile { it.isEmpty() } + .forEach { Timber.v(it) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index 645fb55bb9..48705ee7b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -27,7 +27,7 @@ interface LoginWizard { * @param password the password field * @param deviceName the initial device name * @param callback the matrix callback on which you'll receive the result of authentication. - * @return return a [Cancelable] + * @return a [Cancelable] */ fun login(login: String, password: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt index a17e65b8e0..e264843ea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt @@ -22,3 +22,8 @@ fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { else -> "$prefix$this" } } + +/** + * Append a new line and then the provided string + */ +fun StringBuilder.appendNl(str: String) = append("\n").append(str) 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 880a7be9ac..4da1662681 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 @@ -15,11 +15,9 @@ */ package org.matrix.android.sdk.api.pushrules -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.rest.RuleSet import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.util.Cancelable interface PushRuleService { /** @@ -29,13 +27,13 @@ interface PushRuleService { fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet - fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable + suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) - fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) - fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback): Cancelable + suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) - fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) fun addPushRuleListener(listener: PushRuleListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt index 4e24a17047..19549a338e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.raw -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * Useful methods to fetch raw data from the server. The access token will not be used to fetched the data */ @@ -26,17 +23,15 @@ interface RawService { /** * Get a URL, either from cache or from the remote server, depending on the cache strategy */ - fun getUrl(url: String, - rawCacheStrategy: RawCacheStrategy, - matrixCallback: MatrixCallback): Cancelable + suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String /** * Specific case for the well-known file. Cache validity is 8 hours */ - fun getWellknown(userId: String, matrixCallback: MatrixCallback): Cancelable + suspend fun getWellknown(userId: String): String /** * Clear all the cache data */ - fun clearCache(matrixCallback: MatrixCallback): Cancelable + suspend fun clearCache() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index b291f087ef..85ba3100b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -49,6 +49,12 @@ object EventType { const val STATE_ROOM_JOIN_RULES = "m.room.join_rules" const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" + + /** + * Note that this Event has been deprecated, see + * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events + * - https://github.com/matrix-org/matrix-doc/pull/2432 + */ const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" @@ -56,6 +62,7 @@ object EventType { const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" const val STATE_ROOM_ENCRYPTION = "m.room.encryption" + const val STATE_ROOM_SERVER_ACL = "m.room.server_acl" // Call Events const val CALL_INVITE = "m.call.invite" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt index a4186b5a32..25c69e5025 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.group -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to interact within a group. */ @@ -28,8 +25,7 @@ interface Group { /** * This methods allows you to refresh data about this group. It will be reflected on the GroupSummary. * The SDK also takes care of refreshing group data every hour. - * @param callback : the matrix callback to be notified of success or failure * @return a Cancelable to be able to cancel requests. */ - fun fetchGroupData(callback: MatrixCallback): Cancelable + suspend fun fetchGroupData() } 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 537104a084..aedb813735 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 @@ -92,9 +92,29 @@ interface IdentityService { /** * Search MatrixId of users providing email and phone numbers + * Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure + * 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 + /** + * Return the current user consent for the current identity server, which has been stored using [setUserConsent]. + * If [setUserConsent] has not been called, the returned value will be false. + * Note that if the identity server is changed, the user consent is reset to false. + * @return the value stored using [setUserConsent] or false if [setUserConsent] has never been called, or if the identity server + * has been changed + */ + fun getUserConsent(): Boolean + + /** + * Set the user consent to the provided value. Application MUST explicitly ask for the user consent to send their private data + * (email and phone numbers) to the identity server. + * Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details. + * @param newValue true if the user explicitly give their consent, false if the user wants to revoke their consent. + */ + fun setUserConsent(newValue: Boolean) + /** * Get the status of the current user's threePid * A lookup will be performed, but also pending binding state will be restored diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt index 72bb72cc2c..42fdb97643 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt @@ -24,6 +24,7 @@ sealed class IdentityServiceError : Failure.FeatureFailure() { object NoIdentityServerConfigured : IdentityServiceError() object TermsNotSignedException : IdentityServiceError() object BulkLookupSha256NotSupported : IdentityServiceError() + object UserConsentNotProvided : IdentityServiceError() object BindingError : IdentityServiceError() object NoCurrentBindingError : IdentityServiceError() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt index e27d81edb7..60af93888e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.integrationmanager -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This is the entry point to manage integration. You can grab an instance of this service through an active session. */ @@ -80,19 +77,17 @@ interface IntegrationManagerService { /** * Offers to enable or disable the integration. * @param enable the param to change - * @param callback the matrix callback to listen for result. * @return Cancelable */ - fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable + suspend fun setIntegrationEnabled(enable: Boolean) /** * Offers to allow or disallow a widget. * @param stateEventId the eventId of the state event defining the widget. * @param allowed the param to change - * @param callback the matrix callback to listen for result. * @return Cancelable */ - fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable + suspend fun setWidgetAllowed(stateEventId: String, allowed: Boolean) /** * Returns true if the widget is allowed, false otherwise. @@ -105,7 +100,7 @@ interface IntegrationManagerService { * @param widgetType the widget type to check for * @param domain the domain to check for */ - fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable + suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) /** * Returns true if the widget domain is allowed, false otherwise. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt index 7f264c6228..5e9f3e1eb9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.permalinks import android.text.Spannable +import org.matrix.android.sdk.api.MatrixPatterns /** * MatrixLinkify take a piece of text and turns all of the @@ -35,7 +36,7 @@ object MatrixLinkify { * I disable it because it mess up with pills, and even with pills, it does not work correctly: * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to */ - /* + // sanity checks if (spannable.isEmpty()) { return false @@ -48,14 +49,21 @@ object MatrixLinkify { val startPos = match.range.first if (startPos == 0 || text[startPos - 1] != '/') { val endPos = match.range.last + 1 - val url = text.substring(match.range) + var url = text.substring(match.range) + if (MatrixPatterns.isUserId(url) + || MatrixPatterns.isRoomAlias(url) + || MatrixPatterns.isRoomId(url) + || MatrixPatterns.isGroupId(url) + || MatrixPatterns.isEventId(url)) { + url = PermalinkService.MATRIX_TO_URL_BASE + url + } val span = MatrixPermalinkSpan(url, callback) spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } } return hasMatch - */ - return false + +// return false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt index dc47c81a5f..347a3bb531 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt @@ -44,13 +44,12 @@ object PermalinkParser { if (fragment.isNullOrEmpty()) { return PermalinkData.FallbackLink(uri) } - val indexOfQuery = fragment.indexOf("?") - val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment + val safeFragment = fragment.substringBefore('?') val viaQueryParameters = fragment.getViaParameters() // we are limiting to 2 params val params = safeFragment - .split(MatrixPatterns.SEP_REGEX.toRegex()) + .split(MatrixPatterns.SEP_REGEX) .filter { it.isNotEmpty() } .take(2) 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 837bda031b..cb6690b5c5 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 @@ -18,6 +18,7 @@ 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 import org.matrix.android.sdk.api.session.room.members.MembershipService @@ -46,6 +47,7 @@ interface Room : DraftService, ReadService, TypingService, + AliasService, TagsService, MembershipService, StateService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt index dc5b3d55f5..61970ce848 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility 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.session.room.model.thirdparty.ThirdPartyProtocol @@ -39,4 +40,14 @@ interface RoomDirectoryService { * Includes both the available protocols and all fields required for queries against each protocol. */ fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable + + /** + * Get the visibility of a room in the directory + */ + suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility + + /** + * Set the visibility of a room in the directory + */ + suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) } 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 b772225f51..477bef66cf 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 @@ -19,6 +19,7 @@ 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.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.util.Cancelable @@ -121,6 +122,11 @@ interface RoomService { searchOnServer: Boolean, callback: MatrixCallback>): Cancelable + /** + * Delete a room alias + */ + suspend fun deleteRoomAlias(roomAlias: String) + /** * Return a live data of all local changes membership that happened since the session has been opened. * It allows you to track this in your client to known what is currently being processed by the SDK. @@ -141,4 +147,20 @@ interface RoomService { * - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room */ fun getExistingDirectRoomWithUser(otherUserId: String): String? + + /** + * Get a room member for the tuple {userId,roomId} + * @param userId the userId to look for. + * @param roomId the roomId to look for. + * @return the room member or null + */ + fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? + + /** + * Observe a live room member for the tuple {userId,roomId} + * @param userId the userId to look for. + * @param roomId the roomId to look for. + * @return a LiveData of the optional found room member + */ + fun getRoomMemberLive(userId: String, roomId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/AliasService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/AliasService.kt new file mode 100644 index 0000000000..5fe7e99425 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/AliasService.kt @@ -0,0 +1,32 @@ +/* + * 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.api.session.room.alias + +interface AliasService { + /** + * Get list of local alias of the room + * @return the list of the aliases (full aliases, not only the local part) + */ + suspend fun getRoomAliases(): List + + /** + * Add local alias to the room + * @param aliasLocalPart the local part of the alias. + * Ex: for the alias "#my_alias:example.org", the local part is "my_alias" + */ + suspend fun addAlias(aliasLocalPart: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt new file mode 100644 index 0000000000..d2cb7c58a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt @@ -0,0 +1,23 @@ +/* + * 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.api.session.room.alias + +sealed class RoomAliasError : Throwable() { + object AliasEmpty : RoomAliasError() + object AliasNotAvailable : RoomAliasError() + object AliasInvalid : RoomAliasError() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt index e7e6bacc22..1251fd9857 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.room.crypto -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM interface RoomCryptoService { @@ -30,6 +29,5 @@ interface RoomCryptoService { /** * Enable encryption of the room */ - fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, - callback: MatrixCallback) + suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt index b0df2963f7..208cdd4556 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -18,8 +18,10 @@ package org.matrix.android.sdk.api.session.room.failure 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 sealed class CreateRoomFailure : Failure.FeatureFailure() { object CreatedWithTimeout : CreateRoomFailure() data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure() + data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt index f70e013786..59989f3045 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt @@ -21,6 +21,9 @@ import com.squareup.moshi.JsonClass /** * Class representing the EventType.STATE_ROOM_ALIASES state event content + * Note that this Event has been deprecated, see + * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events + * - https://github.com/matrix-org/matrix-doc/pull/2432 */ @JsonClass(generateAdapter = true) data class RoomAliasesContent( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt index 5487b2ff82..4e8bd2e71b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt @@ -24,5 +24,14 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomCanonicalAliasContent( - @Json(name = "alias") val canonicalAlias: String? = null + /** + * The canonical alias for the room. If not present, null, or empty the room should be considered to have no canonical alias. + */ + @Json(name = "alias") val canonicalAlias: String? = null, + + /** + * Alternative aliases the room advertises. + * This list can have aliases despite the alias field being null, empty, or otherwise not present. + */ + @Json(name = "alt_aliases") val alternativeAliases: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt new file mode 100644 index 0000000000..92078054b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt @@ -0,0 +1,59 @@ +/* + * 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.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_SERVER_ACL state event content + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-server-acl + */ +@JsonClass(generateAdapter = true) +data class RoomServerAclContent( + /** + * True to allow server names that are IP address literals. False to deny. + * Defaults to true if missing or otherwise not a boolean. + * This is strongly recommended to be set to false as servers running with IP literal names are strongly + * discouraged in order to require legitimate homeservers to be backed by a valid registered domain name. + */ + @Json(name = "allow_ip_literals") + val allowIpLiterals: Boolean = true, + + /** + * The server names to allow in the room, excluding any port information. Wildcards may be used to cover + * a wider range of hosts, where * matches zero or more characters and ? matches exactly one character. + * + * This defaults to an empty list when not provided, effectively disallowing every server. + */ + @Json(name = "allow") + val allowList: List = emptyList(), + + /** + * The server names to disallow in the room, excluding any port information. Wildcards may be used to cover + * a wider range of hosts, where * matches zero or more characters and ? matches exactly one character. + * + * This defaults to an empty list when not provided. + */ + @Json(name = "deny") + val denyList: List = emptyList() + +) { + companion object { + const val ALL = "*" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index 892a865751..80e3741a0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -94,7 +94,22 @@ class CreateRoomParams { * The server will clobber the following keys: creator. * Future versions of the specification may allow the server to clobber other keys. */ - var creationContent: Any? = null + val creationContent = mutableMapOf() + + /** + * Set to true to disable federation of this room. + * Default: false + */ + var disableFederation = false + set(value) { + field = value + if (value) { + creationContent[CREATION_CONTENT_KEY_M_FEDERATE] = false + } else { + // This is the default value, we remove the field + creationContent.remove(CREATION_CONTENT_KEY_M_FEDERATE) + } + } /** * The power level content to override in the default power level event @@ -120,4 +135,8 @@ class CreateRoomParams { fun enableEncryption() { algorithm = MXCRYPTO_ALGORITHM_MEGOLM } + + companion object { + private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate" + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt index 32d6033578..eb822c68ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt @@ -17,12 +17,10 @@ package org.matrix.android.sdk.api.session.room.notification import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable interface RoomPushRuleService { fun getLiveRoomNotificationState(): LiveData - fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable + suspend fun setRoomNotificationState(roomNotificationState: RoomNotificationState) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt index 0ccdfd1d3c..a444e2346e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.room.reporting -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to report content of an event. */ @@ -28,5 +25,5 @@ interface ReportingService { * Report content * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid */ - fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable + suspend fun reportContent(eventId: String, score: Int, reason: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt index 116a60e323..a9481d71a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.api.session.room.send import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional interface DraftService { @@ -26,12 +24,12 @@ interface DraftService { /** * Save or update a draft to the room */ - fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable + suspend fun saveDraft(draft: UserDraft) /** * Delete the last draft, basically just after sending the message */ - fun deleteDraft(callback: MatrixCallback): Cancelable + suspend fun deleteDraft() /** * Return the current draft or null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index e4baa58c30..74e3faf38a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -21,7 +21,9 @@ 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.Event +import org.matrix.android.sdk.api.session.room.model.GuestAccess 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.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -38,21 +40,23 @@ interface StateService { */ fun updateName(name: String, callback: MatrixCallback): Cancelable - /** - * Add new alias to the room. - */ - fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable - /** * Update the canonical alias of the room + * @param alias the canonical alias, or null to reset the canonical alias of this room + * @param altAliases the alternative aliases for this room. It should include the canonical alias if any. */ - fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable + fun updateCanonicalAlias(alias: String?, altAliases: List, callback: MatrixCallback): Cancelable /** * Update the history readability of the room */ fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable + /** + * Update the join rule and/or the guest access + */ + fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback): Cancelable + /** * Update the avatar of the room */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt index 3278c640de..69fde61f90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.room.tags -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to handle tags of a room. It's implemented at the room level. */ @@ -26,10 +23,10 @@ interface TagsService { /** * Add a tag to a room */ - fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable + suspend fun addTag(tag: String, order: Double?) /** * Remove tag from a room */ - fun deleteTag(tag: String, callback: MatrixCallback): Cancelable + suspend fun deleteTag(tag: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt index c3cc1eb9ee..e2462d007d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.room.uploads -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level. */ @@ -29,7 +26,5 @@ interface UploadsService { * @param numberOfEvents the expected number of events to retrieve. The result can contain less events. * @param since token to get next page, or null to get the first page */ - fun getUploads(numberOfEvents: Int, - since: String?, - callback: MatrixCallback): Cancelable + suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt index ef2eec433f..bc1c9e5769 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.search -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to search messages in rooms. */ @@ -35,15 +32,13 @@ interface SearchService { * @param beforeLimit how many events before the result are returned. * @param afterLimit how many events after the result are returned. * @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, - roomId: String, - nextBatch: String?, - orderByRecent: Boolean, - limit: Int, - beforeLimit: Int, - afterLimit: Int, - includeProfile: Boolean, - callback: MatrixCallback): Cancelable + suspend fun search(searchTerm: String, + roomId: String, + nextBatch: String?, + orderByRecent: Boolean, + limit: Int, + beforeLimit: Int, + afterLimit: Int, + includeProfile: Boolean): SearchResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt index 2d88125662..10ce0829d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt @@ -16,22 +16,16 @@ package org.matrix.android.sdk.api.session.terms -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - interface TermsService { enum class ServiceType { IntegrationManager, IdentityService } - fun getTerms(serviceType: ServiceType, - baseUrl: String, - callback: MatrixCallback): Cancelable + suspend fun getTerms(serviceType: ServiceType, baseUrl: String): GetTermsResponse - fun agreeToTerms(serviceType: ServiceType, - baseUrl: String, - agreedUrls: List, - token: String?, - callback: MatrixCallback): Cancelable + suspend fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?) } 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 2cfc4b731f..ab85f979bf 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 @@ -35,6 +35,11 @@ interface UserService { */ fun getUser(userId: String): User? + /** + * Try to resolve user from known users, or using profile api + */ + fun resolveUser(userId: String, callback: MatrixCallback) + /** * Search list of users on server directory. * @param search the searched term diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index d3a3fd9fbd..ebd809f777 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -767,9 +767,9 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel() ?: return - Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") + Timber.i("## CRYPTO | onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { - Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields") + Timber.e("## CRYPTO | onRoomKeyEvent() : missing fields") return } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) @@ -782,20 +782,20 @@ internal class DefaultCryptoService @Inject constructor( private fun onKeyWithHeldReceived(event: Event) { val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { - Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") } - Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <$withHeldContent>") + Timber.i("## CRYPTO | onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) if (alg is IMXWithHeldExtension) { alg.onRoomKeyWithHeldEvent(withHeldContent) } else { - Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}") + Timber.e("## CRYPTO | onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}") return } } private fun onSecretSendReceived(event: Event) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") + Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") if (!event.isEncrypted()) { // secret send messages must be encrypted Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 38488f1ca7..92b7728890 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -119,7 +119,7 @@ internal class EventDecryptor @Inject constructor( markOlmSessionForUnwedging(event.senderId ?: "", it) } ?: run { - Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") + Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging") } } } @@ -137,16 +137,18 @@ internal class EventDecryptor @Inject constructor( val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 val now = System.currentTimeMillis() if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") return } - Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") lastNewSessionForcedDates.setObject(senderId, deviceKey, now) // offload this from crypto thread (?) cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + val ensured = ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") // Now send a blank message on that session so the other side knows about it. // (The keyshare request is sent in the clear so that won't do) @@ -159,10 +161,14 @@ internal class EventDecryptor @Inject constructor( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") withContext(coroutineDispatchers.io) { val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.execute(sendToDeviceParams) + try { + sendToDeviceTask.execute(sendToDeviceParams) + } catch (failure: Throwable) { + Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt index 97ae0b9d83..4f94a27bbd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -54,6 +54,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( private val cryptoCoroutineScope: CoroutineScope) { private val executor = Executors.newSingleThreadExecutor() + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // we received in the current sync. private val receivedGossipingRequests = ArrayList() @@ -103,11 +104,11 @@ internal class IncomingGossipingRequestManager @Inject constructor( * @param event the announcement event. */ fun onGossipingRequestEvent(event: Event) { - Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") val roomKeyShare = event.getClearContent().toModel() + Timber.i("## CRYPTO | GOSSIP onGossipingRequestEvent received type ${event.type} from user:${event.senderId}, content:$roomKeyShare") // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } when (roomKeyShare?.action) { - GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { + GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { if (event.getClearType() == EventType.REQUEST_SECRET) { IncomingSecretShareRequest.fromEvent(event)?.let { if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { @@ -324,7 +325,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() when (secretName) { - MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 1a4d1136c8..c952602d93 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -760,7 +760,7 @@ internal class MXOlmDevice @Inject constructor( return session } } else { - Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt index f0a3413978..bcaa16f356 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt @@ -100,7 +100,7 @@ internal class SendGossipWorker(context: Context, requestId = params.requestId, state = GossipingRequestState.FAILED_TO_ACCEPTED ) - Timber.e("no session with this device, probably because there were no one-time keys.") + Timber.e("no session with this device $requestingDeviceId, probably because there were no one-time keys.") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index b05f2cd592..95b99c54e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -69,7 +69,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( // // That should eventually resolve itself, but it's poor form. - Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") + Timber.i("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) @@ -90,7 +90,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( oneTimeKey = key } if (oneTimeKey == null) { - Timber.v("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + Timber.w("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + " for device " + userId + " : " + deviceId) continue } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index e0116fae1c..787d16defc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -243,8 +243,7 @@ internal class MXMegolmDecryption(private val userId: String, return } if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - Timber.v("## CRYPTO | onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" + - " sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}") + Timber.i("## CRYPTO | onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") val forwardedRoomKeyContent = event.getClearContent().toModel() ?: return @@ -273,9 +272,7 @@ internal class MXMegolmDecryption(private val userId: String, keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key } else { - Timber.v("## CRYPTO | onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId - + " sessionKey " + roomKeyContent.sessionKey) // from " + event); - + Timber.i("## CRYPTO | onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") if (null == senderKey) { Timber.e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") return @@ -285,7 +282,7 @@ internal class MXMegolmDecryption(private val userId: String, keysClaimed = event.getKeysClaimed().toMutableMap() } - Timber.e("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") + Timber.i("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, @@ -349,10 +346,10 @@ internal class MXMegolmDecryption(private val userId: String, if (olmSessionResult?.sessionId == null) { // no session with this device, probably because there // were no one-time keys. + Timber.e("no session with this device $deviceId, probably because there were no one-time keys.") return@mapCatching } - Timber.v("## CRYPTO | shareKeysWithDevice() : sharing keys for session" + - " ${body.senderKey}|${body.sessionId} with device $userId:$deviceId") + Timber.i("## CRYPTO | shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } @@ -363,6 +360,7 @@ internal class MXMegolmDecryption(private val userId: String, }, { // TODO + Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") } ) @@ -370,9 +368,13 @@ internal class MXMegolmDecryption(private val userId: String, val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.v("## CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId") + Timber.i("## CRYPTO | shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.execute(sendToDeviceParams) + try { + sendToDeviceTask.execute(sendToDeviceParams) + } catch (failure: Throwable) { + Timber.e(failure, "## CRYPTO | shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") + } } } } 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 e55cf37118..fd431ce735 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 @@ -217,8 +217,10 @@ internal class MXMegolmEncryption( Timber.v("## CRYPTO | shareUserDevicesKey() : starts") val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - Timber.v("## CRYPTO | shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " - + (System.currentTimeMillis() - t0) + " ms") + Timber.v( + """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" + .trimMargin() + ) val contentMap = MXUsersDevicesMap() var haveTargets = false val userIds = results.userIds @@ -242,7 +244,7 @@ internal class MXMegolmEncryption( continue } - Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") + Timber.i("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) haveTargets = true } @@ -270,21 +272,22 @@ internal class MXMegolmEncryption( if (haveTargets) { t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | shareUserDevicesKey() : has target") + Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { sendToDeviceTask.execute(sendToDeviceParams) - Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") } } else { - Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey") + Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") } } private fun notifyKeyWithHeld(targets: List, sessionId: String, senderKey: String?, code: WithHeldCode) { + Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId ") val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, @@ -393,16 +396,16 @@ internal class MXMegolmEncryption( userId: String, deviceId: String, senderKey: String): Boolean { - Timber.d("[MXMegolmEncryption] reshareKey: $sessionId to $userId:$deviceId") + Timber.i("## Crypto process reshareKey for $sessionId to $userId:$deviceId") val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false - .also { Timber.w("Device not found") } + .also { Timber.w("## Crypto reshareKey: Device not found") } // Get the chain index of the key we previously sent this device val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false .also { // Send a room key with held notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) - Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") + Timber.w("## Crypto reshareKey: ERROR : Never share megolm with this device") } val devicesByUser = mapOf(userId to listOf(deviceInfo)) @@ -411,9 +414,11 @@ internal class MXMegolmEncryption( olmSessionResult?.sessionId ?: // no session with this device, probably because there were no one-time keys. // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. - return false + return false.also { + Timber.w("## Crypto reshareKey: no session with this device, probably because there were no one-time keys") + } - Timber.d("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") + Timber.i("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) @@ -425,6 +430,7 @@ internal class MXMegolmEncryption( }, { // TODO + Timber.e(it, "[MXMegolmEncryption] reshareKey: failed to get session $sessionId|$senderKey|$roomId") } ) @@ -432,13 +438,14 @@ internal class MXMegolmEncryption( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId") + Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) return try { sendToDeviceTask.execute(sendToDeviceParams) + Timber.i("## CRYPTO reshareKey() : successfully send <$sessionId> to $userId:$deviceId") true } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId") + Timber.e(failure, "## CRYPTO reshareKey() : fail to send <$sessionId> to $userId:$deviceId") false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 1871dba0e2..bcad448eb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -38,7 +38,6 @@ import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.withoutPrefix import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo @@ -444,7 +443,7 @@ internal class DefaultCrossSigningService @Inject constructor( } else { // Maybe it's signed by a locally trusted device? myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> - val potentialDeviceId = key.withoutPrefix("ed25519:") + val potentialDeviceId = key.removePrefix("ed25519:") val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) if (potentialDevice != null && potentialDevice.isVerified) { // Check signature validity? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index f28fe7d642..665d770e7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -241,9 +241,9 @@ internal class UpdateTrustWorker(context: Context, private fun computeRoomShield(activeMemberUserIds: List, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds") // The set of “all users” depends on the type of room: - // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room - val listToCheck = if (roomSummaryEntity.isDirect) { + val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) { activeMemberUserIds.filter { it != myUserId } } else { activeMemberUserIds diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 72274aa70a..6b83c4f7d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -1679,27 +1679,24 @@ internal class RealmCryptoStore @Inject constructor( // Only keep one week history realm.where() .lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs) - .findAll().let { - Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") - it.deleteAllFromRealm() - } + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") } + .deleteAllFromRealm() // Clean the cancelled ones? realm.where() .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name) .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .findAll().let { - Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") - it.deleteAllFromRealm() - } + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") } + .deleteAllFromRealm() // Only keep one week history realm.where() .lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs) - .findAll().let { - Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") - it.deleteAllFromRealm() - } + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") } + .deleteAllFromRealm() // Can we do something for WithHeldSessionEntity? } 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 29ddd92213..a92f5c5bf1 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 @@ -1204,7 +1204,7 @@ internal class DefaultVerificationService @Inject constructor( Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId) - ?.values?.map { it.deviceId } ?: emptyList() + ?.values?.map { it.deviceId }.orEmpty() val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index cb254eac3c..c7885ce449 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -28,10 +28,10 @@ import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.extensions.toUnsignedInt -import org.matrix.android.sdk.internal.util.withoutPrefix import org.matrix.olm.OlmSAS import org.matrix.olm.OlmUtility import timber.log.Timber +import java.util.Locale /** * Represents an ongoing short code interactive key verification between two devices. @@ -250,7 +250,7 @@ internal abstract class SASDefaultVerificationTransaction( // cannot be empty because it has been validated theirMacSafe.mac.keys.forEach { - val keyIDNoPrefix = it.withoutPrefix("ed25519:") + val keyIDNoPrefix = it.removePrefix("ed25519:") val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() if (otherDeviceKey == null) { Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify") @@ -273,7 +273,7 @@ internal abstract class SASDefaultVerificationTransaction( if (otherCrossSigningMasterKeyPublic != null) { // Did the user signed his master key theirMacSafe.mac.keys.forEach { - val keyIDNoPrefix = it.withoutPrefix("ed25519:") + val keyIDNoPrefix = it.removePrefix("ed25519:") if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { // Check the signature val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) @@ -345,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction( } protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { + if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) { val olmUtil = OlmUtility() val hashBytes = olmUtil.sha256(toHash) olmUtil.releaseUtility() @@ -355,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction( } private fun macUsingAgreedMethod(message: String, info: String): String? { - if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { - return getSAS().calculateMacLongKdf(message, info) - } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { - return getSAS().calculateMac(message, info) + return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) { + SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) + SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) + else -> null } - return null } override fun getDecimalCodeRepresentation(): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt index 6c604f232f..724ec0dc7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt @@ -52,5 +52,8 @@ internal class TimeOutInterceptor @Inject constructor() : Interceptor { const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT" const val READ_TIMEOUT = "READ_TIMEOUT" const val WRITE_TIMEOUT = "WRITE_TIMEOUT" + + // 1 minute + const val DEFAULT_LONG_TIMEOUT: Long = 60_000 } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt index be01366efa..3b0d7546e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt @@ -16,45 +16,28 @@ package org.matrix.android.sdk.internal.raw -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.raw.RawCacheStrategy import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import java.util.concurrent.TimeUnit import javax.inject.Inject internal class DefaultRawService @Inject constructor( - private val taskExecutor: TaskExecutor, private val getUrlTask: GetUrlTask, private val cleanRawCacheTask: CleanRawCacheTask ) : RawService { - override fun getUrl(url: String, - rawCacheStrategy: RawCacheStrategy, - matrixCallback: MatrixCallback): Cancelable { - return getUrlTask - .configureWith(GetUrlTask.Params(url, rawCacheStrategy)) { - callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String { + return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy)) } - override fun getWellknown(userId: String, - matrixCallback: MatrixCallback): Cancelable { + override suspend fun getWellknown(userId: String): String { val homeServerDomain = userId.substringAfter(":") return getUrl( "https://$homeServerDomain/.well-known/matrix/client", - RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false), - matrixCallback + RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) ) } - override fun clearCache(matrixCallback: MatrixCallback): Cancelable { - return cleanRawCacheTask - .configureWith(Unit) { - callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun clearCache() { + cleanRawCacheTask.execute(Unit) } } 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 new file mode 100644 index 0000000000..6a50f3ee37 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt @@ -0,0 +1,70 @@ +/* + * 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.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 +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface DirectoryAPI { + /** + * Get the room ID associated to the room alias. + * + * @param roomAlias the room alias. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call + + /** + * Get the room directory visibility. + * + * @param roomId the room id. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}") + fun getRoomDirectoryVisibility(@Path("roomId") roomId: String): Call + + /** + * Set the room directory visibility. + * + * @param roomId the room id. + * @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 + + /** + * 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 + + /** + * 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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/RoomDirectoryVisibilityJson.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/RoomDirectoryVisibilityJson.kt new file mode 100644 index 0000000000..ddf927a3dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/RoomDirectoryVisibilityJson.kt @@ -0,0 +1,29 @@ +/* + * 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.directory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility + +@JsonClass(generateAdapter = true) +internal data class RoomDirectoryVisibilityJson( + /** + * The visibility of the room in the directory. One of: ["private", "public"] + */ + @Json(name = "visibility") val visibility: RoomDirectoryVisibility +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt index 01b57767b3..4f610fd81b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt @@ -16,20 +16,13 @@ package org.matrix.android.sdk.internal.session.group -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.group.Group -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultGroup(override val groupId: String, - private val taskExecutor: TaskExecutor, private val getGroupDataTask: GetGroupDataTask) : Group { - override fun fetchGroupData(callback: MatrixCallback): Cancelable { + override suspend fun fetchGroupData() { val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) - return getGroupDataTask.configureWith(params) { - this.callback = callback - }.executeBy(taskExecutor) + getGroupDataTask.execute(params) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt index 31450763d8..653d2a6933 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.group import org.matrix.android.sdk.api.session.group.Group import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject internal interface GroupFactory { @@ -26,14 +25,12 @@ internal interface GroupFactory { } @SessionScope -internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask, - private val taskExecutor: TaskExecutor) : +internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) : GroupFactory { override fun create(groupId: String): Group { return DefaultGroup( groupId = groupId, - taskExecutor = taskExecutor, getGroupDataTask = getGroupDataTask ) } 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 20f8b7f868..c6fb34151c 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 @@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.ensureProtocol import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber import javax.inject.Inject import javax.net.ssl.HttpsURLConnection @@ -243,7 +244,20 @@ internal class DefaultIdentityService @Inject constructor( )) } + override fun getUserConsent(): Boolean { + return identityStore.getIdentityData()?.userConsent.orFalse() + } + + override fun setUserConsent(newValue: Boolean) { + identityStore.setUserConsent(newValue) + } + override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { + if (!getUserConsent()) { + callback.onFailure(IdentityServiceError.UserConsentNotProvided) + return NoOpCancellable + } + if (threePids.isEmpty()) { callback.onSuccess(emptyList()) return NoOpCancellable @@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor( } override fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable { + // 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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt index e140cc19f3..7a39a333a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore import io.realm.RealmConfiguration import okhttp3.OkHttpClient +import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration import java.io.File @Module @@ -59,6 +60,7 @@ internal abstract class IdentityModule { @SessionScope fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, @SessionFilesDirectory directory: File, + migration: RealmIdentityStoreMigration, @UserMd5 userMd5: String): RealmConfiguration { return RealmConfiguration.Builder() .directory(directory) @@ -66,6 +68,8 @@ internal abstract class IdentityModule { .apply { realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } + .schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION) + .migration(migration) .allowWritesOnUiThread(true) .modules(IdentityRealmModule()) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt index 0f04f2fe1a..54d35b34fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt @@ -20,5 +20,6 @@ internal data class IdentityData( val identityServerUrl: String?, val token: String?, val hashLookupPepper: String?, - val hashLookupAlgorithm: List + val hashLookupAlgorithm: List, + val userConsent: Boolean ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt index 3a905833d5..0e05224be5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt @@ -27,6 +27,8 @@ internal interface IdentityStore { fun setToken(token: String?) + fun setUserConsent(consent: Boolean) + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt index cc03465cc8..019289a884 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt @@ -23,7 +23,8 @@ internal open class IdentityDataEntity( var identityServerUrl: String? = null, var token: String? = null, var hashLookupPepper: String? = null, - var hashLookupAlgorithm: RealmList = RealmList() + var hashLookupAlgorithm: RealmList = RealmList(), + var userConsent: Boolean = false ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt index 062c28ea55..5152e33743 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -52,6 +52,13 @@ internal fun IdentityDataEntity.Companion.setToken(realm: Realm, } } +internal fun IdentityDataEntity.Companion.setUserConsent(realm: Realm, + newConsent: Boolean) { + get(realm)?.apply { + userConsent = newConsent + } +} + internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, pepper: String, algorithms: List) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt index 98207f1b38..bf23c05811 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt @@ -26,7 +26,8 @@ internal object IdentityMapper { identityServerUrl = entity.identityServerUrl, token = entity.token, hashLookupPepper = entity.hashLookupPepper, - hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList(), + userConsent = entity.userConsent ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt index 0352e9b936..2fa3fc0cfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt @@ -55,6 +55,14 @@ internal class RealmIdentityStore @Inject constructor( } } + override fun setUserConsent(consent: Boolean) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUserConsent(realm, consent) + } + } + } + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { Realm.getInstance(realmConfiguration).use { it.executeTransaction { realm -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt new file mode 100644 index 0000000000..6081dbab12 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt @@ -0,0 +1,43 @@ +/* + * 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.identity.db + +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber +import javax.inject.Inject + +internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration { + + companion object { + const val IDENTITY_STORE_SCHEMA_VERSION = 1L + } + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.v("Migrating Realm Identity from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Add field userConsent (Boolean) and set the value to false") + + realm.schema.get("IdentityDataEntity") + ?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt index 753e865b4a..8bf6437009 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt @@ -16,10 +16,8 @@ package org.matrix.android.sdk.internal.session.integrationmanager -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService -import org.matrix.android.sdk.api.util.Cancelable import javax.inject.Inject internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService { @@ -44,20 +42,20 @@ internal class DefaultIntegrationManagerService @Inject constructor(private val return integrationManager.isIntegrationEnabled() } - override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { - return integrationManager.setIntegrationEnabled(enable, callback) + override suspend fun setIntegrationEnabled(enable: Boolean) { + integrationManager.setIntegrationEnabled(enable) } - override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { - return integrationManager.setWidgetAllowed(stateEventId, allowed, callback) + override suspend fun setWidgetAllowed(stateEventId: String, allowed: Boolean) { + integrationManager.setWidgetAllowed(stateEventId, allowed) } override fun isWidgetAllowed(stateEventId: String): Boolean { return integrationManager.isWidgetAllowed(stateEventId) } - override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { - return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback) + override suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) { + integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed) } override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt index df4e407415..ebd57ce657 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -20,15 +20,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.extensions.observeNotNull @@ -41,7 +38,6 @@ import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccoun 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.configureWith import timber.log.Timber import javax.inject.Inject @@ -137,22 +133,17 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri return integrationProvisioningContent?.enabled ?: false } - fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { + suspend fun setIntegrationEnabled(enable: Boolean) { val isIntegrationEnabled = isIntegrationEnabled() if (enable == isIntegrationEnabled) { - callback.onSuccess(Unit) - return NoOpCancellable + return } val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable) val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent) - return updateUserAccountDataTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return updateUserAccountDataTask.execute(params) } - fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + suspend fun setWidgetAllowed(stateEventId: String, allowed: Boolean) { val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() val newContent = if (currentContent == null) { @@ -165,11 +156,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri currentContent.copy(widgets = allowedWidgets) } val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) - return updateUserAccountDataTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return updateUserAccountDataTask.execute(params) } fun isWidgetAllowed(stateEventId: String): Boolean { @@ -178,7 +165,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri return currentContent?.widgets?.get(stateEventId) ?: false } - fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) { val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() val newContent = if (currentContent == null) { @@ -195,11 +182,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri currentContent.copy(native = nativeAllowedWidgets) } val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) - return updateUserAccountDataTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return updateUserAccountDataTask.execute(params) } fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean { 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 217da269f9..e00d2ff26c 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,7 +16,6 @@ package org.matrix.android.sdk.internal.session.notification import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleSetKey @@ -24,7 +23,6 @@ import org.matrix.android.sdk.api.pushrules.getActions import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.rest.RuleSet import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper import org.matrix.android.sdk.internal.database.model.PushRulesEntity import org.matrix.android.sdk.internal.database.query.where @@ -103,37 +101,21 @@ internal class DefaultPushRuleService @Inject constructor( ) } - override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + override suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) { // The rules will be updated, and will come back from the next sync response - return updatePushRuleEnableStatusTask - .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) { - this.callback = callback - } - .executeBy(taskExecutor) + updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) } - override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { - return addPushRuleTask - .configureWith(AddPushRuleTask.Params(kind, pushRule)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) { + addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule)) } - override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback): Cancelable { - return updatePushRuleActionsTask - .configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) { + updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) } - override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { - return removePushRuleTask - .configureWith(RemovePushRuleTask.Params(kind, pushRule)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) { + removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule)) } override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { 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 8d09277295..5265e4f17d 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 @@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.UserThreePidEntity import org.matrix.android.sdk.internal.di.SessionDatabase 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 @@ -49,6 +50,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask, private val deleteThreePidTask: DeleteThreePidTask, private val pendingThreePidMapper: PendingThreePidMapper, + private val userStore: UserStore, private val fileUploader: FileUploader) : ProfileService { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { @@ -70,17 +72,17 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto } override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback): Cancelable { - return setDisplayNameTask - .configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) { - callback = matrixCallback - } - .executeBy(taskExecutor) + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.io, matrixCallback) { + 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) { val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg") setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) + userStore.updateAvatar(userId, response.contentUri) } } 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 1338df6878..7a819250cf 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 @@ -21,6 +21,7 @@ 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 +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.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -58,6 +59,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val roomCallService: RoomCallService, private val readService: ReadService, private val typingService: TypingService, + private val aliasService: AliasService, private val tagsService: TagsService, private val cryptoService: CryptoService, private val relationService: RelationService, @@ -76,6 +78,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, RoomCallService by roomCallService, ReadService by readService, TypingService by typingService, + AliasService by aliasService, TagsService by tagsService, RelationService by relationService, MembershipService by roomMembersService, @@ -101,13 +104,13 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, return cryptoService.shouldEncryptForInvitedMembers(roomId) } - override fun enableEncryption(algorithm: String, callback: MatrixCallback) { + override suspend fun enableEncryption(algorithm: String) { when { isEncrypted() -> { - callback.onFailure(IllegalStateException("Encryption is already enabled for this room")) + throw IllegalStateException("Encryption is already enabled for this room") } algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { - callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")) + throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported") } else -> { val params = SendStateTask.Params( @@ -118,11 +121,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, "algorithm" to algorithm )) - sendStateTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + sendStateTask.execute(params) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt index a091b5f85e..0d41c6f35e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -18,19 +18,25 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility 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.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import javax.inject.Inject -internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, - private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, - private val taskExecutor: TaskExecutor) : RoomDirectoryService { +internal class DefaultRoomDirectoryService @Inject constructor( + private val getPublicRoomTask: GetPublicRoomTask, + private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, + private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, + private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask, + private val taskExecutor: TaskExecutor) : RoomDirectoryService { override fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, @@ -49,4 +55,12 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu } .executeBy(taskExecutor) } + + override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility { + return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId)) + } + + override suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) { + setRoomDirectoryVisibilityTask.execute(SetRoomDirectoryVisibilityTask.Params(roomId, roomDirectoryVisibility)) + } } 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 d49c2f120c..9ec985e0b6 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 @@ -17,32 +17,44 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback 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.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.util.Cancelable import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask 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 javax.inject.Inject internal class DefaultRoomService @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, private val joinRoomTask: JoinRoomTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val roomIdByAliasTask: GetRoomIdByAliasTask, + private val deleteRoomAliasTask: DeleteRoomAliasTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @@ -115,7 +127,31 @@ internal class DefaultRoomService @Inject constructor( .executeBy(taskExecutor) } + override suspend fun deleteRoomAlias(roomAlias: String) { + deleteRoomAliasTask.execute(DeleteRoomAliasTask.Params(roomAlias)) + } + override fun getChangeMembershipsLive(): LiveData> { return roomChangeMembershipStateDataSource.getLiveStates() } + + override fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? { + val roomMemberEntity = monarchy.fetchCopied { + RoomMemberHelper(it, roomId).getLastRoomMember(userId) + } + return roomMemberEntity?.asDomain() + } + + override fun getRoomMemberLive(userId: String, roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.USER_ID, userId) + }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } } 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 fc80842f73..955a251b52 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 @@ -23,8 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsRe import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.JsonDict 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 org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse import org.matrix.android.sdk.internal.session.room.create.CreateRoomBody import org.matrix.android.sdk.internal.session.room.create.CreateRoomResponse import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse @@ -321,20 +320,11 @@ internal interface RoomAPI { @Body body: ReportContentBody): Call /** - * Get the room ID associated to the room alias. - * - * @param roomAlias the room alias. + * 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_R0 + "directory/room/{roomAlias}") - fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call - - /** - * 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 + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases") + fun getAliases(@Path("roomId") roomId: String): Call /** * Inform that the user is starting to type or has stopped typing 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 90ee99a919..99f9d3644d 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 @@ -26,6 +26,8 @@ import org.matrix.android.sdk.internal.database.query.getOrNull 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) { @@ -46,11 +48,14 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId 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) - if (members.size == 1) { - res = members.firstOrNull()?.avatarUrl - } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() - res = firstOtherMember?.avatarUrl + val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false + if (isDirectRoom) { + if (members.size == 1) { + res = members.firstOrNull()?.avatarUrl + } else if (members.size == 2) { + val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl + } } return res } 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 d4fa040d06..63370a1ad8 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService @@ -54,6 +55,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val roomCallServiceFactory: DefaultRoomCallService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val typingServiceFactory: DefaultTypingService.Factory, + private val aliasServiceFactory: DefaultAliasService.Factory, private val tagsServiceFactory: DefaultTagsService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, @@ -76,6 +78,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomCallService = roomCallServiceFactory.create(roomId), readService = readServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId), + aliasService = aliasServiceFactory.create(roomId), tagsService = tagsServiceFactory.create(roomId), cryptoService = cryptoService, relationService = relationServiceFactory.create(roomId), 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 6381796ee0..3a94396a61 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 @@ -26,16 +26,25 @@ import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.internal.session.DefaultFileService import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DefaultAddRoomAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultDeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAliasesTask +import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask @@ -90,6 +99,13 @@ internal abstract class RoomModule { return retrofit.create(RoomAPI::class.java) } + @Provides + @JvmStatic + @SessionScope + fun providesDirectoryAPI(retrofit: Retrofit): DirectoryAPI { + return retrofit.create(DirectoryAPI::class.java) + } + @Provides @JvmStatic fun providesParser(): Parser { @@ -127,6 +143,12 @@ internal abstract class RoomModule { @Binds abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask + @Binds + abstract fun bindGetRoomDirectoryVisibilityTask(task: DefaultGetRoomDirectoryVisibilityTask): GetRoomDirectoryVisibilityTask + + @Binds + abstract fun bindSetRoomDirectoryVisibilityTask(task: DefaultSetRoomDirectoryVisibilityTask): SetRoomDirectoryVisibilityTask + @Binds abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask @@ -181,9 +203,15 @@ internal abstract class RoomModule { @Binds abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask + @Binds + abstract fun bindGetRoomLocalAliasesTask(task: DefaultGetRoomLocalAliasesTask): GetRoomLocalAliasesTask + @Binds abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask + @Binds + abstract fun bindDeleteRoomAliasTask(task: DefaultDeleteRoomAliasTask): DeleteRoomAliasTask + @Binds abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask 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 695be3f633..9793750fa0 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 @@ -16,28 +16,38 @@ package org.matrix.android.sdk.internal.session.room.alias -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 org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasAvailabilityChecker.Companion.toFullLocalAlias +import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface AddRoomAliasTask : Task { data class Params( val roomId: String, - val roomAlias: String + /** + * the local part of the alias. + * Ex: for the alias "#my_alias:example.org", the local part is "my_alias" + */ + val aliasLocalPart: String ) } internal class DefaultAddRoomAliasTask @Inject constructor( - private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val directoryAPI: DirectoryAPI, + private val aliasAvailabilityChecker: RoomAliasAvailabilityChecker, private val eventBus: EventBus ) : AddRoomAliasTask { override suspend fun execute(params: AddRoomAliasTask.Params) { + aliasAvailabilityChecker.check(params.aliasLocalPart) + executeRequest(eventBus) { - apiCall = roomAPI.addRoomAlias( - roomAlias = params.roomAlias, + apiCall = 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/DefaultAliasService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt new file mode 100644 index 0000000000..b6c69224e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt @@ -0,0 +1,41 @@ +/* + * 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.alias + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.session.room.alias.AliasService + +internal class DefaultAliasService @AssistedInject constructor( + @Assisted private val roomId: String, + private val getRoomLocalAliasesTask: GetRoomLocalAliasesTask, + private val addRoomAliasTask: AddRoomAliasTask +) : AliasService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): AliasService + } + + override suspend fun getRoomAliases(): List { + return getRoomLocalAliasesTask.execute(GetRoomLocalAliasesTask.Params(roomId)) + } + + override suspend fun addAlias(aliasLocalPart: String) { + addRoomAliasTask.execute(AddRoomAliasTask.Params(roomId, aliasLocalPart)) + } +} 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 new file mode 100644 index 0000000000..3400fd994c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt @@ -0,0 +1,43 @@ +/* + * 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.alias + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface DeleteRoomAliasTask : Task { + data class Params( + val roomAlias: String + ) +} + +internal class DefaultDeleteRoomAliasTask @Inject constructor( + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) : DeleteRoomAliasTask { + + override suspend fun execute(params: DeleteRoomAliasTask.Params) { + executeRequest(eventBus) { + apiCall = directoryAPI.deleteRoomAlias( + roomAlias = params.roomAlias + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetAliasesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetAliasesResponse.kt new file mode 100644 index 0000000000..5965924085 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetAliasesResponse.kt @@ -0,0 +1,28 @@ +/* + * 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.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GetAliasesResponse( + /** + * Required. The server's local aliases on the room. Can be empty. + */ + @Json(name = "aliases") val aliases: List = emptyList() +) 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 8b011980d0..3c47ee6ef0 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 @@ -17,15 +17,16 @@ package org.matrix.android.sdk.internal.session.room.alias import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.findByAlias import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI import org.matrix.android.sdk.internal.task.Task -import io.realm.Realm -import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface GetRoomIdByAliasTask : Task> { @@ -37,7 +38,7 @@ internal interface GetRoomIdByAliasTask : Task(null) } else { - roomId = executeRequest(eventBus) { - apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) - }.roomId + roomId = tryOrNull("## Failed to get roomId from alias") { + executeRequest(eventBus) { + apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias) + } + }?.roomId Optional.from(roomId) } } 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 new file mode 100644 index 0000000000..7cfce4ecdc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt @@ -0,0 +1,44 @@ +/* + * 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.alias + +import org.greenrobot.eventbus.EventBus +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 + +internal interface GetRoomLocalAliasesTask : Task> { + data class Params( + val roomId: String + ) +} + +internal class DefaultGetRoomLocalAliasesTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetRoomLocalAliasesTask { + + 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(eventBus) { + apiCall = 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 new file mode 100644 index 0000000000..25ba493891 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -0,0 +1,65 @@ +/* + * 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.alias + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import javax.inject.Inject + +internal class RoomAliasAvailabilityChecker @Inject constructor( + @UserId private val userId: String, + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) { + /** + * @param aliasLocalPart the local part of the alias. + * Ex: for the alias "#my_alias:example.org", the local part is "my_alias" + */ + @Throws(RoomAliasError::class) + suspend fun check(aliasLocalPart: String?) { + if (aliasLocalPart.isNullOrEmpty()) { + throw RoomAliasError.AliasEmpty + } + // Check alias availability + val fullAlias = aliasLocalPart.toFullLocalAlias(userId) + try { + executeRequest(eventBus) { + apiCall = directoryAPI.getRoomIdByAlias(fullAlias) + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == 404) { + // This is a 404, so the alias is available: nominal case + null + } else { + // Other error, propagate it + throw throwable + } + } + ?.let { + // Alias already exists: error case + throw RoomAliasError.AliasNotAvailable + } + } + + companion object { + internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.substringAfter(":") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt index c30f11b9af..13d403e2e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -74,8 +74,8 @@ internal data class CreateRoomBody( val invite3pids: List?, /** - * Extra keys to be added to the content of the m.room.create. - * The server will clobber the following keys: creator. + * Extra keys, such as m.federate, to be added to the content of the m.room.create event. + * The server will clobber the following keys: creator, room_version. * Future versions of the specification may allow the server to clobber other keys. */ @Json(name = "creation_content") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 632fcab70b..79ff9db087 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -81,7 +81,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( topic = params.topic, invitedUserIds = params.invitedUserIds, invite3pids = invite3pids, - creationContent = params.creationContent, + creationContent = params.creationContent.takeIf { it.isNotEmpty() }, initialStates = initialStates, preset = params.preset, isDirect = params.isDirect, 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 4f0aaf083d..ef792ab98e 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 @@ -22,6 +22,7 @@ import kotlinx.coroutines.TimeoutCancellationException import org.greenrobot.eventbus.EventBus 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.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset @@ -33,6 +34,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase 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.alias.RoomAliasAvailabilityChecker import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask @@ -46,6 +48,7 @@ internal interface CreateRoomTask : Task internal class DefaultCreateRoomTask @Inject constructor( private val roomAPI: RoomAPI, @SessionDatabase private val monarchy: Monarchy, + private val aliasAvailabilityChecker: RoomAliasAvailabilityChecker, private val directChatsHelper: DirectChatsHelper, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val readMarkersTask: SetReadMarkersTask, @@ -61,6 +64,14 @@ internal class DefaultCreateRoomTask @Inject constructor( ?: throw IllegalStateException("You can't create a direct room without an invitedUser") } else null + if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) { + try { + aliasAvailabilityChecker.check(params.roomAliasName) + } catch (aliasError: RoomAliasError) { + throw CreateRoomFailure.AliasError(aliasError) + } + } + val createRoomBody = createRoomBodyBuilder.build(params) val createRoomResponse = try { @@ -68,14 +79,18 @@ internal class DefaultCreateRoomTask @Inject constructor( apiCall = roomAPI.createRoom(createRoomBody) } } catch (throwable: Throwable) { - if (throwable is Failure.ServerError - && throwable.httpCode == 403 - && throwable.error.code == MatrixError.M_FORBIDDEN - && throwable.error.message.startsWith("Federation denied with")) { - throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error) - } else { - throw throwable + if (throwable is Failure.ServerError) { + if (throwable.httpCode == 403 + && throwable.error.code == MatrixError.M_FORBIDDEN + && throwable.error.message.startsWith("Federation denied with")) { + throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error) + } else if (throwable.httpCode == 400 + && throwable.error.code == MatrixError.M_UNKNOWN + && throwable.error.message == "Invalid characters in room alias") { + throw CreateRoomFailure.AliasError(RoomAliasError.AliasInvalid) + } } + throw throwable } val roomId = createRoomResponse.roomId // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) 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 new file mode 100644 index 0000000000..fbdd6a03eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt @@ -0,0 +1,44 @@ +/* + * 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.directory + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +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 + +internal interface GetRoomDirectoryVisibilityTask : Task { + data class Params( + val roomId: String + ) +} + +internal class DefaultGetRoomDirectoryVisibilityTask @Inject constructor( + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) : GetRoomDirectoryVisibilityTask { + + override suspend fun execute(params: GetRoomDirectoryVisibilityTask.Params): RoomDirectoryVisibility { + return executeRequest(eventBus) { + apiCall = 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 new file mode 100644 index 0000000000..33b12aa1ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt @@ -0,0 +1,47 @@ +/* + * 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.directory + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +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 + +internal interface SetRoomDirectoryVisibilityTask : Task { + data class Params( + val roomId: String, + val roomDirectoryVisibility: RoomDirectoryVisibility + ) +} + +internal class DefaultSetRoomDirectoryVisibilityTask @Inject constructor( + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) : SetRoomDirectoryVisibilityTask { + + override suspend fun execute(params: SetRoomDirectoryVisibilityTask.Params) { + executeRequest(eventBus) { + apiCall = directoryAPI.setRoomDirectoryVisibility( + params.roomId, + RoomDirectoryVisibilityJson(visibility = params.roomDirectoryVisibility) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt index 92e16a3501..93fbfb4df0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -19,18 +19,14 @@ package org.matrix.android.sdk.internal.session.room.draft import androidx.lifecycle.LiveData import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.UserDraft -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, private val draftRepository: DraftRepository, - private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : DraftService { @@ -43,14 +39,14 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, * or even move an existing draft to the top of the list */ - override fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + override suspend fun saveDraft(draft: UserDraft) { + withContext(coroutineDispatchers.main) { draftRepository.saveDraft(roomId, draft) } } - override fun deleteDraft(callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + override suspend fun deleteDraft() { + withContext(coroutineDispatchers.main) { draftRepository.deleteDraft(roomId) } } 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 7f3796c1ce..784b610af7 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 @@ -21,7 +21,6 @@ import org.matrix.android.sdk.R 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.RoomAliasesContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -71,12 +70,6 @@ internal class RoomDisplayNameResolver @Inject constructor( return name } - val aliases = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root - name = ContentMapper.map(aliases?.content).toModel()?.aliases?.firstOrNull() - if (!name.isNullOrEmpty()) { - return name - } - val roomMembers = RoomMemberHelper(realm, roomId) val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() @@ -93,6 +86,8 @@ internal class RoomDisplayNameResolver @Inject constructor( } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + val invitedCount = roomSummary?.invitedMembersCount ?: 0 + val joinedCount = roomSummary?.joinedMembersCount ?: 0 val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes.mapNotNull { userId -> roomMembers.getLastRoomMember(userId)?.takeIf { @@ -102,22 +97,49 @@ internal class RoomDisplayNameResolver @Inject constructor( } else { activeMembers.where() .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) - .limit(3) + .limit(5) .findAll() .createSnapshot() } val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { - 0 -> stringProvider.getString(R.string.room_displayname_empty_room) + 0 -> { + stringProvider.getString(R.string.room_displayname_empty_room) + // TODO (was xx and yyy) ... + } 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) - 2 -> stringProvider.getString(R.string.room_displayname_two_members, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - resolveRoomMemberName(otherMembersSubset[1], roomMembers) - ) - else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + 2 -> { + stringProvider.getString(R.string.room_displayname_two_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) + ) + } + 3 -> { + stringProvider.getString(R.string.room_displayname_3_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers), + resolveRoomMemberName(otherMembersSubset[2], roomMembers) + ) + } + 4 -> { + stringProvider.getString(R.string.room_displayname_4_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers), + resolveRoomMemberName(otherMembersSubset[2], roomMembers), + resolveRoomMemberName(otherMembersSubset[3], roomMembers) + ) + } + else -> { + val remainingCount = invitedCount + joinedCount - otherMembersCount + 1 + stringProvider.getQuantityString( + R.plurals.room_displayname_four_and_more_members, + remainingCount, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers), + resolveRoomMemberName(otherMembersSubset[2], roomMembers), + remainingCount + ) + } } } return name ?: roomId 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 7105a2cc22..2a7c46bd42 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 @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.session.room.membership +import io.realm.Realm +import io.realm.RealmQuery import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity @@ -25,8 +27,6 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie 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 io.realm.Realm -import io.realm.RealmQuery /** * This class is an helper around STATE_ROOM_MEMBER events. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt index 8797b0c764..67ae55c066 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -21,21 +21,16 @@ import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.model.PushRuleEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String, private val setRoomNotificationStateTask: SetRoomNotificationStateTask, - @SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor) + @SessionDatabase private val monarchy: Monarchy) : RoomPushRuleService { @AssistedInject.Factory @@ -49,12 +44,8 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted } } - override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable { - return setRoomNotificationStateTask - .configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) { - this.callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun setRoomNotificationState(roomNotificationState: RoomNotificationState) { + setRoomNotificationStateTask.execute(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) } private fun getPushRuleForRoom(): LiveData { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt index 384c544ee0..cac87a9d30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt @@ -18,14 +18,9 @@ package org.matrix.android.sdk.internal.session.room.reporting import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.reporting.ReportingService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, - private val taskExecutor: TaskExecutor, private val reportContentTask: ReportContentTask ) : ReportingService { @@ -34,13 +29,8 @@ internal class DefaultReportingService @AssistedInject constructor(@Assisted pri fun create(roomId: String): ReportingService } - override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable { + override suspend fun reportContent(eventId: String, score: Int, reason: String) { val params = ReportContentTask.Params(roomId, eventId, score, reason) - - return reportContentTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + reportContentTask.execute(params) } } 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 3463b26c8a..6015d945c4 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 @@ -24,7 +24,13 @@ import org.matrix.android.sdk.api.MatrixCallback 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.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.Cancelable import org.matrix.android.sdk.api.util.JsonDict @@ -104,18 +110,19 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } - override fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable { - return addRoomAliasTask - .configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) { - this.callback = callback - } - .executeBy(taskExecutor) - } - - override fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable { + override fun updateCanonicalAlias(alias: String?, altAliases: List, callback: MatrixCallback): Cancelable { return sendStateEvent( eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, - body = mapOf("alias" to alias), + body = RoomCanonicalAliasContent( + canonicalAlias = alias, + alternativeAliases = altAliases + // Ensure there is no duplicate + .distinct() + // Ensure the canonical alias is not also included in the alt alias + .minus(listOfNotNull(alias)) + // Sort for the cleanup + .sorted() + ).toContent(), callback = callback, stateKey = null ) @@ -130,6 +137,31 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } + override fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + if (joinRules != null) { + awaitCallback { + sendStateEvent( + eventType = EventType.STATE_ROOM_JOIN_RULES, + body = RoomJoinRulesContent(joinRules).toContent(), + callback = it, + stateKey = null + ) + } + } + if (guestAccess != null) { + awaitCallback { + sendStateEvent( + eventType = EventType.STATE_ROOM_GUEST_ACCESS, + body = RoomGuestAccessContent(guestAccess).toContent(), + callback = it, + stateKey = null + ) + } + } + } + } + override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt index 932cb5d67e..d6c02f0a49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt @@ -18,15 +18,10 @@ package org.matrix.android.sdk.internal.session.room.tags import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.tags.TagsService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultTagsService @AssistedInject constructor( @Assisted private val roomId: String, - private val taskExecutor: TaskExecutor, private val addTagToRoomTask: AddTagToRoomTask, private val deleteTagFromRoomTask: DeleteTagFromRoomTask ) : TagsService { @@ -36,21 +31,13 @@ internal class DefaultTagsService @AssistedInject constructor( fun create(roomId: String): TagsService } - override fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable { + override suspend fun addTag(tag: String, order: Double?) { val params = AddTagToRoomTask.Params(roomId, tag, order) - return addTagToRoomTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + addTagToRoomTask.execute(params) } - override fun deleteTag(tag: String, callback: MatrixCallback): Cancelable { + override suspend fun deleteTag(tag: String) { val params = DeleteTagFromRoomTask.Params(roomId, tag) - return deleteTagFromRoomTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + deleteTagFromRoomTask.execute(params) } } 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 df2d238c05..783aa53ddf 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 @@ -103,7 +103,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .findAll() ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } } - ?: emptyList() + .orEmpty() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt index 824bd23c01..895f1cf50d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt @@ -18,17 +18,12 @@ package org.matrix.android.sdk.internal.session.room.uploads import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult import org.matrix.android.sdk.api.session.room.uploads.UploadsService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultUploadsService @AssistedInject constructor( @Assisted private val roomId: String, - private val taskExecutor: TaskExecutor, private val getUploadsTask: GetUploadsTask, private val cryptoService: CryptoService ) : UploadsService { @@ -38,11 +33,7 @@ internal class DefaultUploadsService @AssistedInject constructor( fun create(roomId: String): UploadsService } - override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable { - return getUploadsTask - .configureWith(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult { + return getUploadsTask.execute(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt index 2ba1eebe61..8033b0654d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt @@ -16,40 +16,31 @@ package org.matrix.android.sdk.internal.session.search -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.api.session.search.SearchService -import org.matrix.android.sdk.api.util.Cancelable import javax.inject.Inject -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultSearchService @Inject constructor( - private val taskExecutor: TaskExecutor, private val searchTask: SearchTask ) : SearchService { - override fun search(searchTerm: String, - roomId: String, - nextBatch: String?, - orderByRecent: Boolean, - limit: Int, - beforeLimit: Int, - afterLimit: Int, - includeProfile: Boolean, - callback: MatrixCallback): Cancelable { - return searchTask - .configureWith(SearchTask.Params( - searchTerm = searchTerm, - roomId = roomId, - nextBatch = nextBatch, - orderByRecent = orderByRecent, - limit = limit, - beforeLimit = beforeLimit, - afterLimit = afterLimit, - includeProfile = includeProfile - )) { - this.callback = callback - }.executeBy(taskExecutor) + override suspend fun search(searchTerm: String, + roomId: 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, + orderByRecent = orderByRecent, + limit = limit, + beforeLimit = beforeLimit, + afterLimit = afterLimit, + includeProfile = includeProfile + )) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt index da28199f1b..fc476a3dd6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.sync.model.SyncResponse @@ -39,6 +40,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: toDevice.events?.forEachIndexed { index, event -> initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt()) // Decrypt event if necessary + Timber.i("## CRYPTO | To device event from ${event.senderId} of type:${event.type}") decryptToDeviceEvent(event, null) if (event.getClearType() == EventType.MESSAGE && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { @@ -69,7 +71,12 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: result = cryptoService.decryptEvent(event, timelineId ?: "") } catch (exception: MXCryptoError) { event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) - Timber.e("## CRYPTO | Failed to decrypt to device event: ${event.mCryptoError ?: exception}") + val senderKey = event.content.toModel()?.senderKey ?: "" + // try to find device id to ease log reading + val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull { + it.identityKey() == senderKey + }?.deviceId ?: senderKey + Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") } if (null != result) { @@ -80,6 +87,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ) return true + } else { + // should not happen + Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt index 1655e551f1..f4f3e6ce43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt @@ -28,7 +28,7 @@ internal class RoomTypingUsersHandler @Inject constructor(@UserId private val us fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { val roomMemberHelper = RoomMemberHelper(realm, roomId) - val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList() + val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty() val senderInfo = typingIds.map { userId -> val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) SenderInfo( 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 427a8896c9..77289f04b4 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 @@ -17,18 +17,21 @@ package org.matrix.android.sdk.internal.session.sync import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import retrofit2.Call import retrofit2.http.GET -import retrofit2.http.Headers +import retrofit2.http.Header import retrofit2.http.QueryMap internal interface SyncAPI { - /** - * Set all the timeouts to 1 minute + * Set all the timeouts to 1 minute by default */ - @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync") - fun sync(@QueryMap params: Map): Call + 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 } 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 303bb45419..b4fd6e7386 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.R import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.filter.FilterRepository @@ -78,8 +79,13 @@ internal class DefaultSyncTask @Inject constructor( // Maybe refresh the home server capabilities data we know getHomeServerCapabilitiesTask.execute(Unit) + val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) + val syncResponse = executeRequest(eventBus) { - apiCall = syncAPI.sync(requestParams) + apiCall = syncAPI.sync( + params = requestParams, + readTimeOut = readTimeOut + ) } syncResponseHandler.handleResponse(syncResponse, token) if (isInitialSync) { @@ -87,4 +93,8 @@ internal class DefaultSyncTask @Inject constructor( } Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") } + + companion object { + private const val TIMEOUT_MARGIN: Long = 10_000 + } } 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 5eb97cee3a..41914cc799 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,11 +17,10 @@ package org.matrix.android.sdk.internal.session.terms import dagger.Lazy -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.withContext 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 -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.RetrofitFactory @@ -33,8 +32,6 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTe 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.ensureTrailingSlash import okhttp3.OkHttpClient @@ -49,13 +46,11 @@ internal class DefaultTermsService @Inject constructor( private val getOpenIdTokenTask: GetOpenIdTokenTask, private val identityRegisterTask: IdentityRegisterTask, private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : TermsService { - override fun getTerms(serviceType: TermsService.ServiceType, - baseUrl: String, - callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + 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") @@ -64,12 +59,11 @@ internal class DefaultTermsService @Inject constructor( } } - override fun agreeToTerms(serviceType: TermsService.ServiceType, - baseUrl: String, - agreedUrls: List, - token: String?, - callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + 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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt index 2b7ff2624a..c5c3fc4b59 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt @@ -37,6 +37,6 @@ internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTrac } override fun getTypingUsers(roomId: String): List { - return typingUsers[roomId] ?: emptyList() + return typingUsers[roomId].orEmpty() } } 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 d2eb7a14ef..1740956915 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 @@ -19,10 +19,13 @@ 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 @@ -32,12 +35,40 @@ 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 { override fun getUser(userId: String): User? { return userDataSource.getUser(userId) } + override fun resolveUser(userId: String, callback: MatrixCallback) { + val known = getUser(userId) + if (known != null) { + callback.onSuccess(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) + } + } + override fun getUserLive(userId: String): LiveData> { return userDataSource.getUserLive(userId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt index 5c8cbd08b1..c030872dad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt @@ -18,12 +18,15 @@ package org.matrix.android.sdk.internal.session.user import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.util.awaitTransaction import javax.inject.Inject internal interface UserStore { suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null) + suspend fun updateAvatar(userId: String, avatarUrl: String? = null) + suspend fun updateDisplayName(userId: String, displayName: String? = null) } internal class RealmUserStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : UserStore { @@ -34,4 +37,20 @@ internal class RealmUserStore @Inject constructor(@SessionDatabase private val m it.insertOrUpdate(userEntity) } } + + override suspend fun updateAvatar(userId: String, avatarUrl: String?) { + monarchy.awaitTransaction { realm -> + UserEntity.where(realm, userId).findFirst()?.let { + it.avatarUrl = avatarUrl ?: "" + } + } + } + + override suspend fun updateDisplayName(userId: String, displayName: String?) { + monarchy.awaitTransaction { realm -> + UserEntity.where(realm, userId).findFirst()?.let { + it.displayName = displayName ?: "" + } + } + } } 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 22bdd2c6e4..329903f15b 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 @@ -138,7 +138,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager: ): LiveData> { val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) return Transformations.map(widgetsAccountData) { - it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList() + it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes).orEmpty() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index 3d80ad01d5..e19b1bcca7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import java.security.MessageDigest +import java.util.Locale /** * Compute a Hash of a String, using md5 algorithm @@ -26,7 +27,7 @@ fun String.md5() = try { digest.update(toByteArray()) digest.digest() .joinToString("") { String.format("%02X", it) } - .toLowerCase() + .toLowerCase(Locale.ROOT) } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index 32997e2064..ecfbe311f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -50,8 +50,6 @@ fun convertFromUTF8(s: String): String { } } -fun String.withoutPrefix(prefix: String) = if (startsWith(prefix)) substringAfter(prefix) else this - /** * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. * diff --git a/matrix-sdk-android/src/main/res/values-ca/strings.xml b/matrix-sdk-android/src/main/res/values-ca/strings.xml index 2dc2206c8c..8ba8c9acfd 100644 --- a/matrix-sdk-android/src/main/res/values-ca/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ca/strings.xml @@ -1,79 +1,217 @@ - + %1$s: %2$s %1$s ha enviat una imatge. - - %1s ha sortit - %1s ha entrat + %1$s ha marxat de la sala + %1$s s\'ha unit a la sala Número de telèfon - Correu electrònic - Missatge encriptat - - la invitació de %s + Missatge xifrat + invitació de %s %1$s ha convidat a %2$s - %1$s us ha convidat + %1$s t\'ha convidat %1$s ha rebutjat la invitació - %1$s ha fet fora a %2$s - - - %1$s ha canviat el seu nom visible de %2$s a %3$s - %1$s ha eliminat el seu nom visible (%2$s) + %1$s ha expulsat %2$s + %1$s ha canviat el seu nom de visualització de %2$s a %3$s + %1$s ha eliminat el seu nom de visualització (era %2$s) %1$s ha canviat el tema a: %2$s %1$s ha canviat el nom de la sala a: %2$s - %s ha contestat la trucada. + %s ha respost a la trucada. %s ha finalitzat la trucada. - tots el membres de la sala, des del punt en què són convidats. - tots els membres de la sala. + tots el participants de la sala, des de que són convidats. + tots els participants de la sala. desconegut (%s). - %1$s ha activat l\'encriptació d\'extrem a extrem (%2$s) - + %1$s ha activat el xifrat d\'extrem a extrem (%2$s) %1$s ha sol·licitat una conferència VoIP - %1$s ha readmès a %2$s - %1$s ha vetat a %2$s + %1$s ha tret el veto a %2$s + %1$s ha vetat %2$s %1$s ha retirat la invitació de %2$s %1$s ha canviat el seu avatar - %1$s ha permès a %2$s veure l\'historial que es generi a partir d\'ara - tots els membres de la sala, des del punt en què hi entrin. + %1$s ha establert la visibilitat de l\'historial futur de la sala a %2$s + tots els participants de la sala, des de que s\'hi uneixen. qualsevol. S\'ha iniciat la conferència VoIP - S\'ha finalitzat la conferència de veu IP - - (s\'ha canviat també l\'avatar) + Ha finalitzat la conferència VoIP + (també ha canviat l\'avatar) %1$s ha eliminat el nom de la sala %1$s ha eliminat el tema de la sala %1$s ha actualitzat el seu perfil %2$s - %1$s ha enviat una invitació a %2$s per a entrar a la sala - %1$s ha acceptat la invitació per a %2$s - - ** No s\'ha pogut desencriptar: %s ** + %1$s ha enviat una invitació a %2$s perquè s\'uneixi a la sala + %1$s ha acceptat la invitació de %2$s + ** No s\'ha pogut desxifrar: %s ** El dispositiu del remitent no ens ha enviat les claus per aquest missatge. - No s\'ha pogut redactar No s\'ha pogut enviar el missatge - No s\'ha pogut pujar la imatge - - S\'ha produït un error de xarxa - S\'ha produït un error de Matrix - - Actualment no es pot tornar a entrar a una sala buida. - - %1$s a canviat el seu nom visible a %2$s - %s ha iniciat una trucada de vídeo. - %s ha iniciat una trucada de veu. - + Error de xarxa + Error de Matrix + Ara per ara no és possible tornar a unir-se a una sala buida. + %1$s a canviat el seu nom de visualització a %2$s + %s ha realitzat una videotrucada. + %s ha realitzat una trucada de veu. - Convidat per %s - Convideu a la sala + Invitació de %s + Convida a la sala %1$s i %2$s Sala buida %1$s i 1 altre %1$s i %2$d altres - - %1$s ha enviat un adhesiu. - - + %s s\'ha actualitzat aquí. + Ho has actualitzat aquí. + %s està sol·licitant la verificació de la teva clau, però el teu client no admet la verificació de clau des del xat. Hauràs d\'utilitzar la verificació de claus heretada per fer la verificació. + Has activat el xifrat d\'extrem a extrem (algorisme %1$s no reconegut). + %1$s ha activat el xifrat d\'extrem a extrem (algorisme %2$s no reconegut). + Has activat el xifrat d\'extrem a extrem. + %1$s ha activat el xifrat d\'extrem a extrem. + Has impedit que els convidats es puguin unir a la sala. + %1$s ha impedit que els convidats es puguin unir a la sala. + Has impedit que els convidats es puguin unir a la sala. + %1$s ha impedit que els convidats es puguin unir a la sala. + Has permès que els convidats s\'uneixin aquí. + %1$s ha permès que els convidats s\'uneixin aquí. + Has permès que els convidats s\'uneixin a la sala. + %1$s ha permès que els convidats s\'uneixin a la sala. + Has eliminat l\'adreça principal d\'aquesta sala. + %1$s ha eliminat l\'adreça principal d\'aquesta sala. + Has establert l\'adreça principal d\'aquesta sala a %1$s. + %1$s ha establert l\'adreça principal d\'aquesta sala a %2$s. + Has afegit %1$s i has eliminat %2$s d\'aquesta sala (adreces). + %1$s ha afegit %2$s i ha eliminat %3$s d\'aquesta sala (adreces). + + Has eliminat l\'adreça %1$s d\'aquesta sala. + Has eliminat les adreces %1$s d\'aquesta sala. + + + %1$s ha eliminat l\'adreça %2$s d\'aquesta sala. + %1$s ha eliminat les adreces %3$s d\'aquesta sala. + + + Has afegit l\'adreça %1$s a aquesta sala. + Has afegit les adreces %1$s a aquesta sala. + + + %1$s ha afegit l\'adreça %2$s a aquesta sala. + %1$s ha afegit les adreces %2$s a aquesta sala. + + Has revocat la invitació de %1$s perquè s\'uneixi a la sala. Motiu: %2$s + %1$s ha revocat la invitació de %2$s perquè s\'uneixi a la sala. Motiu: %3$s + Has revocat la invitació de %1$s + %1$s ha revocat la invitació de %2$s + Has revocat la invitació de %1$s perquè s\'uneixi a la sala + %1$s ha revocat la invitació de %2$s perquè s\'uneixi a la sala + Has retirat la invitació de %1$s. Motiu: %2$s + %1$s ha retirat la invitació de %2$s. Motiu: %3$s + Has acceptat la invitació de %1$s. Motiu: %2$s + %1$s ha acceptat la invitació de %2$s. Motiu: %3$s + Has enviat una invitació a %1$s perquè s\'uneixi a la sala. Motiu: %2$s + %1$s ha enviat una invitació a %2$s perquè s\'uneixi a la sala. Motiu: %3$s + Has vetat %1$s. Motiu: %2$s + %1$s ha vetat %2$s. Motiu: %3$s + Has tret el veto a %1$s. Motiu: %2$s + %1$s ha tret el veto a %2$s. Motiu: %3$s + Has vetat %1$s + Has marxat de la sala. Motiu: %1$s + %1$s ha marxat de la sala. Motiu: %2$s + Has marxat de la sala + %1$s ha marxat de la sala + Has marxat de la sala + Has expulsat %1$s + Has expulsat %1$s. Motiu: %2$s + %1$s ha expulsat %2$s. Motiu: %3$s + Has rebutjat la invitació. Motiu: %1$s + %1$s ha rebutjat la invitació. Motiu: %2$s + Has marxat. Motiu: %1$s + %1$s ha marxat. Motiu: %2$s + T\'has unit. Motiu: %1$s + %1$s s\'ha unit. Motiu: %2$s + T\'has unit a la sala. Motiu: %1$s + %1$s s\'ha unit a la sala. Motiu: %2$s + %1$s t\'ha convidat. Motiu: %2$s + Has convidat %1$s. Motiu: %2$s + %1$s ha convidat %2$s. Motiu: %3$s + La teva invitació. Motiu: %1$s + la invitació de %1$s. Motiu: %2$s + Esborra la cua d\'enviament + Enviant missatge… + Sincronització inicial: +\nImportant dades del compte + Sincronització inicial: +\nImportant comunitats + Sincronització inicial: +\nImportant sales que deixat + Sincronització inicial: +\nImportant compte… + Sincronització inicial: +\nImportant xifrat + Sincronització inicial: +\nImportant sales + Sincronització inicial: +\nImportant sales on hi estàs convidat + Sincronització inicial: +\nImportant sales on hi estàs unit + %1$s de %2$s a %3$s + %1$s ha canviat el nivell d\'autoritat de %2$s. + Has canviat el nivell d\'autoritat de %1$s. + Personalitzat + Personalitzat (%1$d) + Predeterminat + Moderador + Administrador + Has modificat el giny %1$s + %1$s ha modificat el giny %2$s + Has eliminat el giny %1$s + %1$s ha eliminat el giny %2$s + Has afegit el giny %1$s + %1$s ha afegit el giny %2$s + Has acceptat la invitació de %1$s + Has convidat a %1$s + %1$s ha convidat a %2$s + Has enviat una invitació a %1$s perquè s\'uneixi a la sala + Has actualitzat el teu perfil %1$s + Missatge eliminat per %1$s [motiu: %2$s] + Missatge eliminat [motiu: %1$s] + Missatge eliminat per %1$s + Missatge eliminat + Has eliminat l\'avatar de la sala + %1$s ha eliminat l\'avatar de la sala + Has eliminat el tema de la sala + Has eliminat el nom de la sala + Has sol·licitat una conferència VoIP + Has actualitzat aquesta sala. + %s ha actualitzat aquesta sala. + Has activat el xifrat d\'extrem a extrem (%1$s) + Has establert la visibilitat dels missatges futurs a %1$s + %1$s ha establert la visibilitat dels missatges futurs a %2$s + Has establert la visibilitat de l\'historial futur de la sala a %1$s + Has finalitzat la trucada. + Has respost a la trucada. + Has enviat dades per configurar la trucada. + %s ha enviat dades per configurar la trucada. + Has realitzat una trucada de veu. + Has realitzat una videotrucada. + Has canviat el nom de la sala a: %1$s + Has canviat l\'avatar de la sala + %1$s ha canviat l\'avatar de la sala + Has canviat el tema a: %1$s + Has eliminat el teu nom de visualització (era %1$s) + Has canviat el teu nom de visualització de %1$s a %2$s + Has canviat el teu nom de visualització a %1$s + Has canviat el teu avatar + Has retirat la invitació de %1$s + Has tret el veto a %1$s + Has rebutjat la invitació + Has creat la discussió + %1$s ha creat la discussió + T\'has unit + %1$s s\'ha unit + T\'has unit a la sala + Has convidat a %1$s + Has creat la sala + %1$s ha creat la sala + La teva invitació + Has enviat un adhesiu. + Has enviat una imatge. + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml index e2d09c7857..3648ca3a72 100644 --- a/matrix-sdk-android/src/main/res/values-es/strings.xml +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -1,9 +1,7 @@ - + - %1$s: %2$s %1$s envió una imagen. - la invitación de %s %1$s invitó a %2$s %1$s te ha invitado @@ -30,61 +28,44 @@ todos los miembros de la sala. todos. desconocido (%s). - %1$s activó el cifrado de extremo a extremo (%2$s) - + %1$s ha activado la encriptación de Extremo-a-Extremo (%2$s) %1$s solicitó una conferencia de vozIP conferencia de vozIP iniciada conferencia de vozIP finalizada - (el avatar también se cambió) %1$s eliminó el nombre de la sala %1$s eliminó el tema de la sala %1$s actualizó su perfil %2$s %1$s invitó a %2$s a unirse a la sala %1$s aceptó la invitación para %2$s - ** No es posible descifrar: %s ** El dispositivo emisor no nos ha enviado las claves para este mensaje. - No se pudo redactar No es posible enviar el mensaje - No se pudo cargar la imagen - Error de red Error de Matrix - - - - Actualmente no es posible volver a unirse a una sala vacía. - - Mensaje cifrado - + Mensaje encriptado Dirección de correo electrónico Número telefónico - %1$s envió una pegatina. - Invitación de %s Invitación a Sala %1$s y %2$s Sala vacía - %1$s y 1 otro %1$s y %2$d otros - - Mensaje eliminado Mensaje eliminado por %1$s Mensaje eliminado [motivo: %1$s] @@ -98,10 +79,8 @@ \nImportando Comunidades Sincronización Inicial: \nImportando Datos de la Cuenta - Enviando mensaje… Borrar cola de envío - %1$s ha invitado a %2$s. Razón: %3$s %1$s te ha invitado. Razón: %2$s %1$s se ha unido. Razón: %2$s @@ -111,9 +90,7 @@ %1$s ha baneado a %2$s. Razón: %3$s %1$s ha aceptado la invitación para %2$s. Razón: %3$s %1$s ha eliminado la dirección principal para esta sala. - %s ha actualizado la sala. - Sincronización Inicial: \nImportando criptografía Sincronización Inicial: @@ -127,32 +104,25 @@ %1$s envió una invitación a %2$s para que se una a la sala. Razón: %3$s %1$s revocó la invitación de %2$s para unirse a la sala. Razón: %3$s %1$s ha retirado la invitación de %2$s. Razón: %3$s - %1$s ha añadido %2$s como alias de esta sala. %1$s ha añadido %2$s como alias de esta sala. - - %1$s ha quitado %2$s como alias de esta habitación. - %1$s ha quitado %2$s como alias de esta habitación. + %1$s ha quitado %2$s como alias de esta sala. + %1$s ha quitado %2$s como alias de esta sala. - %1$s ha establecido la dirección principal de esta sala a %2$s. %1$s ha permitido que los invitados se unan a la sala. %1$s ha impedido que los invitados se unan a la sala. - %1$s ha activado la encriptación extremo a extremo. %1$s ha activado la encriptación de extremo a extremo (algoritmo no reconocido %2$s). - %s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves. - Enviaste una imagen. Enviaste un sticker. - Tu invitación - %1$s creó la habitación - Tu creaste la habitación + %1$s creó la sala + Creaste la sala Invitaste a %1$s Te uniste a la Sala Dejaste la Sala @@ -167,8 +137,8 @@ Quitaste tu nombre para mostrar (era %1$s) Cambiaste el tema a: %1$s %1$s cambió el avatar de la sala - Cambiaste el avatar de la habitación - Cambiaste el nombre de la habitación a: %1$s + Cambiaste el avatar de la sala + Cambiaste el nombre de la sala a: %1$s Hiciste una videollamada. Hiciste una llamada de voz. %s envió datos para configurar la llamada. @@ -176,40 +146,35 @@ Respondiste la llamada. Terminaste la llamada. Hiciste visible el futuro historial de la %1$s - Activó el cifrado de un extremo a otro (%1$s) - Has mejorado esta habitación. - + Has activado la encriptación de Extremo-a-Extremo (%1$s) + Has actualizado esta sala. Solicitaste una conferencia de VoIP Quitaste el nombre de la sala Quitaste el tema de la sala - %1$s eliminó el avatar de la habitación - Quitaste el avatar de la habitación + %1$s eliminó el avatar de la sala + Quitaste el avatar de la sala Actualizaste tu perfil %1$s Enviaste una invitación a %1$s para unirse a la sala Revocaste la invitación para que %1$s se una a la sala Aceptaste la invitación para %1$s - %1$s agrego el widget %2$s Agregaste el widget %1$s %1$s eliminó el widget %2$s Quitaste el widget %1$s %1$s modifico el widget %2$s Modificaste el widget %1$s - Administrador Moderador Por defecto Personalizado (%1$d) Personalizado - Cambiaste el nivel de potencia de %1$s. %1$s cambió el nivel de potencia de %2$s. %1$s de %2$s a %3$s - Tu invitación. Razón: %1$s - "nvitaste a %1$s. Razón: %2$s" - Te uniste a la habitación. Razón: %1$s - Dejaste la habitación. Razón: %1$s + Invitaste a %1$s. Razón: %2$s + Te uniste a la sala. Razón: %1$s + Dejaste la sala. Razón: %1$s Rechazaste la invitación. Razón: %1$s Pateaste a %1$s. Motivo: %2$s Has desactivado a %1$s. Motivo: %2$s @@ -218,27 +183,42 @@ Revocaste la invitación para que %1$s se una a la sala. Motivo: %2$s Aceptaste la invitación para %1$s. Motivo: %2$s Retiró la invitación de %1$s\'s. Motivo: %2$s - Agregaste %1$s como dirección para esta sala. Agregaste %1$s como direcciones para esta sala. - Quitaste %1$s como dirección para esta sala. Quitaste %1$s como direcciones para esta sala. - - "%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala." + %1$s añadió %2$s y eliminó %3$s como alias para esta sala. Agregaste %1$s y quitaste %2$s como direcciones para esta sala. - Estableciste la dirección principal de esta sala en %1$s. Quitaste la dirección principal de esta sala. - Ha permitido que los invitados se unan a la sala. Ha impedido que los invitados se unan a la sala. - - Activó el cifrado de extremo a extremo. - Activó el cifrado de un extremo a otro (algoritmo %1$s no reconocido). - - + Tu has activado la encriptación de Extremo-a-Extremo. + Has activado la encriptación de Extremo-a-Extremo (algoritmo %1$s no reconocido). + Has impedido que invitados se unan a la sala. + Has permitido a invitados unirse aquí. + Te has ido. Razón: %1$s + Has revocado la invitación de %1$s + Has invitado a %1$s + Has actualizado aquí. + Has hecho futuros mensajes visibles a %1$s + Te saliste de la sala + Te uniste + Creaste la conversación + %1$s ha impedido que invitados se unan a la sala. + %1$s ha permitido a invitados a unirse aquí. + %1$s se ha ido. Razón: %2$s + Tu te has unido. Razón: %1$s + %1$s se ha unido. Razón: %2$s + %1$s ha revocado la invitación de %2$s + %1$s ha invitado %2$s + %s ha actualizado aquí. + %1$s ha hecho futuros mensajes visibles a %2$s + %1$s ha salido de la sala + %1$s se ha unido + %1$s ha creado la conversación + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml index 042fda7ddd..11a786f5ac 100644 --- a/matrix-sdk-android/src/main/res/values-fa/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -181,8 +181,8 @@ نشانی‌های %1$s را به این اتاق افزودید. - نشانی %1$s ار از این اتاق برداشتید. - نشانی‌های %1$s ار از این اتاق برداشتید. + نشانی %1$s را از این اتاق برداشتید. + نشانی‌های %1$s را از این اتاق برداشتید. نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید. نشانی اصلی این اتاق را به %1$s تنظیم کردید. diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml index 4aade76c55..49238ee8ff 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -22,10 +22,10 @@ %s hanghívást indított. %s fogadta a hívást. %s befejezte a hívást. - %1$s láthatóvá tette a jövőbeli előzményeket %2$s számára - az összes szobatag, onnantól, hogy meg lettek hívva. - az összes szobatag, onnantól, hogy csatlakoztak. - az összes szobatag. + %1$s láthatóvá tette a jövőbeli előzményeket %2$s + a szoba összes tagja számára, a meghívásuk időpontjától kezdve. + a szoba összes tagja számára, a csatlakozásuk időpontjától kezdve. + az összes szobatag számára. bárki. ismeretlen (%s). %1$s bekapcsolta a végpontok közötti titkosítást (%2$s) @@ -139,4 +139,40 @@ Létrehoztad a szobát Matricát küldtél. Képet küldtél. + Saját + Saját (%1$d) + Alapértelmezett + Moderátor + Admin + Ön megváltoztatta a %1$s kisalkalmazást + %1$s megváltoztatta a %2$s kisalkalmazást + Ön eltávolította a %1$s kisalkalmazást + %1$s eltávolította a %2$s kisalkalmazást + Ön hozzáadott egy %1$s kisalkalmazást + %1$s hozzáadott egy %2$s kisalkalmazást + Ön elfogadta a meghívót ehhez: %1$s + Ön visszavonta %1$s felhasználó meghívóját + %1$s visszavonta %2$s felhasználó meghívóját + Ön visszavonta %1$s felhasználó meghívóját + Ön meghívta %1$s felhasználót + %1$s meghívta %2$s felhasználót + Ön meghívót küldött %1$s felhasználónak, hogy csatlakozzon a szobához + Ön frissítette a saját profilját %1$s + Ön eltávolította a szoba képét + %1$s eltávolította a szoba képét + Ön eltávolította a szoba témáját + Ön eltávolította a szoba nevét + Ön videókonferencia kezdeményezését kérte + Ön frissítette ezt a szobát. + %s frissítette a szobát. + Ön frissítette ezt a szobát. + Ön bekapcsolta a végpontok közötti titkosítást (%1$s) + Ön elérhetővé tette a jövőbeni üzeneteket %1$s + Ön elérhetővé tette a jövőbeni üzeneteket %1$s + %1$s elérhetővé tette a jövőbeni üzeneteket %2$s + Ön megváltoztatta a szoba nevét erre: %1$s + Ön eltávolította a saját megjelenített nevét (%1$s volt) + Ön megváltoztatta a saját megjelenítési nevét erről: %1$s, erre: %2$s + Ön beállította a saját megjelenítési nevét erre: %1$s + Az ön meghívása \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ja/strings.xml b/matrix-sdk-android/src/main/res/values-ja/strings.xml index 366c743494..add19edfaf 100644 --- a/matrix-sdk-android/src/main/res/values-ja/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ja/strings.xml @@ -1,10 +1,8 @@ - + - %1$s: %2$s %1$sが画像を送信しました。 %1$sがスタンプを送信しました。 - %sの招待 %1$sが%2$sを招待しました %1$sがあなたを招待しました @@ -29,11 +27,9 @@ 部屋への招待 %1$sと%2$s 空の部屋 - %1$sと他%2$d名 - %1$sは、今後の部屋履歴を%2$sに表示させました 部屋のメンバー全員、招待された時点から。 部屋のメンバー全員、参加した時点から。 @@ -41,34 +37,50 @@ 誰でも。 不明 (%s)。 %1$s がエンドツーエンド暗号化を有効にしました (%2$s) - %1$s がVoIP会議をリクエストしました VoIP会議が開始されました VoIP会議が終了しました - (アバターも変更された) %1$s が部屋名を削除しました %1$s がルームトピックを削除しました %1$s がプロフィール %2$s を更新しました %1$s は %2$s に部屋に参加するよう招待状を送りました %1$sは%2$sの招待を受け入れました - ** 解読できません: %s ** 送信者の端末からこのメッセージのキーが送信されていません。 - 修正できませんでした メッセージを送信できません - 画像のアップロードに失敗しました - ネットワークエラー Matrixエラー - 現在空の部屋に再参加することはできません。 - 暗号化されたメッセージ - メールアドレス 電話番号 - - + ルームのアバターを変更しました + %1$sがルームのアバターを変更しました + トピックを%1$sに変更しました + 表示名を削除しました(%1$sでした) + 表示名を%1$sから%2$sに変更しました + 表示名を%1$sに設定しました + アバターを変更しました + %1$sの招待を取り下げました + %1$sをBANしました + %1$sのBANを解除しました + %1$sを退出させました + 招待を拒否しました + ルームから退出しました + %1$sがルームから退出しました + ルームから退出しました + 参加しました + %1$sが参加しました + ルームに参加しました + %1$sを招待しました + ディスカッションを作成しました + %1$sがディスカッションを作成しました + ルームを作成しました + %1$sがルームを作成しました + 招待 + ステッカーを送信しました。 + 画像を送信しました。 + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 27f083269f..7a0fe1d735 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -72,6 +72,23 @@ You upgraded this room. %s upgraded here. You upgraded here. + %s set the server ACLs for this room. + You set the server ACLs for this room. + • Server matching %s are banned. + • Server matching %s are allowed. + • Server matching IP literals are allowed. + • Server matching IP literals are banned. + + %s changed the server ACLs for this room. + You changed the server ACLs for this room. + • Server matching %s are now banned. + • Server matching %s were removed from the ban list. + • Server matching %s are now allowed. + • Server matching %s were removed from the allowed list. + • Server matching IP literals are now allowed. + • Server matching IP literals are now banned. + No change. + 🎉 All servers are banned from participating! This room can no longer be used. %1$s requested a VoIP conference You requested a VoIP conference @@ -158,13 +175,22 @@ %1$s and %2$s - + + %1$s, %2$s and %3$s + + %1$s, %2$s, %3$s and %4$s + + + %1$s, %2$s, %3$s and %4$d other + %1$s, %2$s, %3$s and %4$d others + %1$s and 1 other %1$s and %2$d others Empty room + Empty room (was %s) Initial Sync:\nImporting account… Initial Sync:\nImporting crypto @@ -220,7 +246,7 @@ %1$s removed %2$s as an address for this room. - %1$s removed %3$s as addresses for this room. + %1$s removed %2$s as addresses for this room. @@ -236,6 +262,33 @@ "%1$s removed the main address for this room." "You removed the main address for this room." + + %1$s added the alternative address %2$s for this room. + %1$s added the alternative addresses %2$s for this room. + + + + You added the alternative address %1$s for this room. + You added the alternative addresses %1$s for this room. + + + + %1$s removed the alternative address %2$s for this room. + %1$s removed the alternative addresses %2$s for this room. + + + + You removed the alternative address %1$s for this room. + You removed the alternative addresses %1$s for this room. + + + %1$s changed the alternative addresses for this room. + You changed the alternative addresses for this room. + %1$s changed the main and alternative addresses for this room. + You changed the main and alternative addresses for this room. + %1$s changed the addresses for this room. + You changed the addresses for this room. + "%1$s has allowed guests to join the room." "You have allowed guests to join the room." "%1$s has allowed guests to join here." diff --git a/multipicker/build.gradle b/multipicker/build.gradle index b6e500e493..7c29a5539f 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -43,8 +43,8 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" - implementation 'androidx.exifinterface:exifinterface:1.3.0' + implementation "androidx.fragment:fragment-ktx:1.3.0-beta01" + implementation 'androidx.exifinterface:exifinterface:1.3.1' // Log implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/tools/templates/configure.sh b/tools/templates/configure.sh index 0669ab1312..1b00cef927 100755 --- a/tools/templates/configure.sh +++ b/tools/templates/configure.sh @@ -19,6 +19,7 @@ echo "Configure Element Template..." if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi { +mkdir -p "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other" ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other" } && { echo "Please restart Android Studio." diff --git a/tools/templates/unconfigure.sh b/tools/templates/unconfigure.sh new file mode 100755 index 0000000000..36415c50e8 --- /dev/null +++ b/tools/templates/unconfigure.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# +# Copyright 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. +# + +# Template prevent from upgrading Android Studio, so this script de configure the template +echo "Un-configure Element Template..." +if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi + +rm "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other/ElementFeature" +rm -r "${ANDROID_STUDIO%/}/plugins/android/lib/templates" diff --git a/vector/build.gradle b/vector/build.gradle index 7b976873b5..6edaec1755 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -17,7 +17,7 @@ androidExtensions { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 0 -ext.versionPatch = 11 +ext.versionPatch = 12 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -318,9 +318,8 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.0.0" implementation 'androidx.core:core-ktx:1.3.2' @@ -365,11 +364,11 @@ dependencies { implementation "io.arrow-kt:arrow-core:$arrow_version" // Pref - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.3.0-alpha02' + implementation 'com.google.android.material:material:1.3.0-alpha04' implementation 'me.gujun.android:span:1.7' implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:html:$markwon_version" @@ -377,8 +376,8 @@ dependencies { implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" - implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10' implementation 'jp.wasabeef:glide-transformations:4.3.0' + implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // Custom Tab implementation 'androidx.browser:browser:1.2.0' @@ -422,7 +421,7 @@ dependencies { kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:20.3.0') { + gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -465,6 +464,10 @@ dependencies { androidTestImplementation "androidx.arch.core:core-testing:$arch_version" // Plant Timber tree for test androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + // "The one who serves a great Espresso" + androidTestImplementation('com.schibsted.spain:barista:3.7.0') { + exclude group: 'org.jetbrains.kotlin' + } } if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) { diff --git a/vector/lint.xml b/vector/lint.xml index 4ac0f20e51..572f937406 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -41,6 +41,7 @@ + @@ -52,6 +53,9 @@ + + + diff --git a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt index b88356db59..73ca94b148 100644 --- a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt +++ b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt @@ -67,7 +67,7 @@ class RegistrationTest { .perform(click()) // Enter local synapse - onView((withId(R.id.loginServerUrlFormHomeServerUrl))) + onView(withId(R.id.loginServerUrlFormHomeServerUrl)) .perform(typeText(homeServerUrl)) // Click on continue @@ -87,7 +87,7 @@ class RegistrationTest { .check(matches(isDisplayed())) // Ensure user id - onView((withId(R.id.loginField))) + onView(withId(R.id.loginField)) .perform(typeText(userId)) // Ensure login button not yet enabled @@ -95,7 +95,7 @@ class RegistrationTest { .check(matches(not(isEnabled()))) // Ensure password - onView((withId(R.id.passwordField))) + onView(withId(R.id.passwordField)) .perform(closeSoftKeyboard(), typeText(password)) // Submit diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt index 3ab8fe7dd9..0d0ec3dd2b 100644 --- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt +++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt @@ -79,7 +79,7 @@ class SecurityBootstrapTest : VerificationTestBase() { fun testBasicBootstrap() { val userId: String = existingSession!!.myUserId - doLogin(homeServerUrl, userId, password) + uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl) // Thread.sleep(6000) withIdlingResource(activityIdlingResource(HomeActivity::class.java)) { diff --git a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt index 2a1b6d802f..a4b9983ff4 100644 --- a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt +++ b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt @@ -18,15 +18,11 @@ package im.vector.app import android.net.Uri import androidx.lifecycle.Observer -import androidx.test.espresso.Espresso -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers +import im.vector.app.ui.UiTestBase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.hamcrest.CoreMatchers import org.junit.Assert import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixCallback @@ -43,108 +39,12 @@ abstract class VerificationTestBase { val password = "password" val homeServerUrl: String = "http://10.0.2.2:8080" - fun doLogin(homeServerUrl: String, userId: String, password: String) { - Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - .check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit))) + protected val uiTestBase = UiTestBase() - Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit)) - .perform(ViewActions.click()) - - Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - .check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title))) - - // Chose custom server - Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther)) - .perform(ViewActions.click()) - - // Enter local synapse - Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl))) - .perform(ViewActions.typeText(homeServerUrl)) - - Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isEnabled())) - .perform(ViewActions.closeSoftKeyboard(), ViewActions.click()) - - // Click on the signin button - Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSignIn)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - .perform(ViewActions.click()) - - // Ensure password flow supported - Espresso.onView(ViewMatchers.withId(R.id.loginField)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - Espresso.onView(ViewMatchers.withId(R.id.passwordField)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - - Espresso.onView((ViewMatchers.withId(R.id.loginField))) - .perform(ViewActions.typeText(userId)) - Espresso.onView(ViewMatchers.withId(R.id.loginSubmit)) - .check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled()))) - - Espresso.onView((ViewMatchers.withId(R.id.passwordField))) - .perform(ViewActions.closeSoftKeyboard(), ViewActions.typeText(password)) - - Espresso.onView(ViewMatchers.withId(R.id.loginSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isEnabled())) - .perform(ViewActions.closeSoftKeyboard(), ViewActions.click()) - } - - private fun createAccount(userId: String = "UiAutoTest", password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { - Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - .check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit))) - - Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit)) - .perform(ViewActions.click()) - - Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - .check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title))) - - // Chose custom server - Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther)) - .perform(ViewActions.click()) - - // Enter local synapse - Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl))) - .perform(ViewActions.typeText(homeServerUrl)) - - Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isEnabled())) - .perform(ViewActions.closeSoftKeyboard(), ViewActions.click()) - - // Click on the signup button - Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - .perform(ViewActions.click()) - - // Ensure password flow supported - Espresso.onView(ViewMatchers.withId(R.id.loginField)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - Espresso.onView(ViewMatchers.withId(R.id.passwordField)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - - Espresso.onView((ViewMatchers.withId(R.id.loginField))) - .perform(ViewActions.typeText(userId)) - Espresso.onView(ViewMatchers.withId(R.id.loginSubmit)) - .check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled()))) - - Espresso.onView((ViewMatchers.withId(R.id.passwordField))) - .perform(ViewActions.typeText(password)) - - Espresso.onView(ViewMatchers.withId(R.id.loginSubmit)) - .check(ViewAssertions.matches(ViewMatchers.isEnabled())) - .perform(ViewActions.closeSoftKeyboard(), ViewActions.click()) - - Espresso.onView(ViewMatchers.withId(R.id.homeDrawerFragmentContainer)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - } - - fun createAccountAndSync(matrix: Matrix, userName: String, - password: String, - withInitialSync: Boolean): Session { + fun createAccountAndSync(matrix: Matrix, + userName: String, + password: String, + withInitialSync: Boolean): Session { val hs = createHomeServerConfig() doSync { @@ -174,7 +74,7 @@ abstract class VerificationTestBase { return session } - fun createHomeServerConfig(): HomeServerConnectionConfig { + private fun createHomeServerConfig(): HomeServerConnectionConfig { return HomeServerConnectionConfig.Builder() .withHomeServerUri(Uri.parse(homeServerUrl)) .build() @@ -200,7 +100,7 @@ abstract class VerificationTestBase { return result!! } - fun syncSession(session: Session) { + private fun syncSession(session: Session) { val lock = CountDownLatch(1) GlobalScope.launch(Dispatchers.Main) { session.open() } diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index d218b6ef7e..d9005e4a63 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -78,7 +78,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() { fun checkVerifyPopup() { val userId: String = existingSession!!.myUserId - doLogin(homeServerUrl, userId, password) + uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl) // Thread.sleep(6000) withIdlingResource(activityIdlingResource(HomeActivity::class.java)) { @@ -215,10 +215,10 @@ class VerifySessionInteractiveTest : VerificationTestBase() { } fun signout() { - onView((withId(R.id.groupToolbarAvatarImageView))) + onView(withId(R.id.groupToolbarAvatarImageView)) .perform(click()) - onView((withId(R.id.homeDrawerHeaderSettingsView))) + onView(withId(R.id.homeDrawerHeaderSettingsView)) .perform(click()) onView(withText("General")) diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt index f8c2a89ea8..8a21260ac7 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt @@ -88,7 +88,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() { fun checkVerifyWithPassphrase() { val userId: String = existingSession!!.myUserId - doLogin(homeServerUrl, userId, password) + uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl) // Thread.sleep(6000) withIdlingResource(activityIdlingResource(HomeActivity::class.java)) { @@ -137,10 +137,10 @@ class VerifySessionPassphraseTest : VerificationTestBase() { onView(withId(R.id.ssss__root)).check(matches(isDisplayed())) } - onView((withId(R.id.ssss_passphrase_enter_edittext))) + onView(withId(R.id.ssss_passphrase_enter_edittext)) .perform(typeText(passphrase)) - onView((withId(R.id.ssss_passphrase_submit))) + onView(withId(R.id.ssss_passphrase_submit)) .perform(click()) System.out.println("*** passphrase 1") diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt new file mode 100644 index 0000000000..bf60ad681f --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt @@ -0,0 +1,46 @@ +/* + * 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.espresso.tools + +import android.widget.Switch +import androidx.annotation.StringRes +import androidx.preference.Preference +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem +import androidx.test.espresso.matcher.PreferenceMatchers.withKey +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.withClassName +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import im.vector.app.R +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.instanceOf + +fun clickOnPreference(@StringRes textResId: Int) { + onView(withId(R.id.recycler_view)) + .perform(actionOnItem( + hasDescendant(withText(textResId)), click())) +} + +fun clickOnSwitchPreference(preferenceKey: String) { + onData(allOf(`is`(instanceOf(Preference::class.java)), withKey(preferenceKey))) + .onChildView(withClassName(`is`(Switch::class.java.name))).perform(click()) +} diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt new file mode 100644 index 0000000000..2cdca62c74 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt @@ -0,0 +1,25 @@ +/* + * 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.espresso.tools + +import android.app.Activity +import im.vector.app.activityIdlingResource +import im.vector.app.withIdlingResource + +inline fun waitUntilActivityVisible(noinline block: (() -> Unit)) { + withIdlingResource(activityIdlingResource(T::class.java), block) +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt new file mode 100644 index 0000000000..6b30700116 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -0,0 +1,463 @@ +/* + * 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.ui + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.longClick +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.schibsted.spain.barista.assertion.BaristaListAssertions.assertListItemCount +import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed +import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickBack +import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn +import com.schibsted.spain.barista.interaction.BaristaClickInteractions.longClickOn +import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton +import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton +import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo +import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItem +import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItemChild +import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.clickMenu +import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.openMenu +import im.vector.app.EspressoHelper +import im.vector.app.R +import im.vector.app.SleepViewAction +import im.vector.app.activityIdlingResource +import im.vector.app.espresso.tools.clickOnPreference +import im.vector.app.espresso.tools.waitUntilActivityVisible +import im.vector.app.features.MainActivity +import im.vector.app.features.createdirect.CreateDirectRoomActivity +import im.vector.app.features.home.HomeActivity +import im.vector.app.features.home.room.detail.RoomDetailActivity +import im.vector.app.features.login.LoginActivity +import im.vector.app.features.roomdirectory.RoomDirectoryActivity +import im.vector.app.initialSyncIdlingResource +import im.vector.app.waitForView +import im.vector.app.withIdlingResource +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.lang.Thread.sleep +import java.util.UUID + +/** + * This test aim to open every possible screen of the application + */ +@RunWith(AndroidJUnit4::class) +@LargeTest +class UiAllScreensSanityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + private val uiTestBase = UiTestBase() + + // Last passing: 2020-11-09 + @Test + fun allScreensTest() { + // Create an account + val userId = "UiTest_" + UUID.randomUUID().toString() + uiTestBase.createAccount(userId = userId) + + withIdlingResource(activityIdlingResource(HomeActivity::class.java)) { + assertDisplayed(R.id.roomListContainer) + closeSoftKeyboard() + } + + val activity = EspressoHelper.getCurrentActivity()!! + val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession() + + withIdlingResource(initialSyncIdlingResource(uiSession)) { + assertDisplayed(R.id.roomListContainer) + } + + assertDisplayed(R.id.bottomNavigationView) + + // Settings + navigateToSettings() + + // Create DM + clickOn(R.id.bottom_action_people) + createDm() + + // Create Room + // First navigate to the other tab + clickOn(R.id.bottom_action_rooms) + createRoom() + + assertDisplayed(R.id.bottomNavigationView) + + // Long click on the room + onView(withId(R.id.roomListView)) + .perform( + actionOnItem( + hasDescendant(withText(R.string.room_displayname_empty_room)), + longClick() + ) + ) + pressBack() + + uiTestBase.signout() + + // We have sent a message in a e2e room, accept to loose it + clickOn(R.id.exitAnywayButton) + // Dark pattern + clickDialogNegativeButton() + + // Login again on the same account + waitUntilActivityVisible { + assertDisplayed(R.id.loginSplashLogo) + } + + uiTestBase.login(userId) + ignoreVerification() + + uiTestBase.signout() + clickDialogPositiveButton() + + // TODO Deactivate account instead of logout? + } + + private fun ignoreVerification() { + Thread.sleep(6000) + val activity = EspressoHelper.getCurrentActivity()!! + + val popup = activity.findViewById(com.tapadoo.alerter.R.id.llAlertBackground) + activity.runOnUiThread { + popup.performClick() + } + + assertDisplayed(R.id.bottomSheetFragmentContainer) + + onView(ViewMatchers.isRoot()).perform(SleepViewAction.sleep(2000)) + + clickOn(R.string.skip) + assertDisplayed(R.string.are_you_sure) + clickOn(R.string.skip) + } + + private fun createRoom() { + clickOn(R.id.createGroupRoomButton) + waitUntilActivityVisible { + assertDisplayed(R.id.publicRoomsList) + } + clickOn(R.string.create_new_room) + + // Create + assertListItemCount(R.id.createRoomForm, 10) + clickListItemChild(R.id.createRoomForm, 9, R.id.form_submit_button) + + waitUntilActivityVisible { + assertDisplayed(R.id.roomDetailContainer) + } + + clickOn(R.id.attachmentButton) + clickBack() + + // Send a message + writeTo(R.id.composerEditText, "Hello world!") + clickOn(R.id.sendButton) + + navigateToRoomSettings() + + // Long click on the message + longClickOnMessageTest() + + // Menu + openMenu() + pressBack() + clickMenu(R.id.voice_call) + pressBack() + clickMenu(R.id.video_call) + pressBack() + + pressBack() + } + + private fun longClickOnMessageTest() { + // Test quick reaction + longClickOnMessage() + // Add quick reaction + clickOn("👍") + + sleep(1000) + + // Open reactions + longClickOn("👍") + pressBack() + + // Test add reaction + longClickOnMessage() + clickOn(R.string.message_add_reaction) + // Filter + // TODO clickMenu(R.id.search) + clickListItem(R.id.emojiRecyclerView, 4) + + // Test Edit mode + longClickOnMessage() + clickOn(R.string.edit) + // TODO Cancel action + writeTo(R.id.composerEditText, "Hello universe!") + clickOn(R.id.sendButton) + // Open edit history + longClickOnMessage("Hello universe! (edited)") + clickOn(R.string.message_view_edit_history) + pressBack() + } + + private fun longClickOnMessage(text: String = "Hello world!") { + onView(withId(R.id.timelineRecyclerView)) + .perform( + actionOnItem( + hasDescendant(withText(text)), + longClick() + ) + ) + } + + private fun navigateToRoomSettings() { + clickOn(R.id.roomToolbarTitleView) + assertDisplayed(R.id.roomProfileAvatarView) + + // Room settings + clickListItem(R.id.matrixProfileRecyclerView, 3) + pressBack() + + // Notifications + clickListItem(R.id.matrixProfileRecyclerView, 5) + pressBack() + + assertDisplayed(R.id.roomProfileAvatarView) + + // People + clickListItem(R.id.matrixProfileRecyclerView, 7) + assertDisplayed(R.id.inviteUsersButton) + navigateToRoomPeople() + // Fab + navigateToInvite() + pressBack() + pressBack() + + assertDisplayed(R.id.roomProfileAvatarView) + + // Uploads + clickListItem(R.id.matrixProfileRecyclerView, 9) + // File tab + clickOn(R.string.uploads_files_title) + pressBack() + + assertDisplayed(R.id.roomProfileAvatarView) + + // Leave + clickListItem(R.id.matrixProfileRecyclerView, 13) + clickDialogNegativeButton() + + // Menu share + // clickMenu(R.id.roomProfileShareAction) + // pressBack() + + pressBack() + } + + private fun navigateToInvite() { + assertDisplayed(R.id.inviteUsersButton) + clickOn(R.id.inviteUsersButton) + closeSoftKeyboard() + pressBack() + } + + private fun navigateToRoomPeople() { + // Open first user + clickListItem(R.id.roomSettingsRecyclerView, 1) + assertDisplayed(R.id.memberProfilePowerLevelView) + + // Verification + clickListItem(R.id.matrixProfileRecyclerView, 1) + clickBack() + + // Role + clickListItem(R.id.matrixProfileRecyclerView, 3) + clickDialogNegativeButton() + + clickBack() + } + + private fun createDm() { + clickOn(R.id.createChatRoomButton) + + withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) { + onView(withId(R.id.userListRecyclerView)) + .perform(waitForView(withText(R.string.qr_code))) + onView(withId(R.id.userListRecyclerView)) + .perform(waitForView(withText(R.string.invite_friends))) + } + + closeSoftKeyboard() + pressBack() + pressBack() + } + + private fun navigateToSettings() { + clickOn(R.id.groupToolbarAvatarImageView) + clickOn(R.id.homeDrawerHeaderSettingsView) + + clickOn(R.string.settings_general_title) + navigateToSettingsGeneral() + pressBack() + + clickOn(R.string.settings_notifications) + navigateToSettingsNotifications() + pressBack() + + clickOn(R.string.settings_preferences) + navigateToSettingsPreferences() + pressBack() + + clickOn(R.string.preference_voice_and_video) + pressBack() + + clickOn(R.string.settings_ignored_users) + pressBack() + + clickOn(R.string.settings_security_and_privacy) + navigateToSettingsSecurity() + pressBack() + + clickOn(R.string.room_settings_labs_pref_title) + pressBack() + + clickOn(R.string.settings_advanced_settings) + navigateToSettingsAdvanced() + pressBack() + + clickOn(R.string.preference_root_help_about) + navigateToSettingsHelp() + pressBack() + + pressBack() + } + + private fun navigateToSettingsHelp() { + /* + clickOn(R.string.settings_app_info_link_title) + Cannot go back... + pressBack() + clickOn(R.string.settings_copyright) + pressBack() + clickOn(R.string.settings_app_term_conditions) + pressBack() + clickOn(R.string.settings_privacy_policy) + pressBack() + */ + clickOn(R.string.settings_third_party_notices) + clickDialogPositiveButton() + } + + private fun navigateToSettingsAdvanced() { + clickOnPreference(R.string.settings_notifications_targets) + pressBack() + + clickOnPreference(R.string.settings_push_rules) + pressBack() + + /* TODO P2 test developer screens + // Enable developer mode + clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY") + + clickOnPreference(R.string.settings_account_data) + clickOn("m.push_rules") + pressBack() + pressBack() + clickOnPreference(R.string.settings_key_requests) + pressBack() + + // Disable developer mode + clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY") + */ + } + + private fun navigateToSettingsSecurity() { + clickOnPreference(R.string.settings_active_sessions_show_all) + pressBack() + + clickOnPreference(R.string.encryption_message_recovery) + // TODO go deeper here + pressBack() + /* Cannot exit + clickOnPreference(R.string.encryption_export_e2e_room_keys) + pressBack() + */ + } + + private fun navigateToSettingsPreferences() { + clickOn(R.string.settings_interface_language) + onView(isRoot()) + .perform(waitForView(withText("Dansk (Danmark)"))) + pressBack() + clickOn(R.string.settings_theme) + clickDialogNegativeButton() + clickOn(R.string.font_size) + clickDialogNegativeButton() + } + + private fun navigateToSettingsNotifications() { + clickOn(R.string.settings_notification_advanced) + pressBack() + /* + clickOn(R.string.settings_noisy_notifications_preferences) + TODO Cannot go back + pressBack() + clickOn(R.string.settings_silent_notifications_preferences) + pressBack() + clickOn(R.string.settings_call_notifications_preferences) + pressBack() + */ + clickOnPreference(R.string.settings_notification_troubleshoot) + pressBack() + } + + private fun navigateToSettingsGeneral() { + clickOn(R.string.settings_profile_picture) + clickDialogPositiveButton() + clickOn(R.string.settings_display_name) + clickDialogNegativeButton() + clickOn(R.string.settings_password) + clickDialogNegativeButton() + clickOn(R.string.settings_emails_and_phone_numbers_title) + pressBack() + clickOn(R.string.settings_discovery_manage) + clickOn(R.string.add_identity_server) + pressBack() + pressBack() + // Identity server + clickOnPreference(R.string.settings_identity_server) + pressBack() + // Deactivate account + clickOnPreference(R.string.settings_deactivate_my_account) + pressBack() + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt b/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt new file mode 100644 index 0000000000..ed174b50a2 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt @@ -0,0 +1,90 @@ +/* + * 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.ui + +import androidx.test.espresso.Espresso.closeSoftKeyboard +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertDisabled +import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertEnabled +import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed +import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn +import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo +import im.vector.app.R +import im.vector.app.espresso.tools.waitUntilActivityVisible +import im.vector.app.features.home.HomeActivity +import im.vector.app.waitForView + +class UiTestBase { + fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { + initSession(true, userId, password, homeServerUrl) + } + + fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { + initSession(false, userId, password, homeServerUrl) + } + + private fun initSession(createAccount: Boolean, + userId: String, + password: String, + homeServerUrl: String) { + assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit) + clickOn(R.id.loginSplashSubmit) + assertDisplayed(R.id.loginServerTitle, R.string.login_server_title) + // Chose custom server + clickOn(R.id.loginServerChoiceOther) + // Enter local synapse + writeTo(R.id.loginServerUrlFormHomeServerUrl, homeServerUrl) + assertEnabled(R.id.loginServerUrlFormSubmit) + closeSoftKeyboard() + clickOn(R.id.loginServerUrlFormSubmit) + onView(isRoot()).perform(waitForView(withId(R.id.loginSignupSigninSubmit))) + + if (createAccount) { + // Click on the signup button + assertDisplayed(R.id.loginSignupSigninSubmit) + clickOn(R.id.loginSignupSigninSubmit) + } else { + // Click on the signin button + assertDisplayed(R.id.loginSignupSigninSignIn) + clickOn(R.id.loginSignupSigninSignIn) + } + + // Ensure password flow supported + assertDisplayed(R.id.loginField) + assertDisplayed(R.id.passwordField) + + writeTo(R.id.loginField, userId) + assertDisabled(R.id.loginSubmit) + writeTo(R.id.passwordField, password) + assertEnabled(R.id.loginSubmit) + + closeSoftKeyboard() + clickOn(R.id.loginSubmit) + + // Wait + waitUntilActivityVisible { + assertDisplayed(R.id.homeDetailFragmentContainer) + } + } + + fun signout() { + clickOn(R.id.groupToolbarAvatarImageView) + clickOn(R.id.homeDrawerHeaderSignoutView) + } +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt index f22784bc12..869058eff6 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt @@ -30,12 +30,12 @@ class DebugSasEmojiActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.fragment_generic_recycler) val controller = SasEmojiController() - recyclerView.configureWith(controller) + genericRecyclerView.configureWith(controller) controller.setData(SasState(getAllVerificationEmojis())) } override fun onDestroy() { - recyclerView.cleanup() + genericRecyclerView.cleanup() super.onDestroy() } } diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index 458b44fd05..9a95085a07 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -72,7 +72,7 @@ android:id="@+id/debug_qr_code" android:layout_width="200dp" android:layout_height="200dp" - tools:src="@tools:sample/avatars" /> + tools:src="@drawable/ic_qr_code_add" /> diff --git a/vector/src/debug/res/layout/activity_test_linkify.xml b/vector/src/debug/res/layout/activity_test_linkify.xml index bbaadb20a2..7e625ad08c 100644 --- a/vector/src/debug/res/layout/activity_test_linkify.xml +++ b/vector/src/debug/res/layout/activity_test_linkify.xml @@ -4,7 +4,7 @@ android:id="@+id/test_linkify_coordinator" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/riot_secondary_text_color_status" + android:background="#7F70808D" tools:context=".features.debug.TestLinkifyActivity"> - - - - - - \ No newline at end of file diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index 32888dafd7..1107737888 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -18,7 +18,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity -import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.startAddGoogleAccountIntent @@ -36,29 +36,33 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi override fun perform(activityResultLauncher: ActivityResultLauncher) { status = TestStatus.RUNNING try { - FirebaseInstanceId.getInstance().instanceId + FirebaseMessaging.getInstance().token .addOnCompleteListener(context) { task -> if (!task.isSuccessful) { - val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage // Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated) - if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) - } else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) - } else if ("ACCOUNT_MISSING".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) - quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { - override fun doFix() { - startAddGoogleAccountIntent(context, activityResultLauncher) - } + description = when (val errorMsg = task.exception?.localizedMessage ?: "Unknown") { + "SERVICE_NOT_AVAILABLE" -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) + } + "TOO_MANY_REGISTRATIONS" -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) + } + "ACCOUNT_MISSING" -> { + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { + override fun doFix() { + startAddGoogleAccountIntent(context, activityResultLauncher) + } + } + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) + } + else -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) } - } else { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) } status = TestStatus.FAILED } else { - task.result?.token?.let { token -> - val tok = token.substring(0, Math.min(8, token.length)) + "********************" + task.result?.let { token -> + val tok = token.take(8) + "********************" description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok) Timber.e("Retrieved FCM token success [$tok].") // Ensure it is well store in our local storage diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 913eab211d..f3bdcafb1c 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,7 +21,7 @@ import android.widget.Toast import androidx.core.content.edit import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences @@ -71,14 +71,16 @@ object FcmHelper { // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' if (checkPlayServices(activity)) { try { - FirebaseInstanceId.getInstance().instanceId - .addOnSuccessListener(activity) { instanceIdResult -> - storeFcmToken(activity, instanceIdResult.token) + FirebaseMessaging.getInstance().token + .addOnSuccessListener { token -> + storeFcmToken(activity, token) if (registerPusher) { - pushersManager.registerPusherWithFcmKey(instanceIdResult.token) + pushersManager.registerPusherWithFcmKey(token) } } - .addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } + .addOnFailureListener { e -> + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") + } } catch (e: Throwable) { Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 90ded9f29b..bd146b5757 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -81,7 +81,8 @@ android:resource="@xml/shortcuts" /> - + - + - @@ -230,6 +230,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index acdad5407c..188ca32559 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -36,6 +36,7 @@ import im.vector.app.features.crypto.recover.BootstrapMigrateBackupFragment import im.vector.app.features.crypto.recover.BootstrapSaveRecoveryKeyFragment import im.vector.app.features.crypto.recover.BootstrapSetupRecoveryKeyFragment import im.vector.app.features.crypto.recover.BootstrapWaitingFragment +import im.vector.app.features.crypto.verification.QuadSLoadingFragment import im.vector.app.features.crypto.verification.cancel.VerificationCancelFragment import im.vector.app.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.app.features.crypto.verification.choose.VerificationChooseMethodFragment @@ -83,6 +84,7 @@ import im.vector.app.features.roomprofile.RoomProfileFragment import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment +import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment @@ -111,8 +113,8 @@ import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment import im.vector.app.features.share.IncomingShareFragment import im.vector.app.features.signout.soft.SoftLogoutFragment import im.vector.app.features.terms.ReviewTermsFragment -import im.vector.app.features.userdirectory.KnownUsersFragment -import im.vector.app.features.userdirectory.UserDirectoryFragment +import im.vector.app.features.usercode.ShowUserCodeFragment +import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.widgets.WidgetFragment @Module @@ -255,13 +257,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(UserDirectoryFragment::class) - fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment - - @Binds - @IntoMap - @FragmentKey(KnownUsersFragment::class) - fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment + @FragmentKey(UserListFragment::class) + fun bindUserListFragment(fragment: UserListFragment): Fragment @Binds @IntoMap @@ -368,6 +365,11 @@ interface FragmentModule { @FragmentKey(RoomSettingsFragment::class) fun bindRoomSettingsFragment(fragment: RoomSettingsFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomAliasFragment::class) + fun bindRoomAliasFragment(fragment: RoomAliasFragment): Fragment + @Binds @IntoMap @FragmentKey(RoomMemberProfileFragment::class) @@ -423,6 +425,11 @@ interface FragmentModule { @FragmentKey(VerificationCancelFragment::class) fun bindVerificationCancelFragment(fragment: VerificationCancelFragment): Fragment + @Binds + @IntoMap + @FragmentKey(QuadSLoadingFragment::class) + fun bindQuadSLoadingFragment(fragment: QuadSLoadingFragment): Fragment + @Binds @IntoMap @FragmentKey(VerificationNotMeFragment::class) @@ -582,4 +589,9 @@ interface FragmentModule { @IntoMap @FragmentKey(SearchFragment::class) fun bindSearchFragment(fragment: SearchFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(ShowUserCodeFragment::class) + fun bindShowUserCodeFragment(fragment: ShowUserCodeFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index fde40f9195..f56a6a3d70 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -50,6 +50,7 @@ import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.link.LinkHandlerActivity import im.vector.app.features.login.LoginActivity +import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.VectorAttachmentViewerActivity import im.vector.app.features.navigation.Navigator @@ -66,12 +67,16 @@ import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.roommemberprofile.RoomMemberProfileActivity import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet import im.vector.app.features.roomprofile.RoomProfileActivity +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.ui.UiStateRepository +import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment @@ -140,6 +145,7 @@ interface ScreenComponent { fun inject(activity: VectorAttachmentViewerActivity) fun inject(activity: VectorJitsiActivity) fun inject(activity: SearchActivity) + fun inject(activity: UserCodeActivity) /* ========================================================================================== * BottomSheets @@ -150,6 +156,9 @@ interface ScreenComponent { fun inject(bottomSheet: ViewEditHistoryBottomSheet) fun inject(bottomSheet: DisplayReadReceiptsBottomSheet) fun inject(bottomSheet: RoomListQuickActionsBottomSheet) + fun inject(bottomSheet: RoomAliasBottomSheet) + fun inject(bottomSheet: RoomHistoryVisibilityBottomSheet) + fun inject(bottomSheet: RoomJoinRuleBottomSheet) fun inject(bottomSheet: VerificationBottomSheet) fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) fun inject(bottomSheet: DeviceListBottomSheet) @@ -158,6 +167,7 @@ interface ScreenComponent { fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: CallControlsBottomSheet) fun inject(bottomSheet: SignOutBottomSheetDialogFragment) + fun inject(bottomSheet: MatrixToBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index 836dab00c5..bed2e0b850 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -35,7 +35,10 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.reactions.EmojiChooserViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel -import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel +import im.vector.app.features.userdirectory.UserListSharedActionViewModel @Module interface ViewModelModule { @@ -87,8 +90,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(UserDirectorySharedActionViewModel::class) - fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel + @ViewModelKey(UserListSharedActionViewModel::class) + fun bindUserListSharedActionViewModel(viewModel: UserListSharedActionViewModel): ViewModel @Binds @IntoMap @@ -105,6 +108,21 @@ interface ViewModelModule { @ViewModelKey(RoomListQuickActionsSharedActionViewModel::class) fun bindRoomListQuickActionsSharedActionViewModel(viewModel: RoomListQuickActionsSharedActionViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(RoomAliasBottomSheetSharedActionViewModel::class) + fun bindRoomAliasBottomSheetSharedActionViewModel(viewModel: RoomAliasBottomSheetSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomHistoryVisibilitySharedActionViewModel::class) + fun bindRoomHistoryVisibilitySharedActionViewModel(viewModel: RoomHistoryVisibilitySharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomJoinRuleSharedActionViewModel::class) + fun bindRoomJoinRuleSharedActionViewModel(viewModel: RoomJoinRuleSharedActionViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(RoomDirectorySharedActionViewModel::class) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt new file mode 100644 index 0000000000..2f32fafa9e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt @@ -0,0 +1,46 @@ +/* + * 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.core.epoxy + +import android.widget.CompoundButton +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.checkbox.MaterialCheckBox +import im.vector.app.R + +@EpoxyModelClass(layout = R.layout.item_checkbox) +abstract class CheckBoxItem : VectorEpoxyModel() { + + @EpoxyAttribute + var checked: Boolean = false + + @EpoxyAttribute lateinit var title: String + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.checkbox.isChecked = checked + holder.checkbox.text = title + holder.checkbox.setOnCheckedChangeListener(checkChangeListener) + } + + class Holder : VectorEpoxyHolder() { + val checkbox by bind(R.id.checkbox) + } +} diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt index e28bec6874..80792648f6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isInvisible @@ -43,6 +44,13 @@ abstract class BottomSheetActionItem : VectorEpoxyModel() var destructive: Boolean = false @EpoxyAttribute - var listener: View.OnClickListener? = null + var listener: ClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - holder.view.setOnClickListener(listener) + holder.view.onClick(listener) if (listener == null) { holder.view.isClickable = false } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt index fdbe9f7f94..99acd6cb36 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt @@ -59,9 +59,7 @@ fun EpoxyController.buildProfileAction( accessoryRes(accessory) accessoryMatrixItem(accessoryMatrixItem) avatarRenderer(avatarRenderer) - listener { _ -> - action?.invoke() - } + listener(action) } if (divider) { diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 6065c74541..b9bc935890 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -136,6 +136,7 @@ class DefaultErrorFormatter @Inject constructor( IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error + IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided }) } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt new file mode 100644 index 0000000000..b1b30da156 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt @@ -0,0 +1,28 @@ +/* + * 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.core.extensions + +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet + +fun ConstraintLayout.updateConstraintSet(block: (ConstraintSet) -> Unit) { + ConstraintSet().let { + it.clone(this) + block.invoke(it) + it.applyTo(this) + } +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt index 355dd8442f..33e7199334 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt @@ -26,7 +26,7 @@ import androidx.annotation.DrawableRes import im.vector.app.R import im.vector.app.core.platform.SimpleTextWatcher -fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter, +fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_search, @DrawableRes clearIconRes: Int = R.drawable.ic_x_gray) { addTextChangedListener(object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { @@ -57,3 +57,15 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filte return@OnTouchListener false }) } + +/** + * Update the edit text value, only if necessary and move the cursor to the end of the text + */ +fun EditText.setTextSafe(value: String?) { + if (value != null && text.toString() != value) { + setText(value) + // To fix jumping cursor to the start https://github.com/airbnb/epoxy/issues/426 + // Note: there is still a known bug if deleting char in the middle of the text, by long pressing on the backspace button. + setSelection(value.length) + } +} diff --git a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt index c8a2bf65d5..1299f4086b 100644 --- a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt +++ b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.webkit.MimeTypeMap import im.vector.app.core.utils.getFileExtension import timber.log.Timber +import java.util.Locale /** * Returns the mimetype from a uri. @@ -44,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? { if (null != mimeType) { // the mimetype is sometimes in uppercase. - mimeType = mimeType.toLowerCase() + mimeType = mimeType.toLowerCase(Locale.ROOT) } } catch (e: Exception) { Timber.e(e, "Failed to open resource input stream") diff --git a/vector/src/main/java/im/vector/app/core/platform/StateView.kt b/vector/src/main/java/im/vector/app/core/platform/StateView.kt index 2af3235cdf..57f5a11a91 100755 --- a/vector/src/main/java/im/vector/app/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/app/core/platform/StateView.kt @@ -23,6 +23,7 @@ import android.view.View import android.widget.FrameLayout import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.updateConstraintSet import kotlinx.android.synthetic.main.view_state.view.* class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) @@ -31,7 +32,12 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? sealed class State { object Content : State() object Loading : State() - data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State() + data class Empty( + val title: CharSequence? = null, + val image: Drawable? = null, + val isBigImage: Boolean = false, + val message: CharSequence? = null + ) : State() data class Error(val message: CharSequence? = null) : State() } @@ -71,6 +77,9 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? is State.Loading -> Unit is State.Empty -> { emptyImageView.setImageDrawable(newState.image) + emptyView.updateConstraintSet { + it.constrainPercentHeight(R.id.emptyImageView, if (newState.isBigImage) 0.5f else 0.1f) + } emptyMessageView.text = newState.message emptyTitleView.text = newState.title } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 79021902c4..f58f0b87ae 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -587,6 +587,16 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } } + fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) { + coordinatorLayout?.let { + Snackbar.make(it, message, Snackbar.LENGTH_LONG).apply { + withActionTitle?.let { + setAction(withActionTitle, { action?.invoke() }) + } + }.show() + } + } + /* ========================================================================================== * User Consent * ========================================================================================== */ 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 179e21a6d8..cd38f5aeaa 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 @@ -97,12 +97,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog() viewModelFactory = screenComponent.viewModelFactory() childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() - injectWith(injector()) super.onAttach(context) } - protected open fun injectWith(injector: ScreenComponent) = Unit - @CallSuper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index 002dfcf068..d6f43beaf7 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -43,7 +43,7 @@ abstract class VectorViewModel Single.toAsync(stateReducer: S.(Async) -> S): Single> { setState { stateReducer(Loading()) } return map { Success(it) as Async } @@ -56,7 +56,7 @@ abstract class VectorViewModel Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { setState { stateReducer(Loading()) } return map { Success(it) as Async } diff --git a/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt b/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt index c184b04bd9..96b1cfbb8e 100644 --- a/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt @@ -26,12 +26,12 @@ import javax.inject.Inject class DrawableProvider @Inject constructor(private val context: Context) { - fun getDrawable(@DrawableRes colorRes: Int): Drawable? { - return ContextCompat.getDrawable(context, colorRes) + fun getDrawable(@DrawableRes drawableRes: Int): Drawable? { + return ContextCompat.getDrawable(context, drawableRes) } - fun getDrawable(@DrawableRes colorRes: Int, @ColorInt color: Int): Drawable? { - return ContextCompat.getDrawable(context, colorRes)?.let { + fun getDrawable(@DrawableRes drawableRes: Int, @ColorInt color: Int): Drawable? { + return ContextCompat.getDrawable(context, drawableRes)?.let { ThemeUtils.tintDrawableWithColor(it, color) } } diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt new file mode 100644 index 0000000000..da136fb072 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt @@ -0,0 +1,61 @@ +/* + * 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.core.ui.bottomsheet + +import android.os.Bundle +import android.view.View +import androidx.annotation.CallSuper +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import javax.inject.Inject + +/** + * Generic Bottom sheet with actions + */ +abstract class BottomSheetGeneric : + VectorBaseBottomSheetDialogFragment(), + BottomSheetGenericController.Listener { + + @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView + + final override val showExpanded = true + + final override fun getLayoutResId() = R.layout.bottom_sheet_generic_list + + abstract fun getController(): BottomSheetGenericController + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recyclerView.configureWith(getController(), viewPool = sharedViewPool, hasFixedSize = false, disableItemAnimation = true) + getController().listener = this + } + + @CallSuper + override fun onDestroyView() { + recyclerView.cleanup() + getController().listener = null + super.onDestroyView() + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericAction.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericAction.kt new file mode 100644 index 0000000000..da48accf35 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericAction.kt @@ -0,0 +1,42 @@ +/* + * 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.core.ui.bottomsheet + +import androidx.annotation.DrawableRes +import im.vector.app.core.epoxy.bottomsheet.BottomSheetActionItem_ +import im.vector.app.core.platform.VectorSharedAction + +/** + * Parent class for a bottom sheet action + */ +open class BottomSheetGenericAction( + open val title: String, + @DrawableRes open val iconResId: Int, + open val isSelected: Boolean, + open val destructive: Boolean +) : VectorSharedAction { + + fun toBottomSheetItem(): BottomSheetActionItem_ { + return BottomSheetActionItem_().apply { + id("action_$title") + iconRes(iconResId) + text(title) + selected(isSelected) + destructive(destructive) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt new file mode 100644 index 0000000000..67347c3220 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt @@ -0,0 +1,64 @@ +/* + * 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.core.ui.bottomsheet + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.epoxy.dividerItem + +/** + * Epoxy controller for generic bottom sheet actions + */ +abstract class BottomSheetGenericController + : TypedEpoxyController() { + + var listener: Listener? = null + + abstract fun getTitle(): String? + + open fun getSubTitle(): String? = null + + abstract fun getActions(state: State): List + + override fun buildModels(state: State?) { + state ?: return + // Title + getTitle()?.let { title -> + bottomSheetTitleItem { + id("title") + title(title) + subTitle(getSubTitle()) + } + + dividerItem { + id("title_separator") + } + } + // Actions + val actions = getActions(state) + val showIcons = actions.any { it.iconResId > 0 } + actions.forEach { action -> + action.toBottomSheetItem() + .showIcon(showIcons) + .listener(View.OnClickListener { listener?.didSelectAction(action) }) + .addTo(this) + } + } + + interface Listener { + fun didSelectAction(action: Action) + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericSharedActionViewModel.kt new file mode 100644 index 0000000000..49147b954a --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericSharedActionViewModel.kt @@ -0,0 +1,25 @@ +/* + * 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.core.ui.bottomsheet + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel + +/** + * Activity shared view model to handle bottom sheet quick actions + */ +abstract class BottomSheetGenericSharedActionViewModel : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt new file mode 100644 index 0000000000..38c81a7ef6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt @@ -0,0 +1,21 @@ +/* + * 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.core.ui.bottomsheet + +import com.airbnb.mvrx.MvRxState + +abstract class BottomSheetGenericState : MvRxState diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt new file mode 100644 index 0000000000..6cc2c4c981 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt @@ -0,0 +1,30 @@ +/* + * 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.core.ui.bottomsheet + +import com.airbnb.mvrx.MvRxState +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel + +abstract class BottomSheetGenericViewModel(initialState: State) : + VectorViewModel(initialState) { + + override fun handle(action: EmptyAction) { + // No op + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt new file mode 100644 index 0000000000..27fb634480 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt @@ -0,0 +1,49 @@ +/* + * 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.core.ui.bottomsheet + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide + +/** + * A title for bottom sheet, with an optional subtitle. It does not include the bottom separator. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_title) +abstract class BottomSheetTitleItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var subTitle: String? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.title.text = title + holder.subtitle.setTextOrHide(subTitle) + } + + class Holder : VectorEpoxyHolder() { + val title by bind(R.id.itemBottomSheetTitleTitle) + val subtitle by bind(R.id.itemBottomSheetTitleSubtitle) + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index 8a908ad1d4..06bdeb9277 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -44,7 +44,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD } override fun post(value: T) { - behaviorRelay.accept(value) + behaviorRelay.accept(value!!) } private fun createRelay(): BehaviorRelay { @@ -68,6 +68,6 @@ open class PublishDataSource : MutableDataSource { } override fun post(value: T) { - publishRelay.accept(value) + publishRelay.accept(value!!) } } 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 ff1055fa44..4c6aa51348 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 @@ -29,6 +29,7 @@ import android.os.Build import android.os.Environment import android.provider.Browser import android.provider.MediaStore +import android.provider.Settings import android.webkit.MimeTypeMap import android.widget.Toast import androidx.activity.result.ActivityResultLauncher @@ -448,6 +449,19 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID } } +fun openAppSettingsPage(activity: Activity) { + try { + activity.startActivity( + Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + data = Uri.fromParts("package", activity.packageName, null) + }) + } catch (activityNotFoundException: ActivityNotFoundException) { + activity.toast(R.string.error_no_external_application_found) + } +} + /** * Ask the user to select a location and a file name to write in */ diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index ab99ba61bd..aa36dd0959 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.content.Context import timber.log.Timber import java.io.File +import java.util.Locale // Implementation should return true in case of success typealias ActionOnFile = (file: File) -> Boolean @@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? { val ext = filename.substring(dotPos + 1) if (ext.isNotBlank()) { - return ext.toLowerCase() + return ext.toLowerCase(Locale.ROOT) } } } diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 44fc6afa4e..606321fff2 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -30,6 +30,7 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import im.vector.app.R +import im.vector.app.core.platform.VectorBaseActivity import timber.log.Timber // Android M permission request code management @@ -284,6 +285,12 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int, return isPermissionGranted } +fun VectorBaseActivity.onPermissionDeniedSnackbar(@StringRes rationaleMessage: Int) { + showSnackbar(getString(rationaleMessage), R.string.settings) { + openAppSettingsPage(this) + } +} + /** * Helper method used in [.checkPermissions] to populate the list of the * permissions to be granted (permissionsListToBeGrantedOut) and the list of the permissions already denied (permissionAlreadyDeniedListOut). diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index d228adab12..2348b07c7b 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -136,13 +136,19 @@ fun startSharePlainTextIntent(fragment: Fragment, activityResultLauncher: ActivityResultLauncher?, chooserTitle: String?, text: String, - subject: String? = null) { + subject: String? = null, + extraTitle: String? = null) { val share = Intent(Intent.ACTION_SEND) share.type = "text/plain" share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) // Add data to the intent, the receiving app will decide what to do with it. share.putExtra(Intent.EXTRA_SUBJECT, subject) share.putExtra(Intent.EXTRA_TEXT, text) + + extraTitle?.let { + share.putExtra(Intent.EXTRA_TITLE, it) + } + val intent = Intent.createChooser(share, chooserTitle) try { if (activityResultLauncher != null) { 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 e553b5e0d3..8499b740f7 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -62,8 +62,8 @@ data class MainActivityArgs( ) : Parcelable /** - * This is the entry point of RiotX - * This Activity, when started with argument, is also doing some cleanup when user disconnects, + * This is the entry point of Element Android + * This Activity, when started with argument, is also doing some cleanup when user signs out, * clears cache, is logged out, or is soft logged out */ class MainActivity : VectorBaseActivity(), UnlockedActivity { @@ -78,6 +78,8 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity { intent.putExtra(EXTRA_ARGS, args) activity.startActivity(intent) + // Ensure all the Activities are destroyed, it seems that the intent flags are not enough now. + activity.finishAffinity() } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index ba0250724c..f67b0946cc 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -59,7 +59,6 @@ data class AttachmentsPreviewArgs( ) : Parcelable class AttachmentsPreviewFragment @Inject constructor( - val viewModelFactory: AttachmentsPreviewViewModel.Factory, private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, private val attachmentBigPreviewController: AttachmentBigPreviewController, private val colorProvider: ColorProvider diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt index 59a0937d89..28d617e613 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -17,31 +17,12 @@ package im.vector.app.features.attachments.preview -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState) +class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) : VectorViewModel(initialState) { - @AssistedInject.Factory - interface Factory { - fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel - } - - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? { - val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } - override fun handle(action: AttachmentsPreviewAction) { when (action) { is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index cf4d1417e3..783b519706 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -46,7 +46,7 @@ class JitsiCallViewModel @AssistedInject constructor( } init { - val me = session.getUser(session.myUserId)?.toMatrixItem() + val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem() val userInfo = JitsiMeetUserInfo().apply { displayName = me?.getBestName() avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 1ab6fb6363..43b41f7b5a 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -109,6 +109,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMeetActivityInterface, Ji .setFeatureFlag("invite.enabled", false) .setFeatureFlag("add-people.enabled", false) .setFeatureFlag("video-share.enabled", false) + .setFeatureFlag("call-integration.enabled", false) .setRoom(viewState.confId) .setSubject(viewState.subject) .build() diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt index 8eb5bc733b..e380998fd2 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt @@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class ContactsBookAction : VectorViewModelAction { data class FilterWith(val filter: String) : ContactsBookAction() data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction() + object UserConsentGranted : ContactsBookAction() } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt index 9eca2afa60..59c23f4ac7 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt @@ -52,11 +52,10 @@ class ContactsBookController @Inject constructor( override fun buildModels() { val currentState = state ?: return - val hasSearch = currentState.searchTerm.isNotEmpty() when (val asyncMappedContacts = currentState.mappedContacts) { is Uninitialized -> renderEmptyState(false) is Loading -> renderLoading() - is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts) + is Success -> renderSuccess(currentState) is Fail -> renderFailure(asyncMappedContacts.error) } } @@ -75,13 +74,13 @@ class ContactsBookController @Inject constructor( } } - private fun renderSuccess(mappedContacts: List, - hasSearch: Boolean, - onlyBoundContacts: Boolean) { + private fun renderSuccess(state: ContactsBookViewState) { + val mappedContacts = state.filteredMappedContacts + if (mappedContacts.isEmpty()) { - renderEmptyState(hasSearch) + renderEmptyState(state.searchTerm.isNotEmpty()) } else { - renderContacts(mappedContacts, onlyBoundContacts) + renderContacts(mappedContacts, state.onlyBoundContacts) } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index c4cf9eab39..6c3ec06f75 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook import android.os.Bundle import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState @@ -29,10 +30,10 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.features.userdirectory.PendingInvitee -import im.vector.app.features.userdirectory.UserDirectoryAction -import im.vector.app.features.userdirectory.UserDirectorySharedAction -import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.app.features.userdirectory.UserDirectoryViewModel +import im.vector.app.features.userdirectory.UserListAction +import im.vector.app.features.userdirectory.UserListSharedAction +import im.vector.app.features.userdirectory.UserListSharedActionViewModel +import im.vector.app.features.userdirectory.UserListViewModel import kotlinx.android.synthetic.main.fragment_contacts_book.* import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User @@ -45,22 +46,38 @@ class ContactsBookFragment @Inject constructor( ) : VectorBaseFragment(), ContactsBookController.Callback { override fun getLayoutResId() = R.layout.fragment_contacts_book - private val viewModel: UserDirectoryViewModel by activityViewModel() + private val viewModel: UserListViewModel by activityViewModel() // Use activityViewModel to avoid loading several times the data private val contactsBookViewModel: ContactsBookViewModel by activityViewModel() - private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel + private lateinit var sharedActionViewModel: UserListSharedActionViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) setupRecyclerView() setupFilterView() + setupConsentView() setupOnlyBoundContactsView() setupCloseView() } + private fun setupConsentView() { + phoneBookSearchForMatrixContacts.setOnClickListener { + withState(contactsBookViewModel) { state -> + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: "")) + .setPositiveButton(R.string.yes) { _, _ -> + contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) + } + .setNegativeButton(R.string.no, null) + .show() + } + } + } + private fun setupOnlyBoundContactsView() { phoneBookOnlyBoundContacts.checkedChanges() .subscribe { @@ -93,24 +110,25 @@ class ContactsBookFragment @Inject constructor( private fun setupCloseView() { phoneBookClose.debouncedClicks { - sharedActionViewModel.post(UserDirectorySharedAction.GoBack) + sharedActionViewModel.post(UserListSharedAction.GoBack) } } override fun invalidate() = withState(contactsBookViewModel) { state -> + phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved contactsBookController.setData(state) } override fun onMatrixIdClick(matrixId: String) { view?.hideKeyboard() - viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) - sharedActionViewModel.post(UserDirectorySharedAction.GoBack) + viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) + sharedActionViewModel.post(UserListSharedAction.GoBack) } override fun onThreePidClick(threePid: ThreePid) { view?.hideKeyboard() - viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) - sharedActionViewModel.post(UserDirectorySharedAction.GoBack) + viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) + sharedActionViewModel.post(UserListSharedAction.GoBack) } } 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 167660d11e..2c4c5d0596 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 @@ -38,11 +38,10 @@ 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 -private typealias PhoneBookSearch = String - class ContactsBookViewModel @AssistedInject constructor(@Assisted initialState: ContactsBookViewState, private val contactsDataSource: ContactsDataSource, @@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted private fun loadContacts() { setState { copy( - mappedContacts = Loading() + mappedContacts = Loading(), + identityServerUrl = session.identityService().getCurrentIdentityServerUrl(), + userConsent = session.identityService().getUserConsent() ) } @@ -109,6 +110,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } private fun performLookup(data: List) { + if (!session.identityService().getUserConsent()) { + return + } viewModelScope.launch { val threePids = data.flatMap { contact -> contact.emails.map { ThreePid.Email(it.email) } + @@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } session.identityService().lookUp(threePids, object : MatrixCallback> { override fun onFailure(failure: Throwable) { - // Ignore 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) { @@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted when (action) { is ContactsBookAction.FilterWith -> handleFilterWith(action) is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action) + ContactsBookAction.UserConsentGranted -> handleUserConsentGranted() }.exhaustive } + private fun handleUserConsentGranted() { + session.identityService().setUserConsent(true) + + setState { + copy(userConsent = true) + } + + // Perform the lookup + performLookup(allContacts) + } + private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt index 3e4f4ddcb6..d2ee684c4d 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt @@ -26,10 +26,14 @@ data class ContactsBookViewState( val mappedContacts: Async> = Loading(), // Use to filter contacts by display name val searchTerm: String = "", - // Tru to display only bound contacts with their bound 2pid + // True to display only bound contacts with their bound 2pid val onlyBoundContacts: Boolean = false, // All contacts, filtered by searchTerm and onlyBoundContacts val filteredMappedContacts: List = emptyList(), // True when the identity service has return some data - val isBoundRetrieved: Boolean = false + val isBoundRetrieved: Boolean = false, + // The current identity server url if any + val identityServerUrl: String? = null, + // User consent to perform lookup (send emails to the identity server) + val userConsent: Boolean = false ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 10ab1673e4..6fafe0b977 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -37,28 +37,31 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel -import im.vector.app.features.userdirectory.KnownUsersFragment -import im.vector.app.features.userdirectory.KnownUsersFragmentArgs -import im.vector.app.features.userdirectory.UserDirectoryFragment -import im.vector.app.features.userdirectory.UserDirectorySharedAction -import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.app.features.userdirectory.UserDirectoryViewModel +import im.vector.app.features.userdirectory.UserListFragment +import im.vector.app.features.userdirectory.UserListFragmentArgs +import im.vector.app.features.userdirectory.UserListSharedAction +import im.vector.app.features.userdirectory.UserListSharedActionViewModel +import im.vector.app.features.userdirectory.UserListViewModel +import im.vector.app.features.userdirectory.UserListViewState import kotlinx.android.synthetic.main.activity.* import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import java.net.HttpURLConnection import javax.inject.Inject -class CreateDirectRoomActivity : SimpleFragmentActivity() { +class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory { private val viewModel: CreateDirectRoomViewModel by viewModel() - private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel - @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory + private lateinit var sharedActionViewModel: UserListSharedActionViewModel + @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter @@ -68,31 +71,34 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { injector.inject(this) } + override fun create(initialState: UserListViewState): UserListViewModel { + return userListViewModelFactory.create(initialState) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + + sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel .observe() - .subscribe { sharedAction -> - when (sharedAction) { - UserDirectorySharedAction.OpenUsersDirectory -> - addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) - UserDirectorySharedAction.Close -> finish() - UserDirectorySharedAction.GoBack -> onBackPressed() - is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) - UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook() + .subscribe { action -> + when (action) { + UserListSharedAction.Close -> finish() + UserListSharedAction.GoBack -> onBackPressed() + is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action) + UserListSharedAction.OpenPhoneBook -> openPhoneBook() + UserListSharedAction.AddByQrCode -> openAddByQrCode() }.exhaustive } .disposeOnDestroy() if (isFirstCreation()) { addFragment( R.id.container, - KnownUsersFragment::class.java, - KnownUsersFragmentArgs( + UserListFragment::class.java, + UserListFragmentArgs( title = getString(R.string.fab_menu_create_chat), - menuResId = R.menu.vector_create_direct_room, - isCreatingRoom = true + menuResId = R.menu.vector_create_direct_room ) ) } @@ -101,6 +107,12 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } } + private fun openAddByQrCode() { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA, 0)) { + addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java) + } + } + private fun openPhoneBook() { // Check permission first if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, @@ -116,15 +128,23 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } + } else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { + addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java) + } + } else { + if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { + onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) + } else if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { + onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact) } } } - private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { + private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { if (action.itemId == R.id.action_create_direct_room) { viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers( action.invitees, - action.existingDmRoomId + null )) } } @@ -178,6 +198,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } companion object { + fun getIntent(context: Context): Intent { return Intent(context, CreateDirectRoomActivity::class.java) } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt new file mode 100644 index 0000000000..3fee3a3285 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt @@ -0,0 +1,122 @@ +/* + * Copyright 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.createdirect + +import android.widget.Toast +import com.airbnb.mvrx.activityViewModel +import com.google.zxing.Result +import com.google.zxing.ResultMetadataType +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.features.userdirectory.PendingInvitee +import kotlinx.android.synthetic.main.fragment_qr_code_scanner.* +import me.dm7.barcodescanner.zxing.ZXingScannerView +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 javax.inject.Inject + +class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler { + + private val viewModel: CreateDirectRoomViewModel by activityViewModel() + + override fun getLayoutResId() = R.layout.fragment_qr_code_scanner + + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + if (allGranted) { + startCamera() + } + } + + private fun startCamera() { + // Start camera on resume + scannerView.startCamera() + } + + override fun onResume() { + super.onResume() + view?.hideKeyboard() + // Register ourselves as a handler for scan results. + scannerView.setResultHandler(this) + // Start camera on resume + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { + startCamera() + } + } + + override fun onPause() { + super.onPause() + // Unregister ourselves as a handler for scan results. + scannerView.setResultHandler(null) + // Stop camera on pause + scannerView.stopCamera() + } + + // Copied from https://github.com/markusfisch/BinaryEye/blob/ + // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434 + private fun getRawBytes(result: Result): ByteArray? { + val metadata = result.resultMetadata ?: return null + val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null + var bytes = ByteArray(0) + @Suppress("UNCHECKED_CAST") + for (seg in segments as Iterable) { + bytes += seg + } + // byte segments can never be shorter than the text. + // Zxing cuts off content prefixes like "WIFI:" + return if (bytes.size >= result.text.length) bytes else null + } + + private fun addByQrCode(value: String) { + val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId + + if (mxid === null) { + Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show() + requireActivity().finish() + } else { + val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid) + // The following assumes MXIDs are case insensitive + if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) { + Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show() + requireActivity().finish() + } else { + // Try to get user from known users and fall back to creating a User object from MXID + val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null) + + viewModel.handle( + CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm) + ) + } + } + } + + override fun handleResult(result: Result?) { + if (result === null) { + Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() + requireActivity().finish() + } else { + val rawBytes = getRawBytes(result) + val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) + val value = rawBytesStr ?: result.text + addByQrCode(value) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index be9449b77a..d074c93587 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -38,7 +38,7 @@ import org.matrix.android.sdk.rx.rx class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, private val rawService: RawService, - private val session: Session) + val session: Session) : VectorViewModel(initialState) { @AssistedInject.Factory diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/QuadSLoadingFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/QuadSLoadingFragment.kt new file mode 100644 index 0000000000..a0ab1c86a7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/QuadSLoadingFragment.kt @@ -0,0 +1,25 @@ +/* + * 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.crypto.verification + +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseFragment +import javax.inject.Inject + +class QuadSLoadingFragment @Inject constructor() : VectorBaseFragment() { + override fun getLayoutResId() = R.layout.fragment_progress +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt index a32a9de97f..a5142ad8bf 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt @@ -31,5 +31,6 @@ sealed class VerificationAction : VectorViewModelAction { object SkipVerification : VerificationAction() object VerifyFromPassphrase : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() + object CancelledFromSsss : VerificationAction() object SecuredStorageHasBeenReset : VerificationAction() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 35ea96de6f..a9b76366df 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -155,6 +155,8 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { // all have been reset, so we are verified? viewModel.handle(VerificationAction.SecuredStorageHasBeenReset) } + } else { + viewModel.handle(VerificationAction.CancelledFromSsss) } } @@ -209,6 +211,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { return@withState } + if (state.selfVerificationMode && state.verifyingFrom4S) { + showFragment(QuadSLoadingFragment::class, Bundle()) + return@withState + } if (state.selfVerificationMode && state.verifiedFromPrivateKeys) { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) 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 aa20a9a992..23ed9b6483 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 @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session @@ -70,6 +71,7 @@ data class VerificationBottomSheetViewState( // true when we display the loading and we wait for the other (incoming request) val selfVerificationMode: Boolean = false, val verifiedFromPrivateKeys: Boolean = false, + val verifyingFrom4S: Boolean = false, val isMe: Boolean = false, val currentDeviceCanCrossSign: Boolean = false, val userWantsToCancel: Boolean = false, @@ -170,7 +172,9 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } else { // if the verification is already done you can't cancel anymore - if (state.pendingRequest.invoke()?.cancelConclusion != null || state.sasTransactionState is VerificationTxState.TerminalTxState) { + if (state.pendingRequest.invoke()?.cancelConclusion != null + || state.sasTransactionState is VerificationTxState.TerminalTxState + || state.verifyingFrom4S) { // you cannot cancel anymore } else { setState { @@ -346,6 +350,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) } is VerificationAction.VerifyFromPassphrase -> { + setState { copy(verifyingFrom4S = true) } _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore) } is VerificationAction.GotResultFromSsss -> { @@ -354,56 +359,73 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( VerificationAction.SecuredStorageHasBeenReset -> { if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) { setState { - copy(quadSHasBeenReset = true) + copy(quadSHasBeenReset = true, verifyingFrom4S = false) } } Unit } + VerificationAction.CancelledFromSsss -> { + setState { + copy(verifyingFrom4S = false) + } + } }.exhaustive } private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) { - try { - action.cypherData.fromBase64().inputStream().use { ins -> - val res = session.loadSecureSecret>(ins, action.alias) - val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( - res?.get(MASTER_KEY_SSSS_NAME), - res?.get(USER_SIGNING_KEY_SSSS_NAME), - res?.get(SELF_SIGNING_KEY_SSSS_NAME) - ) - if (trustResult.isVerified()) { - // Sign this device and upload the signature - session.sessionParams.deviceId?.let { deviceId -> - session.cryptoService() - .crossSigningService().trustDevice(deviceId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.w(failure, "Failed to sign my device after recovery") - } - }) - } + viewModelScope.launch(Dispatchers.IO) { + try { + action.cypherData.fromBase64().inputStream().use { ins -> + val res = session.loadSecureSecret>(ins, action.alias) + val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( + res?.get(MASTER_KEY_SSSS_NAME), + res?.get(USER_SIGNING_KEY_SSSS_NAME), + res?.get(SELF_SIGNING_KEY_SSSS_NAME) + ) + if (trustResult.isVerified()) { + // Sign this device and upload the signature + session.sessionParams.deviceId?.let { deviceId -> + session.cryptoService() + .crossSigningService().trustDevice(deviceId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.w(failure, "Failed to sign my device after recovery") + } + }) + } - setState { - copy(verifiedFromPrivateKeys = true) - } + setState { + copy( + verifyingFrom4S = false, + verifiedFromPrivateKeys = true + ) + } - // try to get keybackup key - } else { - // POP UP something - _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(stringProvider.getString(R.string.error_failed_to_import_keys))) + // try the keybackup + tentativeRestoreBackup(res) + } else { + setState { + copy( + verifyingFrom4S = false + ) + } + // POP UP something + _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(stringProvider.getString(R.string.error_failed_to_import_keys))) + } } - - // try the keybackup - tentativeRestoreBackup(res) - Unit + } catch (failure: Throwable) { + setState { + copy( + verifyingFrom4S = false + ) + } + _viewEvents.post( + VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage ?: stringProvider.getString(R.string.unexpected_error))) } - } catch (failure: Throwable) { - _viewEvents.post( - VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage ?: stringProvider.getString(R.string.unexpected_error))) } } private fun tentativeRestoreBackup(res: Map?) { - viewModelScope.launch(Dispatchers.IO) { + GlobalScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { Timber.v("## Keybackup secret not restored from SSSS") diff --git a/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt b/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt index c2cd2e11e3..028d37ff5f 100644 --- a/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt +++ b/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt @@ -28,7 +28,7 @@ import im.vector.app.features.settings.VectorSettingsUrls // Increase this value to show again the disclaimer dialog after an upgrade of the application private const val CURRENT_DISCLAIMER_VALUE = 2 -private const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE" +const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE" fun showDisclaimerDialog(activity: Activity) { val sharedPrefs = DefaultSharedPreferences.getInstance(activity) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt index c66ae69e6a..426f1321e7 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt @@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction { object DisconnectIdentityServer : DiscoverySettingsAction() data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() + data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction() data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt index 306d9bffd1..55c11f3a50 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt @@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor( buildIdentityServerSection(data) val hasIdentityServer = data.identityServer().isNullOrBlank().not() if (hasIdentityServer && !data.termsNotSigned) { + buildConsentSection(data) buildEmailsSection(data.emailList) buildMsisdnSection(data.phoneNumbersList) } @@ -72,6 +73,38 @@ class DiscoverySettingsController @Inject constructor( } } + private fun buildConsentSection(data: DiscoverySettingsState) { + settingsSectionTitleItem { + id("idConsentTitle") + titleResId(R.string.settings_discovery_consent_title) + } + + if (data.userConsent) { + settingsInfoItem { + id("idConsentInfo") + helperTextResId(R.string.settings_discovery_consent_notice_on) + } + settingsButtonItem { + id("idConsentButton") + colorProvider(colorProvider) + buttonTitleId(R.string.settings_discovery_consent_action_revoke) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonClickListener { listener?.onTapUpdateUserConsent(false) } + } + } else { + settingsInfoItem { + id("idConsentInfo") + helperTextResId(R.string.settings_discovery_consent_notice_off) + } + settingsButtonItem { + id("idConsentButton") + colorProvider(colorProvider) + buttonTitleId(R.string.settings_discovery_consent_action_give_consent) + buttonClickListener { listener?.onTapUpdateUserConsent(true) } + } + } + } + private fun buildIdentityServerSection(data: DiscoverySettingsState) { val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) @@ -359,6 +392,7 @@ class DiscoverySettingsController @Inject constructor( fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) fun onTapChangeIdentityServer() fun onTapDisconnectIdentityServer() + fun onTapUpdateUserConsent(newValue: Boolean) fun onTapRetryToRetrieveBindings() } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index bfbc00b15a..08bf2f12f0 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -55,7 +55,7 @@ class DiscoverySettingsFragment @Inject constructor( sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java) controller.listener = this - recyclerView.configureWith(controller) + genericRecyclerView.configureWith(controller) sharedViewModel.navigateEvent.observeEvent(this) { when (it) { @@ -74,7 +74,7 @@ class DiscoverySettingsFragment @Inject constructor( } override fun onDestroyView() { - recyclerView.cleanup() + genericRecyclerView.cleanup() controller.listener = null super.onDestroyView() } @@ -170,6 +170,23 @@ class DiscoverySettingsFragment @Inject constructor( } } + override fun onTapUpdateUserConsent(newValue: Boolean) { + if (newValue) { + withState(viewModel) { state -> + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke())) + .setPositiveButton(R.string.yes) { _, _ -> + viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) + } + .setNegativeButton(R.string.no, null) + .show() + } + } else { + viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false)) + } + } + override fun onTapRetryToRetrieveBindings() { viewModel.handle(DiscoverySettingsAction.RetrieveBinding) } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt index 6b28c07e89..21fbcf1ca7 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt @@ -25,5 +25,6 @@ data class DiscoverySettingsState( val emailList: Async> = Uninitialized, val phoneNumbersList: Async> = Uninitialized, // Can be true if terms are updated - val termsNotSigned: Boolean = false + val termsNotSigned: Boolean = false, + val userConsent: Boolean = false ) : MvRxState 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 0bfcdd9984..0f294e080a 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 @@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor( val identityServerUrl = identityService.getCurrentIdentityServerUrl() val currentIS = state.identityServer() setState { - copy(identityServer = Success(identityServerUrl)) + copy( + identityServer = Success(identityServerUrl), + userConsent = false + ) } if (currentIS != identityServerUrl) retrieveBinding() } @@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor( init { setState { - copy(identityServer = Success(identityService.getCurrentIdentityServerUrl())) + copy( + identityServer = Success(identityService.getCurrentIdentityServerUrl()), + userConsent = identityService.getUserConsent() + ) } startListenToIdentityManager() observeThreePids() @@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) + is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action) is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) @@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor( }.exhaustive } + private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) { + identityService.setUserConsent(action.newConsent) + setState { copy(userConsent = action.newConsent) } + } + private fun disconnectIdentityServer() { setState { copy(identityServer = Loading()) } viewModelScope.launch { try { awaitCallback { session.identityService().disconnect(it) } - setState { copy(identityServer = Success(null)) } + setState { + copy( + identityServer = Success(null), + userConsent = false + ) + } } catch (failure: Throwable) { setState { copy(identityServer = Fail(failure)) } } @@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor( val data = awaitCallback { session.identityService().setNewIdentityServer(action.url, it) } - setState { copy(identityServer = Success(data)) } + setState { + copy( + identityServer = Success(data), + userConsent = false + ) + } retrieveBinding() } catch (failure: Throwable) { setState { copy(identityServer = Fail(failure)) } diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt index c9ad23f1a9..b59b24fe55 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt @@ -27,6 +27,9 @@ import im.vector.app.core.epoxy.onClick @EpoxyModelClass(layout = R.layout.item_settings_continue_cancel) abstract class SettingsContinueCancelItem : EpoxyModelWithHolder() { + @EpoxyAttribute + var continueText: String? = null + @EpoxyAttribute var continueOnClick: ClickListener? = null @@ -37,6 +40,8 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder { - mxSession.getTerms(TermsService.ServiceType.IdentityService, baseUrl, it) - } + val data = mxSession.getTerms(TermsService.ServiceType.IdentityService, baseUrl) // has all been accepted? val resp = data.serverResponse diff --git a/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt b/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt new file mode 100644 index 0000000000..2d6535758e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt @@ -0,0 +1,51 @@ +/* + * 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.form + +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.themes.ThemeUtils + +@EpoxyModelClass(layout = R.layout.item_form_advanced_toggle) +abstract class FormAdvancedToggleItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var title: CharSequence + @EpoxyAttribute var expanded: Boolean = false + @EpoxyAttribute var listener: (() -> Unit)? = null + + override fun bind(holder: Holder) { + super.bind(holder) + val tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary) + val expandedArrowDrawableRes = if (expanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white + val expandedArrowDrawable = ContextCompat.getDrawable(holder.view.context, expandedArrowDrawableRes)?.also { + DrawableCompat.setTint(it, tintColor) + } + holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) + holder.titleView.text = title + holder.view.setOnClickListener { listener?.invoke() } + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind(R.id.itemFormAdvancedToggleTitleView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index 12538d314a..68e2e6b371 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -26,6 +26,7 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_form_text_input) @@ -65,9 +66,7 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputLayout.error = errorMessage // Update only if text is different and value is not null - if (value != null && holder.textInputEditText.text.toString() != value) { - holder.textInputEditText.setText(value) - } + holder.textInputEditText.setTextSafe(value) holder.textInputEditText.isEnabled = enabled inputType?.let { holder.textInputEditText.inputType = it } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt index eadae3ba0c..08fc435e11 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt @@ -26,6 +26,7 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_form_text_input_with_button) @@ -61,9 +62,7 @@ abstract class FormEditTextWithButtonItem : VectorEpoxyModel() { @EpoxyAttribute var summary: String? = null + @EpoxyAttribute + var showDivider: Boolean = true + override fun bind(holder: Holder) { super.bind(holder) holder.view.setOnClickListener { @@ -56,11 +61,12 @@ abstract class FormSwitchItem : VectorEpoxyModel() { holder.switchView.isEnabled = enabled + holder.switchView.setOnCheckedChangeListener(null) holder.switchView.isChecked = switchChecked - holder.switchView.setOnCheckedChangeListener { _, isChecked -> listener?.invoke(isChecked) } + holder.divider.isVisible = showDivider } override fun shouldSaveViewState(): Boolean { @@ -77,5 +83,6 @@ abstract class FormSwitchItem : VectorEpoxyModel() { val titleView by bind(R.id.formSwitchTitle) val summaryView by bind(R.id.formSwitchSummary) val switchView by bind(R.id.formSwitchSwitch) + val divider by bind(R.id.formSwitchDivider) } } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt index 588d939635..a17aa4dbf2 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.grouplist +import androidx.lifecycle.viewModelScope import arrow.core.Option import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory @@ -28,7 +29,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import io.reactivex.Observable import io.reactivex.functions.BiFunction -import org.matrix.android.sdk.api.NoOpMatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams @@ -95,7 +96,9 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state -> if (state.selectedGroup?.groupId != action.groupSummary.groupId) { // We take care of refreshing group data when selecting to be sure we get all the rooms and users - session.getGroup(action.groupSummary.groupId)?.fetchGroupData(NoOpMatrixCallback()) + viewModelScope.launch { + session.getGroup(action.groupSummary.groupId)?.fetchGroupData() + } setState { copy(selectedGroup = action.groupSummary) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 32a4af1b1b..7dde0edf32 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.MenuItem @@ -38,8 +39,12 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.utils.toast import im.vector.app.features.disclaimer.showDisclaimerDialog +import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.notifications.NotificationDrawerManager +import im.vector.app.features.permalink.NavigationInterceptor +import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert @@ -50,10 +55,12 @@ import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState import im.vector.app.push.fcm.FcmHelper +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.matrix.android.sdk.api.session.InitialSyncProgressService +import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber import javax.inject.Inject @@ -64,7 +71,8 @@ data class HomeActivityArgs( val accountCreation: Boolean ) : Parcelable -class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory { +class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory, + NavigationInterceptor { private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -82,6 +90,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var shortcutsHandler: ShortcutsHandler @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory + @Inject lateinit var permalinkHandler: PermalinkHandler private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -145,6 +154,28 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet shortcutsHandler.observeRoomsAndBuildShortcuts() .disposeOnDestroy() + + if (isFirstCreation()) { + handleIntent(intent) + } + } + + private fun handleIntent(intent: Intent?) { + intent?.dataString?.let { deepLink -> + if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let + + permalinkHandler.launch(this, deepLink, + navigationInterceptor = this, + buildTask = true) + // .delay(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { isHandled -> + if (!isHandled) { + toast(R.string.permalink_malformed) + } + } + .disposeOnDestroy() + } } private fun renderState(state: HomeActivityViewState) { @@ -270,6 +301,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet if (intent?.getParcelableExtra(MvRx.KEY_ARG)?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + handleIntent(intent) } override fun onDestroy() { @@ -313,11 +345,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } - R.id.menu_home_setting -> { + R.id.menu_home_setting -> { navigator.openSettings(this) return true } @@ -334,6 +366,18 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet } } + override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { + val listener = object : MatrixToBottomSheet.InteractionListener { + override fun navigateToRoom(roomId: String) { + navigator.openRoom(this@HomeActivity, roomId) + } + } + // TODO check if there is already one?? + MatrixToBottomSheet.withLink(deepLink.toString(), listener) + .show(supportFragmentManager, "HA#MatrixToBottomSheet") + return true + } + companion object { fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { val args = HomeActivityArgs( diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index 2a29e13572..7753a7f58b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -22,6 +22,6 @@ import org.matrix.android.sdk.api.util.MatrixItem sealed class HomeActivityViewEvents : VectorViewEvents { data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents() data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents() - data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents() + data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents() object PromptToEnableSessionPush : HomeActivityViewEvents() } 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 48a71db35c..680ec17415 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 @@ -78,29 +78,30 @@ class HomeActivityViewModel @AssistedInject constructor( } private fun observeCrossSigningReset() { - val safeActiveSession = activeSessionHolder.getSafeActiveSession() - val crossSigningService = safeActiveSession - ?.cryptoService() - ?.crossSigningService() - onceTrusted = crossSigningService - ?.allPrivateKeysKnown() ?: false + val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return + + onceTrusted = safeActiveSession + .cryptoService() + .crossSigningService().allPrivateKeysKnown() safeActiveSession - ?.rx() - ?.liveCrossSigningInfo(safeActiveSession.myUserId) - ?.subscribe { + .rx() + .liveCrossSigningInfo(safeActiveSession.myUserId) + .subscribe { val isVerified = it.getOrNull()?.isTrusted() ?: false if (!isVerified && onceTrusted) { // cross signing keys have been reset - // Tigger a popup to re-verify - _viewEvents.post( - HomeActivityViewEvents.OnCrossSignedInvalidated( - safeActiveSession.getUser(safeActiveSession.myUserId)?.toMatrixItem() - ) - ) + // Trigger a popup to re-verify + // Note: user can be null in case of logout + safeActiveSession.getUser(safeActiveSession.myUserId) + ?.toMatrixItem() + ?.let { user -> + _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user)) + } } onceTrusted = isVerified - }?.disposeOnClear() + } + .disposeOnClear() } private fun observeInitialSync() { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt index e267248fc3..1a60d8e219 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt @@ -18,15 +18,19 @@ package im.vector.app.features.home import android.os.Bundle import android.view.View +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat import androidx.core.view.isVisible import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.observeK import im.vector.app.core.extensions.replaceChildFragment import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.features.grouplist.GroupListFragment import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity +import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.workers.signout.SignOutUiWorker import kotlinx.android.synthetic.main.fragment_home_drawer.* import org.matrix.android.sdk.api.session.Session @@ -75,6 +79,32 @@ class HomeDrawerFragment @Inject constructor( SignOutUiWorker(requireActivity()).perform() } + homeDrawerQRCodeButton.debouncedClicks { + UserCodeActivity.newIntent(requireContext(), sharedActionViewModel.session.myUserId).let { + val options = + ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + homeDrawerHeaderAvatarView, + ViewCompat.getTransitionName(homeDrawerHeaderAvatarView) ?: "" + ) + startActivity(it, options.toBundle()) + } + } + + homeDrawerInviteFriendButton.debouncedClicks { + session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink -> + val text = getString(R.string.invite_friends_text, permalink) + + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = getString(R.string.invite_friends), + text = text, + extraTitle = getString(R.string.invite_friends_rich_title) + ) + } + } + // Debug menu homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode() homeDrawerHeaderDebugView.debouncedClicks { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt index 58747a4c18..b695f48ee5 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home import im.vector.app.core.platform.VectorSharedActionViewModel +import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +class HomeSharedActionViewModel @Inject constructor(val session: Session) : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 99adc0bf83..8891218a11 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -16,6 +16,8 @@ package im.vector.app.features.home.room.detail +import android.net.Uri +import android.view.View import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event @@ -24,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.MatrixItem sealed class RoomDetailAction : VectorViewModelAction { data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() @@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction { data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() + object QuickActionInvitePeople : RoomDetailAction() + object QuickActionSetAvatar : RoomDetailAction() + data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction() + object QuickActionSetTopic : RoomDetailAction() + data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction() } 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 a1dbc5f014..8dc85bd8af 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 @@ -71,6 +71,7 @@ import com.google.android.material.textfield.TextInputEditText import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder +import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.withColoredButton import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup @@ -82,6 +83,7 @@ import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequests +import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -141,6 +143,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsB import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan +import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -149,6 +152,7 @@ import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.reactions.EmojiReactionPickerActivity +import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData @@ -196,6 +200,7 @@ 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 import javax.inject.Inject @@ -221,7 +226,8 @@ class RoomDetailFragment @Inject constructor( private val callManager: WebRtcCallManager, private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, - private val roomDetailPendingActionStore: RoomDetailPendingActionStore + private val roomDetailPendingActionStore: RoomDetailPendingActionStore, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -229,7 +235,7 @@ class RoomDetailFragment @Inject constructor( JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, -// RoomWidgetsBannerView.Callback, + GalleryOrCameraDialogHelper.Listener, ActiveCallView.Callback { companion object { @@ -250,10 +256,15 @@ class RoomDetailFragment @Inject constructor( private const val ircPattern = " (IRC)" } + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val roomDetailArgs: RoomDetailArgs by args() private val glideRequests by lazy { GlideApp.with(this) } + private val pillsPostProcessor by lazy { + pillsPostProcessorFactory.create(roomDetailArgs.roomId) + } private val autoCompleter: AutoCompleter by lazy { autoCompleterFactory.create(roomDetailArgs.roomId) @@ -364,6 +375,12 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) + RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) + RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() + RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() + is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> + navigator.openBigImageViewer(requireActivity(), it.view, item) + } }.exhaustive } @@ -372,6 +389,24 @@ class RoomDetailFragment @Inject constructor( } } + override fun onImageReady(uri: Uri?) { + uri ?: return + roomDetailViewModel.handle( + RoomDetailAction.SetAvatarAction( + newAvatarUri = uri, + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) + ) + } + + private fun handleOpenRoomSettings() { + navigator.openRoomProfile( + requireContext(), + roomDetailArgs.roomId, + RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS + ) + } + private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) { navigator.openRoom(requireContext(), openRoom.roomId, null) } @@ -508,7 +543,7 @@ class RoomDetailFragment @Inject constructor( modelBuildListener = null autoCompleter.clear() debouncer.cancelAll() - recyclerView.cleanup() + timelineRecyclerView.cleanup() super.onDestroyView() } @@ -535,7 +570,7 @@ class RoomDetailFragment @Inject constructor( jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( jumpToBottomView, debouncer, - recyclerView, + timelineRecyclerView, layoutManager ) } @@ -558,7 +593,7 @@ class RoomDetailFragment @Inject constructor( if (scrollPosition == null) { scrollOnHighlightedEventCallback.scheduleScrollTo(action.eventId) } else { - recyclerView.stopScroll() + timelineRecyclerView.stopScroll() layoutManager.scrollToPosition(scrollPosition) } } @@ -848,7 +883,7 @@ class RoomDetailFragment @Inject constructor( if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody ?: messageContent.body) - formattedBody = eventHtmlRenderer.render(document) + formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor) } composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) @@ -969,14 +1004,14 @@ class RoomDetailFragment @Inject constructor( timelineEventController.callback = this timelineEventController.timeline = roomDetailViewModel.timeline - recyclerView.trackItemsVisibilityChange() + timelineRecyclerView.trackItemsVisibilityChange() layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) - scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(recyclerView, layoutManager, timelineEventController) - recyclerView.layoutManager = layoutManager - recyclerView.itemAnimator = null - recyclerView.setHasFixedSize(true) + scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(timelineRecyclerView, layoutManager, timelineEventController) + timelineRecyclerView.layoutManager = layoutManager + timelineRecyclerView.itemAnimator = null + timelineRecyclerView.setHasFixedSize(true) modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) @@ -985,7 +1020,7 @@ class RoomDetailFragment @Inject constructor( jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() } timelineEventController.addModelBuildListener(modelBuildListener) - recyclerView.adapter = timelineEventController.adapter + timelineRecyclerView.adapter = timelineEventController.adapter if (vectorPreferences.swipeToReplyIsEnabled()) { val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler { @@ -1015,9 +1050,9 @@ class RoomDetailFragment @Inject constructor( } val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler) val touchHelper = ItemTouchHelper(swipeCallback) - touchHelper.attachToRecyclerView(recyclerView) + touchHelper.attachToRecyclerView(timelineRecyclerView) } - recyclerView.addGlidePreloader( + timelineRecyclerView.addGlidePreloader( epoxyController = timelineEventController, requestManager = GlideApp.with(this), preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ -> @@ -1425,7 +1460,7 @@ class RoomDetailFragment @Inject constructor( return false } - override fun navToMemberProfile(userId: String): Boolean { + override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { openRoomMemberProfile(userId) return true } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index b9e3e6b31d..d5d94a0ca5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -17,10 +17,12 @@ package im.vector.app.features.home.room.detail import android.net.Uri +import android.view.View import androidx.annotation.StringRes import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.command.Command import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import java.io.File @@ -43,6 +45,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() + object OpenInvitePeople : RoomDetailViewEvents() + object OpenSetRoomAvatarDialog : RoomDetailViewEvents() + object OpenRoomSettings : RoomDetailViewEvents() + data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() + object ShowWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents() 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 a317177bc4..27cd1b24ae 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 @@ -50,6 +50,7 @@ import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.commonmark.parser.Parser @@ -99,6 +100,7 @@ import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber import java.io.File +import java.lang.Exception import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -164,7 +166,7 @@ class RoomDetailViewModel @AssistedInject constructor( getUnreadState() observeSyncState() observeEventDisplayedActions() - getDraftIfAny() + loadDraftIfAny() observeUnreadState() observeMyRoomMember() observeActiveRoomWidgets() @@ -275,9 +277,39 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { + _viewEvents.post( + RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) + ) + } }.exhaustive } + private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it) + } + _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) + } + } + } + + private fun handleInvitePeople() { + _viewEvents.post(RoomDetailViewEvents.OpenInvitePeople) + } + + private fun handleQuickSetAvatar() { + _viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog) + } + private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) { val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId) if (existingDmRoomId == null) { @@ -475,28 +507,30 @@ class RoomDetailViewModel @AssistedInject constructor( * Convert a send mode to a draft and save the draft */ private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { - when { - it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { - setState { copy(sendMode = it.sendMode.copy(action.draft)) } - room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.REPLY -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.QUOTE -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.EDIT -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + viewModelScope.launch(NonCancellable) { + when { + it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { + setState { copy(sendMode = it.sendMode.copy(action.draft)) } + room.saveDraft(UserDraft.REGULAR(action.draft)) + } + it.sendMode is SendMode.REPLY -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.QUOTE -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.EDIT -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } } } } - private fun getDraftIfAny() { - val currentDraft = room.getDraft() ?: return + private fun loadDraftIfAny() { + val currentDraft = room.getDraft() setState { copy( // Create a sendMode from a draft and retrieve the TimelineEvent @@ -517,6 +551,7 @@ class RoomDetailViewModel @AssistedInject constructor( SendMode.EDIT(timelineEvent, currentDraft.text) } } + else -> null } ?: SendMode.REGULAR("", fromSharing = false) ) } @@ -772,11 +807,13 @@ class RoomDetailViewModel @AssistedInject constructor( private fun popDraft() = withState { if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { // If we were sharing, we want to get back our last value from draft - getDraftIfAny() + loadDraftIfAny() } else { // Otherwise we clear the composer and remove the draft from db setState { copy(sendMode = SendMode.REGULAR("", false)) } - room.deleteDraft(NoOpMatrixCallback()) + viewModelScope.launch { + room.deleteDraft() + } } } @@ -1111,15 +1148,15 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleReportContent(action: RoomDetailAction.ReportContent) { - room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + viewModelScope.launch { + val event = try { + room.reportContent(action.eventId, -100, action.reason) + RoomDetailViewEvents.ActionSuccess(action) + } catch (failure: Exception) { + RoomDetailViewEvents.ActionFailure(action, failure) } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) - } - }) + _viewEvents.post(event) + } } private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { @@ -1300,7 +1337,7 @@ class RoomDetailViewModel @AssistedInject constructor( } if (summary.membership == Membership.INVITE) { summary.inviterId?.let { inviterId -> - session.getUser(inviterId) + session.getRoomMember(inviterId, summary.roomId) }?.also { setState { copy(asyncInviter = Success(it)) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index b31c972d1a..38b93f9363 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -25,7 +25,6 @@ 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.timeline.TimelineEvent import org.matrix.android.sdk.api.session.sync.SyncState -import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.widgets.model.Widget /** @@ -60,7 +59,7 @@ data class RoomDetailViewState( val roomId: String, val eventId: String?, val myRoomMember: Async = Uninitialized, - val asyncInviter: Async = Uninitialized, + val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val activeRoomWidgets: Async> = Uninitialized, val typingMessage: String? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index b927fb5ff3..f661aa5ba9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -123,7 +123,7 @@ class SearchResultController @Inject constructor( .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)) .spannable(spannable) .sender(eventAndSender.sender - ?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem()) + ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem()) .listener { listener?.onItemClicked(eventAndSender.event) } .let { result.add(it) } } 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 0d1e2261cd..716fdca2ad 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 @@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.app.features.html.EventHtmlRenderer +import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.reactions.data.EmojiDataSource @@ -57,18 +58,22 @@ import java.util.ArrayList * Information related to an event and used to display preview in contextual bottom sheet. */ class MessageActionsViewModel @AssistedInject constructor(@Assisted - initialState: MessageActionState, + private val initialState: MessageActionState, private val eventHtmlRenderer: Lazy, private val htmlCompressor: VectorHtmlCompressor, private val session: Session, private val noticeEventFormatter: NoticeEventFormatter, private val stringProvider: StringProvider, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { private val eventId = initialState.eventId private val informationData = initialState.informationData private val room = session.getRoom(initialState.roomId) + private val pillsPostProcessor by lazy { + pillsPostProcessorFactory.create(initialState.roomId) + } @AssistedInject.Factory interface Factory { @@ -164,7 +169,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted return when (timelineEvent.root.getClearType()) { EventType.MESSAGE, - EventType.STICKER -> { + EventType.STICKER -> { val messageContent: MessageContent? = timelineEvent.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val html = messageContent.formattedBody @@ -172,7 +177,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted ?.let { htmlCompressor.compress(it) } ?: messageContent.body - eventHtmlRenderer.get().render(html) + eventHtmlRenderer.get().render(html, pillsPostProcessor) } else if (messageContent is MessageVerificationRequestContent) { stringProvider.getString(R.string.verification_request) } else { @@ -186,6 +191,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted 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, 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 e7a911ceb1..23bd041e95 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 @@ -31,10 +31,14 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_ +import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue 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.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper 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 @@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } + val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId + ?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) } + ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel() } + ?.let { PowerLevelsHelper(it) } + val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" val attributes = MergedRoomCreationItem.Attributes( isCollapsed = isCollapsed, mergeData = mergedData, @@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde hasEncryptionEvent = hasEncryption, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, readReceiptsCallback = callback, - currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" + callback = callback, + currentUserId = currentUserId, + roomSummary = roomSummaryHolder.roomSummary, + canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, + canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, + canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false ) MergedRoomCreationItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(isCollapsed && highlighted) .attributes(attributes) + .movementMethod(createLinkMovementMethod(callback)) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) } 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 dd7a87cce6..2b067ccf3f 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 @@ -60,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer +import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -106,15 +107,19 @@ class MessageItemFactory @Inject constructor( private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val session: Session) { + private val pillsPostProcessor by lazy { + pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId) + } + fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent) if (event.root.isRedacted()) { @@ -139,16 +144,16 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) + 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, roomSummaryHolder.roomSummary, callback) + is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -159,7 +164,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { return when (messageContent.optionType) { - OPTION_TYPE_POLL -> { + OPTION_TYPE_POLL -> { MessagePollItem_() .attributes(attributes) .callback(callback) @@ -217,13 +222,17 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): VerificationRequestItem? { // If this request is not sent by me or sent to me, we should ignore it in timeline val myUserId = session.myUserId + val roomId = roomSummaryHolder.roomSummary?.roomId if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { return null } val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId - val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName - else informationData.memberName + val otherUserName = if (informationData.sentByMe) { + session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName + } else { + informationData.memberName + } return VerificationRequestItem_() .attributes( VerificationRequestItem.Attributes( @@ -362,7 +371,7 @@ class MessageItemFactory @Inject constructor( val codeVisitor = CodeVisitor() codeVisitor.visit(localFormattedBody) when (codeVisitor.codeKind) { - CodeVisitor.Kind.BLOCK -> { + CodeVisitor.Kind.BLOCK -> { val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) if (codeFormattedBlock == null) { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) @@ -378,7 +387,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) } } - CodeVisitor.Kind.NONE -> { + CodeVisitor.Kind.NONE -> { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) } } @@ -393,7 +402,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { val compressed = htmlCompressor.compress(messageContent.formattedBody!!) - val formattedBody = htmlRenderer.get().render(compressed) + val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes) } @@ -528,7 +537,7 @@ class MessageItemFactory @Inject constructor( private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence { return matrixFormattedBody ?.let { htmlCompressor.compress(it) } - ?.let { htmlRenderer.get().render(it) } + ?.let { htmlRenderer.get().render(it, pillsPostProcessor) } ?: body } 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 1a4db3bdfc..243cbbd0e6 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 @@ -53,10 +53,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_AVATAR, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET, @@ -78,6 +78,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me encryptedItemFactory.create(event, nextEvent, highlight, callback) } } + EventType.STATE_ROOM_ALIASES, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_KEY, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 8055ef9a99..9b828e9410 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,6 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.format import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.extensions.appendNl 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.EventType @@ -35,6 +37,7 @@ 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.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent @@ -48,9 +51,12 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import timber.log.Timber import javax.inject.Inject -class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource, - private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, - private val sp: StringProvider) { +class NoticeEventFormatter @Inject constructor( + private val activeSessionDataSource: ActiveSessionDataSource, + private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, + private val vectorPreferences: VectorPreferences, + private val sp: StringProvider +) { private val currentUserId: String? get() = activeSessionDataSource.currentValue?.orNull()?.myUserId @@ -72,6 +78,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) + EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_WIDGET, @@ -253,13 +260,13 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null - val formattedVisibility = roomHistoryVisibilityFormatter.format(historyVisibility) + val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) return if (event.isSentByCurrentUser()) { sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you, - formattedVisibility) + historyVisibilitySuffix) } else { sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility, - senderName, formattedVisibility) + senderName, historyVisibilitySuffix) } } @@ -383,23 +390,147 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour } } + private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? { + val eventContent = event.getClearContent().toModel() ?: return null + val prevEventContent = event.resolvedPrevContent()?.toModel() + + return buildString { + // Title + append(if (prevEventContent == null) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_server_acl_set_title_by_you) + } else { + sp.getString(R.string.notice_room_server_acl_set_title, senderName) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_server_acl_updated_title_by_you) + } else { + sp.getString(R.string.notice_room_server_acl_updated_title, senderName) + } + }) + if (eventContent.allowList.isEmpty()) { + // Special case for stuck room + appendNl(sp.getString(R.string.notice_room_server_acl_allow_is_empty)) + } else if (vectorPreferences.developerMode()) { + // Details, only in developer mode + appendAclDetails(eventContent, prevEventContent) + } + } + } + + private fun StringBuilder.appendAclDetails(eventContent: RoomServerAclContent, prevEventContent: RoomServerAclContent?) { + if (prevEventContent == null) { + eventContent.allowList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_allowed, it)) } + eventContent.denyList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_banned, it)) } + if (eventContent.allowIpLiterals) { + appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_allowed)) + } else { + appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_not_allowed)) + } + } else { + // Display only diff + var hasChanged = false + // New allowed servers + (eventContent.allowList - prevEventContent.allowList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_allowed, it)) } + // Removed allowed servers + (prevEventContent.allowList - eventContent.allowList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_allowed, it)) } + // New denied servers + (eventContent.denyList - prevEventContent.denyList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_banned, it)) } + // Removed denied servers + (prevEventContent.denyList - eventContent.denyList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_banned, it)) } + + if (prevEventContent.allowIpLiterals != eventContent.allowIpLiterals) { + hasChanged = true + if (eventContent.allowIpLiterals) { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_allowed)) + } else { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_not_allowed)) + } + } + + if (!hasChanged) { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_no_change)) + } + } + } + private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? { val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel() - val canonicalAlias = eventContent?.canonicalAlias - return canonicalAlias - ?.takeIf { it.isNotBlank() } - ?.let { + val prevContent: RoomCanonicalAliasContent? = event.resolvedPrevContent().toModel() + val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() } + val prevCanonicalAlias = prevContent?.canonicalAlias?.takeIf { it.isNotEmpty() } + val altAliases = eventContent?.alternativeAliases.orEmpty() + val prevAltAliases = prevContent?.alternativeAliases.orEmpty() + val added = altAliases - prevAltAliases + val removed = prevAltAliases - altAliases + + return when { + added.isEmpty() && removed.isEmpty() && canonicalAlias == prevCanonicalAlias -> { + // No difference between the two events say something as we can't simply hide the event from here + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_no_change_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_no_change, senderName) + } + } + added.isEmpty() && removed.isEmpty() -> { + // Canonical has changed + if (canonicalAlias != null) { if (event.isSentByCurrentUser()) { - sp.getString(R.string.notice_room_canonical_alias_set_by_you, it) + sp.getString(R.string.notice_room_canonical_alias_set_by_you, canonicalAlias) } else { - sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) + sp.getString(R.string.notice_room_canonical_alias_set, senderName, canonicalAlias) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_unset_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_unset, senderName) } } - ?: if (event.isSentByCurrentUser()) { - sp.getString(R.string.notice_room_canonical_alias_unset_by_you) + } + added.isEmpty() && canonicalAlias == prevCanonicalAlias -> { + // Some alternative has been removed + if (event.isSentByCurrentUser()) { + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_removed_by_you, removed.size, removed.joinToString()) } else { - sp.getString(R.string.notice_room_canonical_alias_unset, senderName) + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_removed, removed.size, senderName, removed.joinToString()) } + } + removed.isEmpty() && canonicalAlias == prevCanonicalAlias -> { + // Some alternative has been added + if (event.isSentByCurrentUser()) { + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_added_by_you, added.size, added.joinToString()) + } else { + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_added, added.size, senderName, added.joinToString()) + } + } + canonicalAlias == prevCanonicalAlias -> { + // Alternative added and removed + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_alternative_changed_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_alternative_changed, senderName) + } + } + else -> { + // Main and removed, or main and added, or main and added and removed + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_main_and_alternative_changed_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_main_and_alternative_changed, senderName) + } + } + } } private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt index 4563e6a6ed..14769bc95b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt @@ -24,13 +24,21 @@ import javax.inject.Inject class RoomHistoryVisibilityFormatter @Inject constructor( private val stringProvider: StringProvider ) { + fun getNoticeSuffix(roomHistoryVisibility: RoomHistoryVisibility): String { + return stringProvider.getString(when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE -> R.string.notice_room_visibility_world_readable + RoomHistoryVisibility.SHARED -> R.string.notice_room_visibility_shared + RoomHistoryVisibility.INVITED -> R.string.notice_room_visibility_invited + RoomHistoryVisibility.JOINED -> R.string.notice_room_visibility_joined + }) + } - fun format(roomHistoryVisibility: RoomHistoryVisibility): String { - return when (roomHistoryVisibility) { - RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) - } + fun getSetting(roomHistoryVisibility: RoomHistoryVisibility): String { + return stringProvider.getString(when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE -> R.string.room_settings_read_history_entry_anyone + RoomHistoryVisibility.SHARED -> R.string.room_settings_read_history_entry_members_only_option_time_shared + RoomHistoryVisibility.INVITED -> R.string.room_settings_read_history_entry_members_only_invited + RoomHistoryVisibility.JOINED -> R.string.room_settings_read_history_entry_members_only_joined + }) } } 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 14b8c12fee..4fcac6c7f7 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 @@ -33,6 +33,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_POWER_LEVELS, EventType.CALL_INVITE, EventType.CALL_HANGUP, 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 1896a812fc..34b9ae1b9d 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 @@ -16,11 +16,14 @@ package im.vector.app.features.home.room.detail.timeline.item +import android.text.SpannableString +import android.text.method.MovementMethod +import android.text.style.ClickableSpan import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.RelativeLayout import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible @@ -28,8 +31,16 @@ import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.core.utils.tappableMatchingText import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.tools.linkify +import me.gujun.android.span.span +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.toMatrixItem @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedRoomCreationItem : BasedMergedItem() { @@ -37,11 +48,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem { - this.marginEnd = leftGuideline - } - if (attributes.isEncryptionAlgorithmSecure) { - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) - holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) { - holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description) - } else { - holder.expandView.resources.getString(R.string.encryption_enabled_tile_description) - } - holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER - holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), - null, null, null - ) - } else { - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) - holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) - holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), - null, null, null - ) - } - } else { - holder.encryptionTile.isVisible = false - } + bindEncryptionTile(holder, data) } else { holder.avatarView.visibility = View.INVISIBLE holder.summaryView.visibility = View.GONE @@ -107,6 +96,109 @@ abstract class MergedRoomCreationItem : BasedMergedItem { + this.marginEnd = leftGuideline + } + if (attributes.isEncryptionAlgorithmSecure) { + holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) + holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) { + holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description) + } else { + holder.expandView.resources.getString(R.string.encryption_enabled_tile_description) + } + holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER + holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), + null, null, null + ) + } else { + holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) + holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) + holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), + null, null, null + ) + } + } else { + holder.encryptionTile.isVisible = false + } + } + + private fun bindCreationSummaryTile(holder: Holder) { + val roomSummary = attributes.roomSummary + val roomDisplayName = roomSummary?.displayName + holder.roomNameText.setTextOrHide(roomDisplayName) + val isDirect = roomSummary?.isDirect == true + val membersCount = roomSummary?.otherMemberIds?.size ?: 0 + + if (isDirect) { + holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName ?: "") + } else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) { + holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name) + } else { + holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName) + } + + val topic = roomSummary?.topic + if (topic.isNullOrBlank()) { + // do not show hint for DMs or group DMs + if (!isDirect) { + val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text) + val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink)) + holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() { + override fun onClick(widget: View) { + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic) + } + })) + } + } else { + holder.roomTopicText.setTextOrHide( + span { + span(holder.view.resources.getString(R.string.topic_prefix)) { + textStyle = "bold" + } + +topic.linkify(attributes.callback) + } + ) + } + holder.roomTopicText.movementMethod = movementMethod + + val roomItem = roomSummary?.toMatrixItem() + val shouldSetAvatar = attributes.canChangeAvatar + && (roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) + && roomItem?.avatarUrl.isNullOrBlank() + + holder.roomAvatarImageView.isVisible = roomItem != null + if (roomItem != null) { + attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView) + holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view -> + if (shouldSetAvatar) { + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar) + } else { + // Note: this is no op if there is no avatar on the room + attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view)) + } + })) + } + + holder.setAvatarButton.isVisible = shouldSetAvatar + if (shouldSetAvatar) { + holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ -> + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar) + })) + } + + holder.addPeopleButton.isVisible = !isDirect + if (!isDirect) { + holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ -> + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople) + })) + } + } + class Holder : BasedMergedItem.Holder(STUB_ID) { val summaryView by bind(R.id.itemNoticeTextView) val avatarView by bind(R.id.itemNoticeAvatarView) @@ -114,6 +206,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem(R.id.itemVerificationDoneTitleTextView) val e2eTitleDescriptionView by bind(R.id.itemVerificationDoneDetailTextView) + + val roomNameText by bind(R.id.roomNameTileText) + val roomDescriptionText by bind(R.id.roomNameDescriptionText) + val roomTopicText by bind(R.id.roomNameTopicText) + val roomAvatarImageView by bind(R.id.creationTileRoomAvatarImageView) + val addPeopleButton by bind(R.id.creationTileAddPeopleButton) + val setAvatarButton by bind(R.id.creationTileSetAvatarButton) } companion object { @@ -126,8 +225,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem Unit, + val callback: TimelineEventController.Callback? = null, val currentUserId: String, val hasEncryptionEvent: Boolean, - val isEncryptionAlgorithmSecure: Boolean + val isEncryptionAlgorithmSecure: Boolean, + val roomSummary: RoomSummary?, + val canChangeAvatar: Boolean = false, + val canChangeName: Boolean = false, + val canChangeTopic: Boolean = false ) : BasedMergedItem.Attributes } diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt index 0884844777..42bef2ddae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt @@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.features.home.room.list.widget.FabMenuView +import im.vector.app.features.home.room.list.widget.NotifsFabMenuView @EpoxyModelClass(layout = R.layout.item_room_filter_footer) abstract class FilteredRoomFooterItem : VectorEpoxyModel() { @@ -46,7 +46,7 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel(R.id.roomFilterFooterOpenRoomDirectory) } - interface FilteredRoomFooterItemListener : FabMenuView.Listener { + interface FilteredRoomFooterItemListener : NotifsFabMenuView.Listener { fun createRoom(initialName: String) } } 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 f1d35a74d5..b4f525c119 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 @@ -45,7 +45,7 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel -import im.vector.app.features.home.room.list.widget.FabMenuView +import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.notifications.NotificationDrawerManager import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_list.* @@ -66,8 +66,7 @@ class RoomListFragment @Inject constructor( val roomListViewModelFactory: RoomListViewModel.Factory, private val notificationDrawerManager: NotificationDrawerManager, private val sharedViewPool: RecyclerView.RecycledViewPool - -) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { +) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, NotifsFabMenuView.Listener { private var modelBuildListener: OnModelBuildFinishedListener? = null private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel @@ -295,28 +294,30 @@ class RoomListFragment @Inject constructor( RoomListDisplayMode.NOTIFICATIONS -> { if (hasNoRoom) { StateView.State.Empty( - getString(R.string.room_list_catchup_welcome_title), - ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_catchup), - getString(R.string.room_list_catchup_welcome_body) + 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 { StateView.State.Empty( - getString(R.string.room_list_catchup_empty_title), - ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper), - getString(R.string.room_list_catchup_empty_body)) + 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( - getString(R.string.room_list_people_empty_title), - ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat), - getString(R.string.room_list_people_empty_body) + 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( - getString(R.string.room_list_rooms_empty_title), - ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group), - getString(R.string.room_list_rooms_empty_body) + 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 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 c32629d6ae..84652506cd 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 @@ -33,9 +33,9 @@ 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.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.tag.RoomTag -import org.matrix.android.sdk.internal.util.awaitCallback 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, @@ -169,11 +169,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) { - session.getRoom(action.roomId)?.setRoomNotificationState(action.notificationState, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomListViewEvents.Failure(failure)) + val room = session.getRoom(action.roomId) + if (room != null) { + viewModelScope.launch { + try { + room.setRoomNotificationState(action.notificationState) + } catch (failure: Exception) { + _viewEvents.post(RoomListViewEvents.Failure(failure)) + } } - }) + } } private fun handleToggleTag(action: RoomListAction.ToggleTag) { @@ -185,17 +190,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, action.tag.otherTag() ?.takeIf { room.roomSummary()?.hasTag(it).orFalse() } ?.let { tagToRemove -> - awaitCallback { room.deleteTag(tagToRemove, it) } + room.deleteTag(tagToRemove) } // Set the tag. We do not handle the order for the moment - awaitCallback { - room.addTag(action.tag, 0.5, it) - } + room.addTag(action.tag, 0.5) } else { - awaitCallback { - room.deleteTag(action.tag, it) - } + room.deleteTag(action.tag) } } catch (failure: Throwable) { _viewEvents.post(RoomListViewEvents.Failure(failure)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt index e3a5db4b97..f41104cae1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -77,6 +77,7 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R override fun onDestroyView() { recyclerView.cleanup() + roomListActionsEpoxyController.listener = null super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/widget/FabMenuView.kt b/vector/src/main/java/im/vector/app/features/home/room/list/widget/NotifsFabMenuView.kt similarity index 88% rename from vector/src/main/java/im/vector/app/features/home/room/list/widget/FabMenuView.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/widget/NotifsFabMenuView.kt index f9058840d2..7c96f40dbf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/widget/FabMenuView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/widget/NotifsFabMenuView.kt @@ -22,15 +22,15 @@ import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.view.isVisible import com.google.android.material.floatingactionbutton.FloatingActionButton import im.vector.app.R -import kotlinx.android.synthetic.main.motion_fab_menu_merge.view.* +import kotlinx.android.synthetic.main.motion_notifs_fab_menu_merge.view.* -class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) { +class NotifsFabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) { var listener: Listener? = null init { - inflate(context, R.layout.motion_fab_menu_merge, this) + inflate(context, R.layout.motion_notifs_fab_menu_merge, this) } override fun onFinishInflate() { diff --git a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt index f6c60f6a6d..fe7a8006e0 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt @@ -28,7 +28,7 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault -import im.vector.app.features.userdirectory.KnownUsersFragment +import im.vector.app.features.userdirectory.UserListFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -50,7 +50,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor( companion object : MvRxViewModelFactory { @JvmStatic override fun create(viewModelContext: ViewModelContext, state: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel? { - val fragment: KnownUsersFragment = (viewModelContext as FragmentViewModelContext).fragment() + val fragment: UserListFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.homeServerCapabilitiesViewModelFactory.create(state) } diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index fa644f41b6..d3b3be6a3e 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -17,21 +17,23 @@ package im.vector.app.features.html import android.content.Context -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.glide.GlideApp +import android.text.Spannable +import androidx.core.text.toSpannable import im.vector.app.core.resources.ColorProvider -import im.vector.app.features.home.AvatarRenderer import io.noties.markwon.Markwon import io.noties.markwon.html.HtmlPlugin -import io.noties.markwon.html.TagHandlerNoOp import org.commonmark.node.Node import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class EventHtmlRenderer @Inject constructor(context: Context, - htmlConfigure: MatrixHtmlPluginConfigure) { +class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfigure, + context: Context) { + + interface PostProcessor { + fun afterRender(renderedText: Spannable) + } private val markwon = Markwon.builder(context) .usePlugin(HtmlPlugin.create(htmlConfigure)) @@ -41,35 +43,47 @@ class EventHtmlRenderer @Inject constructor(context: Context, return markwon.parse(text) } - fun render(text: String): CharSequence { + /** + * @param text the text you want to render + * @param postProcessors an optional array of post processor to add any span if needed + */ + fun render(text: String, vararg postProcessors: PostProcessor): CharSequence { return try { - markwon.toMarkdown(text) + val parsed = markwon.parse(text) + renderAndProcess(parsed, postProcessors) } catch (failure: Throwable) { Timber.v("Fail to render $text to html") text } } - fun render(node: Node): CharSequence? { + /** + * @param node the node you want to render + * @param postProcessors an optional array of post processor to add any span if needed + */ + fun render(node: Node, vararg postProcessors: PostProcessor): CharSequence? { return try { - markwon.render(node) + renderAndProcess(node, postProcessors) } catch (failure: Throwable) { Timber.v("Fail to render $node to html") return null } } + + private fun renderAndProcess(node: Node, postProcessors: Array): CharSequence { + val renderedText = markwon.render(node).toSpannable() + postProcessors.forEach { + it.afterRender(renderedText) + } + return renderedText + } } -class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context, - private val colorProvider: ColorProvider, - private val avatarRenderer: AvatarRenderer, - private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure { +class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure { override fun configureHtml(plugin: HtmlPlugin) { plugin - .addHandler(TagHandlerNoOp.create("a")) .addHandler(FontTagHandler()) - .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session)) .addHandler(MxReplyTagHandler()) .addHandler(SpanHandler(colorProvider)) } diff --git a/vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt deleted file mode 100644 index 368fdd27ff..0000000000 --- a/vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt +++ /dev/null @@ -1,89 +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.html - -import android.content.Context -import android.text.style.URLSpan -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.glide.GlideRequests -import im.vector.app.features.home.AvatarRenderer -import io.noties.markwon.MarkwonVisitor -import io.noties.markwon.SpannableBuilder -import io.noties.markwon.html.HtmlTag -import io.noties.markwon.html.MarkwonHtmlRenderer -import io.noties.markwon.html.tag.LinkHandler -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.room.model.RoomSummary -import org.matrix.android.sdk.api.util.MatrixItem - -class MxLinkTagHandler(private val glideRequests: GlideRequests, - private val context: Context, - private val avatarRenderer: AvatarRenderer, - private val sessionHolder: ActiveSessionHolder) : LinkHandler() { - - override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - val link = tag.attributes()["href"] - if (link != null) { - val permalinkData = PermalinkParser.parse(link) - val matrixItem = when (permalinkData) { - is PermalinkData.UserLink -> { - val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) - } - is PermalinkData.RoomLink -> { - if (permalinkData.eventId == null) { - val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) - if (permalinkData.isRoomAlias) { - MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) - } else { - MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) - } - } else { - // Exclude event link (used in reply events, we do not want to pill the "in reply to") - null - } - } - is PermalinkData.GroupLink -> { - val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) - MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) - } - else -> null - } - - if (matrixItem == null) { - super.handle(visitor, renderer, tag) - } else { - val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem) - SpannableBuilder.setSpans( - visitor.builder(), - span, - tag.start(), - tag.end() - ) - SpannableBuilder.setSpans( - visitor.builder(), - URLSpan(link), - tag.start(), - tag.end() - ) - } - } else { - super.handle(visitor, renderer, tag) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt new file mode 100644 index 0000000000..c13f5fdfb3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -0,0 +1,91 @@ +/* + * 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.html + +import android.content.Context +import android.text.Spannable +import android.text.Spanned +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.glide.GlideApp +import im.vector.app.features.home.AvatarRenderer +import io.noties.markwon.core.spans.LinkSpan +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.room.model.RoomSummary +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem + +class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomId: String?, + private val context: Context, + private val avatarRenderer: AvatarRenderer, + private val sessionHolder: ActiveSessionHolder) + : EventHtmlRenderer.PostProcessor { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String?): PillsPostProcessor + } + + override fun afterRender(renderedText: Spannable) { + addPillSpans(renderedText, roomId) + } + + private fun addPillSpans(renderedText: Spannable, roomId: String?) { + // We let markdown handle links and then we add PillImageSpan if needed. + val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java) + linkSpans.forEach { linkSpan -> + val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach + val startSpan = renderedText.getSpanStart(linkSpan) + val endSpan = renderedText.getSpanEnd(linkSpan) + renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? { + val permalinkData = PermalinkParser.parse(url) + val matrixItem = when (permalinkData) { + is PermalinkData.UserLink -> { + if (roomId == null) { + sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem() + } else { + sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem() + } + } + is PermalinkData.RoomLink -> { + if (permalinkData.eventId == null) { + val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) + if (permalinkData.isRoomAlias) { + MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } else { + MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } + } else { + // Exclude event link (used in reply events, we do not want to pill the "in reply to") + null + } + } + is PermalinkData.GroupLink -> { + val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) + MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) + } + else -> null + } ?: return null + return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) + } +} diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 087b7c2f55..65f547a662 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.View +import android.widget.Toast import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel @@ -29,7 +30,6 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH @@ -39,12 +39,12 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.toast import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel -import im.vector.app.features.userdirectory.KnownUsersFragment -import im.vector.app.features.userdirectory.KnownUsersFragmentArgs -import im.vector.app.features.userdirectory.UserDirectoryFragment -import im.vector.app.features.userdirectory.UserDirectorySharedAction -import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.app.features.userdirectory.UserDirectoryViewModel +import im.vector.app.features.userdirectory.UserListFragment +import im.vector.app.features.userdirectory.UserListFragmentArgs +import im.vector.app.features.userdirectory.UserListSharedAction +import im.vector.app.features.userdirectory.UserListSharedActionViewModel +import im.vector.app.features.userdirectory.UserListViewModel +import im.vector.app.features.userdirectory.UserListViewState import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity.* import org.matrix.android.sdk.api.failure.Failure @@ -54,11 +54,11 @@ import javax.inject.Inject @Parcelize data class InviteUsersToRoomArgs(val roomId: String) : Parcelable -class InviteUsersToRoomActivity : SimpleFragmentActivity() { +class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory { private val viewModel: InviteUsersToRoomViewModel by viewModel() - private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel - @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory + private lateinit var sharedActionViewModel: UserListSharedActionViewModel + @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter @@ -68,32 +68,40 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { injector.inject(this) } + override fun create(initialState: UserListViewState): UserListViewModel { + return userListViewModelFactory.create(initialState) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + + sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - UserDirectorySharedAction.OpenUsersDirectory -> - addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) - UserDirectorySharedAction.Close -> finish() - UserDirectorySharedAction.GoBack -> onBackPressed() - is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) - UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook() - }.exhaustive + UserListSharedAction.Close -> finish() + UserListSharedAction.GoBack -> onBackPressed() + is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) + UserListSharedAction.OpenPhoneBook -> openPhoneBook() + // not exhaustive because it's a sharedAction + else -> { + } + } } .disposeOnDestroy() if (isFirstCreation()) { + val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG) addFragment( R.id.container, - KnownUsersFragment::class.java, - KnownUsersFragmentArgs( + UserListFragment::class.java, + UserListFragmentArgs( title = getString(R.string.invite_users_to_room_title), menuResId = R.menu.vector_invite_users_to_room, - excludedUserIds = viewModel.getUserIdsOfRoomMembers() + excludedUserIds = viewModel.getUserIdsOfRoomMembers(), + existingRoomId = args?.roomId ) ) } @@ -101,6 +109,12 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { viewModel.observeViewEvents { renderInviteEvents(it) } } + private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { + if (action.itemId == R.id.action_invite_users_to_room_invite) { + viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees)) + } + } + private fun openPhoneBook() { // Check permission first if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, @@ -117,12 +131,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } } - } - } - - private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { - if (action.itemId == R.id.action_invite_users_to_room_invite) { - viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees)) + } else { + Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show() } } diff --git a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt index 881c446eb2..5e8c5b3cca 100644 --- a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt +++ b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt @@ -27,7 +27,7 @@ import im.vector.app.core.platform.ButtonStateView import im.vector.app.features.home.AvatarRenderer import kotlinx.android.synthetic.main.vector_invite_view.view.* import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState -import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -73,7 +73,7 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib } } - fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) { + fun render(sender: RoomMemberSummary, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) { if (mode == Mode.LARGE) { updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT } avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView) diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt index c6658925af..e3c1aa7b12 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt @@ -69,6 +69,11 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { } override fun showFailure(throwable: Throwable) { + // Only the resumed Fragment can eventually show the error, to avoid multiple dialog display + if (!isResumed) { + return + } + when (throwable) { is Failure.Cancelled -> /* Ignore this error, user has cancelled the action */ diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 81d6a78123..1f47916538 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -207,7 +207,6 @@ class LoginViewModel @AssistedInject constructor( private fun handleCheckIfEmailHasBeenValidated(action: LoginAction.CheckIfEmailHasBeenValidated) { // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state currentTask?.cancel() - currentTask = null currentTask = registrationWizard?.checkIfEmailHasBeenValidated(action.delayMillis, registrationCallback) } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt new file mode 100644 index 0000000000..e1c6800494 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt @@ -0,0 +1,24 @@ +/* + * 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.matrixto + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.util.MatrixItem + +sealed class MatrixToAction : VectorViewModelAction { + data class StartChattingWithUser(val matrixItem: MatrixItem) : MatrixToAction() +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt new file mode 100644 index 0000000000..69f30bb470 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -0,0 +1,145 @@ +/* + * 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.matrixto + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.features.home.AvatarRenderer +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.* +import javax.inject.Inject + +class MatrixToBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class MatrixToArgs( + val matrixToLink: String + ) : Parcelable + + @Inject lateinit var avatarRenderer: AvatarRenderer + + @Inject + lateinit var matrixToBottomSheetViewModelFactory: MatrixToBottomSheetViewModel.Factory + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + private var interactionListener: InteractionListener? = null + + override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card + + private val viewModel by fragmentViewModel(MatrixToBottomSheetViewModel::class) + + interface InteractionListener { + fun navigateToRoom(roomId: String) + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + when (val item = state.matrixItem) { + Uninitialized -> { + matrixToCardContentLoading.isVisible = false + matrixToCardUserContentVisibility.isVisible = false + } + is Loading -> { + matrixToCardContentLoading.isVisible = true + matrixToCardUserContentVisibility.isVisible = false + } + is Success -> { + matrixToCardContentLoading.isVisible = false + matrixToCardUserContentVisibility.isVisible = true + matrixToCardNameText.setTextOrHide(item.invoke().displayName) + matrixToCardUserIdText.setTextOrHide(item.invoke().id) + avatarRenderer.render(item.invoke(), matrixToCardAvatar) + } + is Fail -> { + // TODO display some error copy? + dismiss() + } + } + + when (state.startChattingState) { + Uninitialized -> { + matrixToCardButtonLoading.isVisible = false + matrixToCardSendMessageButton.isVisible = false + } + is Success -> { + matrixToCardButtonLoading.isVisible = false + matrixToCardSendMessageButton.isVisible = true + } + is Fail -> { + matrixToCardButtonLoading.isVisible = false + matrixToCardSendMessageButton.isVisible = true + // TODO display some error copy? + dismiss() + } + is Loading -> { + matrixToCardButtonLoading.isVisible = true + matrixToCardSendMessageButton.isInvisible = true + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + matrixToCardSendMessageButton.debouncedClicks { + withState(viewModel) { + it.matrixItem.invoke()?.let { item -> + viewModel.handle(MatrixToAction.StartChattingWithUser(item)) + } + } + } + + viewModel.observeViewEvents { + when (it) { + is MatrixToViewEvents.NavigateToRoom -> { + interactionListener?.navigateToRoom(it.roomId) + dismiss() + } + MatrixToViewEvents.Dismiss -> dismiss() + } + } + } + + companion object { + fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet { + return MatrixToBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, MatrixToBottomSheet.MatrixToArgs( + matrixToLink = matrixToLink + )) + } + interactionListener = listener + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt new file mode 100644 index 0000000000..9b1ce9fea8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -0,0 +1,33 @@ +/* + * 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.matrixto + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.util.MatrixItem + +data class MatrixToBottomSheetState( + val deepLink: String, + val matrixItem: Async = Uninitialized, + val startChattingState: Async = Uninitialized +) : MvRxState { + + constructor(args: MatrixToBottomSheet.MatrixToArgs) : this( + deepLink = args.matrixToLink + ) +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt new file mode 100644 index 0000000000..6e8a530c9a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -0,0 +1,166 @@ +/* + * 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.matrixto + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isE2EByDefault +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +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.room.model.create.CreateRoomParams +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 MatrixToBottomSheetViewModel @AssistedInject constructor( + @Assisted initialState: MatrixToBottomSheetState, + private val session: Session, + private val stringProvider: StringProvider, + private val rawService: RawService) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel + } + + init { + setState { + copy(matrixItem = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + resolveLink(initialState) + } + } + + private suspend fun resolveLink(initialState: MatrixToBottomSheetState) { + val permalinkData = PermalinkParser.parse(initialState.deepLink) + if (permalinkData is PermalinkData.FallbackLink) { + setState { + copy( + matrixItem = Fail(IllegalArgumentException(stringProvider.getString(R.string.permalink_malformed))), + startChattingState = Uninitialized + ) + } + return + } + + when (permalinkData) { + is PermalinkData.UserLink -> { + val user = resolveUser(permalinkData.userId) + setState { + copy( + matrixItem = Success(user.toMatrixItem()), + startChattingState = Success(Unit) + ) + } + } + is PermalinkData.RoomLink -> { + // not yet supported + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + is PermalinkData.GroupLink -> { + // not yet supported + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + is PermalinkData.FallbackLink -> { + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + } + } + + private suspend fun resolveUser(userId: String): User { + return tryOrNull { + awaitCallback { + session.resolveUser(userId, it) + } + } + // Create raw user in case the user is not searchable + ?: User(userId, null, null) + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: MatrixToBottomSheetState): MatrixToBottomSheetViewModel? { + val fragment: MatrixToBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + + return fragment.matrixToBottomSheetViewModelFactory.create(state) + } + } + + override fun handle(action: MatrixToAction) { + when (action) { + is MatrixToAction.StartChattingWithUser -> handleStartChatting(action) + }.exhaustive + } + + private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) { + val mxId = action.matrixItem.id + val existing = session.getExistingDirectRoomWithUser(mxId) + if (existing != null) { + // navigate to this room + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing)) + } else { + setState { + copy(startChattingState = Loading()) + } + // we should create the room then navigate + viewModelScope.launch(Dispatchers.IO) { + val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) + ?.isE2EByDefault() + ?: true + + val roomParams = CreateRoomParams() + .apply { + invitedUserIds.add(mxId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault + } + + val roomId = try { + awaitCallback { session.createRoom(roomParams, it) } + } catch (failure: Throwable) { + setState { + copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure)))) + } + return@launch + } + setState { + // we can hide this button has we will navigate out + copy(startChattingState = Uninitialized) + } + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt new file mode 100644 index 0000000000..f9491fd361 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.matrixto + +import im.vector.app.core.platform.VectorViewEvents + +sealed class MatrixToViewEvents : VectorViewEvents { + data class NavigateToRoom(val roomId: String) : MatrixToViewEvents() + object Dismiss : MatrixToViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt new file mode 100644 index 0000000000..b549e01551 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt @@ -0,0 +1,53 @@ +/* + * 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.media + +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.StringProvider +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.timeline.TimelineEvent +import javax.inject.Inject + +class AttachmentProviderFactory @Inject constructor( + private val imageContentRenderer: ImageContentRenderer, + private val vectorDateFormatter: VectorDateFormatter, + private val stringProvider: StringProvider, + private val session: Session +) { + + fun createProvider(attachments: List): RoomEventsAttachmentProvider { + return RoomEventsAttachmentProvider( + attachments, + imageContentRenderer, + vectorDateFormatter, + session.fileService(), + stringProvider + ) + } + + fun createProvider(attachments: List, room: Room?): DataAttachmentRoomProvider { + return DataAttachmentRoomProvider( + attachments, + room, + imageContentRenderer, + vectorDateFormatter, + session.fileService(), + stringProvider + ) + } +} 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 3846e56ecf..e23b905919 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 @@ -20,17 +20,30 @@ import android.content.Context import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView +import androidx.core.view.isVisible import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.transition.Transition +import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.StringProvider 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 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 import java.io.File -abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider { +abstract class BaseAttachmentProvider( + private val attachments: List, + private val imageContentRenderer: ImageContentRenderer, + protected val fileService: FileService, + private val dateFormatter: VectorDateFormatter, + private val stringProvider: StringProvider +) : AttachmentSourceProvider { interface InteractionListener { fun onDismissTapped() @@ -41,9 +54,13 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend var interactionListener: InteractionListener? = null - protected var overlayView: AttachmentOverlayView? = null + private var overlayView: AttachmentOverlayView? = null - override fun overlayViewAtPosition(context: Context, position: Int): View? { + final override fun getItemCount() = attachments.size + + protected fun getItem(position: Int) = attachments[position] + + final override fun overlayViewAtPosition(context: Context, position: Int): View? { if (position == -1) return null if (overlayView == null) { overlayView = AttachmentOverlayView(context) @@ -60,9 +77,24 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend interactionListener?.videoSeekTo(percent) } } + + val timelineEvent = getTimelineEventAtPosition(position) + if (timelineEvent != null) { + val dateString = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) + overlayView?.updateWith( + counter = stringProvider.getString(R.string.attachment_viewer_item_x_of_y, position + 1, getItemCount()), + senderInfo = "${timelineEvent.senderInfo.displayName} $dateString" + ) + overlayView?.videoControlsGroup?.isVisible = timelineEvent.root.isVideoMessage() + } else { + overlayView?.updateWith("", "") + } + return overlayView } + abstract fun getTimelineEventAtPosition(position: Int): TimelineEvent? + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { (info.data as? ImageContentRenderer.Data)?.let { imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { 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 085153a721..18312b4aa0 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 @@ -16,30 +16,26 @@ package im.vector.app.features.media -import android.content.Context -import android.view.View -import androidx.core.view.isVisible -import im.vector.app.core.date.DateFormatKind 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 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.Room +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import java.io.File class DataAttachmentRoomProvider( - private val attachments: List, + attachments: List, private val room: Room?, - private val initialIndex: Int, imageContentRenderer: ImageContentRenderer, - private val dateFormatter: VectorDateFormatter, - fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) { - - override fun getItemCount(): Int = attachments.size + dateFormatter: VectorDateFormatter, + fileService: FileService, + stringProvider: StringProvider +) : BaseAttachmentProvider(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) { override fun getAttachmentInfoAt(position: Int): AttachmentInfo { - return attachments[position].let { + return getItem(position).let { when (it) { is ImageContentRenderer.Data -> { if (it.mimeType == "image/gif") { @@ -73,22 +69,13 @@ class DataAttachmentRoomProvider( } } - override fun overlayViewAtPosition(context: Context, position: Int): View? { - super.overlayViewAtPosition(context, position) - val item = attachments[position] - val timeLineEvent = room?.getTimeLineEvent(item.eventId) - if (timeLineEvent != null) { - val dateString = dateFormatter.format(timeLineEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString") - overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage() - } else { - overlayView?.updateWith("", "") - } - return overlayView + override fun getTimelineEventAtPosition(position: Int): TimelineEvent? { + val item = getItem(position) + return room?.getTimeLineEvent(item.eventId) } override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { - val item = attachments[position] + val item = getItem(position) fileService.downloadFile( downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, id = item.eventId, diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 4f1c52b240..187c2e85c3 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.os.Parcelable import android.view.View import android.widget.ImageView +import androidx.core.view.updateLayoutParams import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -96,15 +97,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: fun render(data: Data, mode: Mode, imageView: ImageView) { val size = processSize(data, mode) - imageView.layoutParams.width = size.width - imageView.layoutParams.height = size.height + imageView.updateLayoutParams { + width = size.width + height = size.height + } // a11y imageView.contentDescription = data.filename createGlideRequest(data, mode, imageView, size) .dontAnimate() .transform(RoundedCorners(dimensionConverter.dpToPx(8))) - .thumbnail(0.3f) + // .thumbnail(0.3f) .into(imageView) } @@ -117,6 +120,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: } } + /** + * Used by Attachment Viewer + */ fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) { val req = if (data.elementToDecrypt != null) { // Encrypted image 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 5c0c33d078..1e2761dde0 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 @@ -16,18 +16,12 @@ package im.vector.app.features.media -import android.content.Context -import android.view.View -import androidx.core.view.isVisible -import im.vector.app.core.date.DateFormatKind 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 org.matrix.android.sdk.api.session.Session -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.file.FileService -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent @@ -36,22 +30,17 @@ 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 import java.io.File -import javax.inject.Inject class RoomEventsAttachmentProvider( - private val attachments: List, - private val initialIndex: Int, + attachments: List, imageContentRenderer: ImageContentRenderer, - private val dateFormatter: VectorDateFormatter, - fileService: FileService -) : BaseAttachmentProvider(imageContentRenderer, fileService) { - - override fun getItemCount(): Int { - return attachments.size - } + dateFormatter: VectorDateFormatter, + fileService: FileService, + stringProvider: StringProvider +) : BaseAttachmentProvider(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) { override fun getAttachmentInfoAt(position: Int): AttachmentInfo { - return attachments[position].let { + return getItem(position).let { val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent if (content is MessageImageContent) { val data = ImageContentRenderer.Data( @@ -125,17 +114,12 @@ class RoomEventsAttachmentProvider( } } - override fun overlayViewAtPosition(context: Context, position: Int): View? { - super.overlayViewAtPosition(context, position) - val item = attachments[position] - val dateString = dateFormatter.format(item.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString") - overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage() - return overlayView + override fun getTimelineEventAtPosition(position: Int): TimelineEvent? { + return getItem(position) } override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { - attachments[position].let { timelineEvent -> + getItem(position).let { timelineEvent -> val messageContent = timelineEvent.root.getClearContent().toModel() as? MessageWithAttachmentContent @@ -160,18 +144,3 @@ class RoomEventsAttachmentProvider( } } } - -class AttachmentProviderFactory @Inject constructor( - private val imageContentRenderer: ImageContentRenderer, - private val vectorDateFormatter: VectorDateFormatter, - private val session: Session -) { - - fun createProvider(attachments: List, initialIndex: Int): RoomEventsAttachmentProvider { - return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService()) - } - - fun createProvider(attachments: List, room: Room?, initialIndex: Int): DataAttachmentRoomProvider { - return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService()) - } -} diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index 9302be502d..e7f4806e31 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -70,7 +70,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen private var initialIndex = 0 private var isAnimatingOut = false - var currentSourceProvider: BaseAttachmentProvider? = null + private var currentSourceProvider: BaseAttachmentProvider<*>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -117,36 +117,22 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen val room = args.roomId?.let { session.getRoom(it) } val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA) - if (inMemoryData != null) { - val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex) - val index = inMemoryData.indexOfFirst { it.eventId == args.eventId } - initialIndex = index - sourceProvider.interactionListener = this - setSourceProvider(sourceProvider) - this.currentSourceProvider = sourceProvider - if (savedInstanceState == null) { - pager2.setCurrentItem(index, false) - // The page change listener is not notified of the change... - pager2.post { - onSelectedPositionChanged(index) - } - } + val sourceProvider = if (inMemoryData != null) { + initialIndex = inMemoryData.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0) + dataSourceFactory.createProvider(inMemoryData, room) } else { - val events = room?.getAttachmentMessages() - ?: emptyList() - val index = events.indexOfFirst { it.eventId == args.eventId } - initialIndex = index - - val sourceProvider = dataSourceFactory.createProvider(events, index) - sourceProvider.interactionListener = this - setSourceProvider(sourceProvider) - this.currentSourceProvider = sourceProvider - if (savedInstanceState == null) { - pager2.setCurrentItem(index, false) - // The page change listener is not notified of the change... - pager2.post { - onSelectedPositionChanged(index) - } + val events = room?.getAttachmentMessages().orEmpty() + initialIndex = events.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0) + dataSourceFactory.createProvider(events) + } + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) + currentSourceProvider = sourceProvider + if (savedInstanceState == null) { + pager2.setCurrentItem(initialIndex, false) + // The page change listener is not notified of the change... + pager2.post { + onSelectedPositionChanged(initialIndex) } } @@ -278,7 +264,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen } override fun onShareTapped() { - this.currentSourceProvider?.getFileForSharing(currentPosition) { data -> + currentSourceProvider?.getFileForSharing(currentPosition) { data -> if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 106d804cd3..2d0ca86d52 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor( context.startActivity(KeysBackupManageActivity.intent(context)) } - override fun openRoomProfile(context: Context, roomId: String) { - context.startActivity(RoomProfileActivity.newIntent(context, roomId)) + override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) { + context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess)) } override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) { diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 1d01a5e4f0..504fccb63a 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -78,7 +78,7 @@ interface Navigator { fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false) - fun openRoomProfile(context: Context, roomId: String) + fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null) fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) 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 0740295191..9c2dc9b26d 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 @@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? { val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null - val dName = event.senderId?.let { session.getUser(it)?.displayName } + val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } if (Membership.INVITE == content.membership) { val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId)) ?: stringProvider.getString(R.string.notification_new_invitation) 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 9cfed991bb..d79d16a052 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 @@ -120,7 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { null, false, System.currentTimeMillis(), - session.getUser(session.myUserId)?.displayName + session.getRoomMember(session.myUserId, room.roomId)?.displayName ?: context?.getString(R.string.notification_sender_me), session.myUserId, message, diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index 11c55f6a73..f1149d8990 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -63,13 +63,14 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .flatMap { permalinkData -> - handlePermalink(permalinkData, context, navigationInterceptor, buildTask) + handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask) } .onErrorReturnItem(false) } private fun handlePermalink( permalinkData: PermalinkData, + rawLink: Uri, context: Context, navigationInterceptor: NavigationInterceptor?, buildTask: Boolean @@ -96,7 +97,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti Single.just(true) } is PermalinkData.UserLink -> { - if (navigationInterceptor?.navToMemberProfile(permalinkData.userId) != true) { + if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) { navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask) } Single.just(true) @@ -175,7 +176,7 @@ interface NavigationInterceptor { /** * Return true if the navigation has been intercepted */ - fun navToMemberProfile(userId: String): Boolean { + fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { return false } } diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt index e005dd06c5..e8064aaec5 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt @@ -23,11 +23,9 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.utils.toast +import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.LoadingFragment import im.vector.app.features.login.LoginActivity -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit import javax.inject.Inject class PermalinkHandlerActivity : VectorBaseActivity() { @@ -45,23 +43,28 @@ class PermalinkHandlerActivity : VectorBaseActivity() { if (isFirstCreation()) { replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java) } + handleIntent() + } + + private fun handleIntent() { // If we are not logged in, open login screen. // In the future, we might want to relaunch the process after login. if (!sessionHolder.hasActiveSession()) { startLoginActivity() return } - val uri = intent.dataString - permalinkHandler.launch(this, uri, buildTask = true) - .delay(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { isHandled -> - if (!isHandled) { - toast(R.string.permalink_malformed) - } - finish() - } - .disposeOnDestroy() + // We forward intent to HomeActivity (singleTask) to avoid the dueling app problem + // https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances + intent.setClass(this, HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + startActivity(intent) + + finish() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent() } private fun startLoginActivity() { diff --git a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt index b6e238c2dc..1aa4846f38 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt @@ -56,6 +56,7 @@ class PinFragment @Inject constructor( when (fragmentArgs.pinMode) { PinMode.CREATE -> showCreateFragment() PinMode.AUTH -> showAuthFragment() + PinMode.MODIFY -> showCreateFragment() // No need to create another function for now because texts are generic } } @@ -73,6 +74,10 @@ class PinFragment @Inject constructor( Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show() } + override fun onPinCodeEnteredFirst(pinCode: String?): Boolean { + return false + } + override fun onCodeCreated(encodedCode: String) { lifecycleScope.launch { pinCodeStore.storeEncodedPin(encodedCode) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinMode.kt b/vector/src/main/java/im/vector/app/features/pin/PinMode.kt index c24ac5adf2..9801912bd6 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinMode.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinMode.kt @@ -18,5 +18,6 @@ package im.vector.app.features.pin enum class PinMode { CREATE, - AUTH + AUTH, + MODIFY } 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 665eb93428..b2257b250a 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 @@ -46,7 +46,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy? = null private var currentAlerter: VectorAlert? = null - private val alertFiFo = ArrayList() + private val alertFiFo = mutableListOf() fun postVectorAlert(alert: VectorAlert) { synchronized(alertFiFo) { diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt index 119be66f94..c1118e40cb 100644 --- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt +++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt @@ -18,10 +18,9 @@ package im.vector.app.features.raw.wellknown import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.internal.util.awaitCallback suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? { - return tryOrNull { awaitCallback { getWellknown(userId, it) } } + return tryOrNull { getWellknown(userId) } ?.let { ElementWellKnownMapper.from(it) } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt index 685f0dd64e..28df628cf1 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt @@ -41,12 +41,12 @@ class EmojiSearchResultFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) epoxyController.listener = this - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) } override fun onDestroyView() { epoxyController.listener = null - recyclerView.cleanup() + genericRecyclerView.cleanup() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index 9dc41cbc21..a21f4e670a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -26,18 +26,15 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.features.roomdirectory.createroom.CreateRoomAction import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment -import im.vector.app.features.roomdirectory.createroom.CreateRoomViewModel +import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment import javax.inject.Inject class RoomDirectoryActivity : VectorBaseActivity() { - @Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory @Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel() - private val createRoomViewModel: CreateRoomViewModel by viewModel() private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel override fun getLayoutRes() = R.layout.activity_simple @@ -60,10 +57,13 @@ class RoomDirectoryActivity : VectorBaseActivity() { when (sharedAction) { is RoomDirectorySharedAction.Back -> onBackPressed() is RoomDirectorySharedAction.CreateRoom -> { - addFragmentToBackstack(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) - // Transmit the filter to the createRoomViewModel + // Transmit the filter to the CreateRoomFragment withState(roomDirectoryViewModel) { - createRoomViewModel.handle(CreateRoomAction.SetName(it.currentFilter)) + addFragmentToBackstack( + R.id.simpleFragmentContainer, + CreateRoomFragment::class.java, + CreateRoomArgs(it.currentFilter) + ) } } is RoomDirectorySharedAction.ChangeProtocol -> 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 42b17b4dad..c58e255bcc 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 @@ -204,9 +204,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: Timber.w("Try to join an already joining room. Should not happen") return@withState } - val viaServers = state.roomDirectoryData.homeServer?.let { - listOf(it) - } ?: emptyList() + val viaServers = state.roomDirectoryData.homeServer + ?.let { listOf(it) } + .orEmpty() session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt index 4b3eacffaa..b50c56e1db 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt @@ -24,9 +24,12 @@ sealed class CreateRoomAction : VectorViewModelAction { data class SetName(val name: String) : CreateRoomAction() data class SetTopic(val topic: String) : CreateRoomAction() data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction() - data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction() + data class SetRoomAliasLocalPart(val aliasLocalPart: String) : CreateRoomAction() data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction() + object ToggleShowAdvanced : CreateRoomAction() + data class DisableFederation(val disableFederation: Boolean) : CreateRoomAction() + object Create : CreateRoomAction() object Reset : CreateRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt index e003c4905c..b6ee00a52f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -20,7 +20,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.widget.Toolbar -import com.airbnb.mvrx.viewModel import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment @@ -28,16 +27,12 @@ import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel -import javax.inject.Inject /** * Simple container for [CreateRoomFragment] */ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { - @Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory - private val createRoomViewModel: CreateRoomViewModel by viewModel() - private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel override fun getLayoutRes() = R.layout.activity_simple @@ -52,8 +47,11 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { override fun initUiAndData() { if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) - createRoomViewModel.handle(CreateRoomAction.SetName(intent?.getStringExtra(INITIAL_NAME) ?: "")) + addFragment( + R.id.simpleFragmentContainer, + CreateRoomFragment::class.java, + CreateRoomArgs(intent?.getStringExtra(INITIAL_NAME) ?: "") + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index d1cc884336..94b419797d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -19,22 +19,20 @@ package im.vector.app.features.roomdirectory.createroom import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.errorWithRetryItem -import im.vector.app.core.epoxy.loadingItem -import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.discovery.settingsSectionTitleItem +import im.vector.app.features.form.formAdvancedToggleItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formSubmitButtonItem import im.vector.app.features.form.formSwitchItem +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import javax.inject.Inject -class CreateRoomController @Inject constructor(private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter +class CreateRoomController @Inject constructor( + private val stringProvider: StringProvider, + private val roomAliasErrorFormatter: RoomAliasErrorFormatter ) : TypedEpoxyController() { var listener: Listener? = null @@ -42,31 +40,8 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin var index = 0 override fun buildModels(viewState: CreateRoomViewState) { - when (val asyncCreateRoom = viewState.asyncCreateRoomRequest) { - is Success -> { - // Nothing to display, the screen will be closed - } - is Loading -> { - // display the form - buildForm(viewState, false) - loadingItem { - id("loading") - } - } - is Uninitialized -> { - // display the form - buildForm(viewState, true) - } - is Fail -> { - // display the form - buildForm(viewState, true) - errorWithRetryItem { - id("error") - text(errorFormatter.toHumanReadable(asyncCreateRoom.error)) - listener { listener?.retry() } - } - } - } + // display the form + buildForm(viewState, viewState.asyncCreateRoomRequest !is Loading) } private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) { @@ -114,38 +89,63 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin enabled(enableFormElement) title(stringProvider.getString(R.string.create_room_public_title)) summary(stringProvider.getString(R.string.create_room_public_description)) - switchChecked(viewState.isPublic) + switchChecked(viewState.roomType is CreateRoomViewState.RoomType.Public) + showDivider(viewState.roomType !is CreateRoomViewState.RoomType.Public) listener { value -> listener?.setIsPublic(value) } } - formSwitchItem { - id("directory") - enabled(enableFormElement) - title(stringProvider.getString(R.string.create_room_directory_title)) - summary(stringProvider.getString(R.string.create_room_directory_description)) - switchChecked(viewState.isInRoomDirectory) + if (viewState.roomType is CreateRoomViewState.RoomType.Public) { + // Room alias for public room + roomAliasEditItem { + id("alias") + enabled(enableFormElement) + value(viewState.roomType.aliasLocalPart) + homeServer(":" + viewState.homeServerName) + errorMessage( + roomAliasErrorFormatter.format( + (((viewState.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) + ) + onTextChange { value -> + listener?.setAliasLocalPart(value) + } + } + } else { + // Room encryption for private room + formSwitchItem { + id("encryption") + enabled(enableFormElement) + title(stringProvider.getString(R.string.create_room_encryption_title)) + summary( + if (viewState.hsAdminHasDisabledE2E) { + stringProvider.getString(R.string.settings_hs_admin_e2e_disabled) + } else { + stringProvider.getString(R.string.create_room_encryption_description) + } + ) + switchChecked(viewState.isEncrypted) - listener { value -> - listener?.setIsInRoomDirectory(value) + listener { value -> + listener?.setIsEncrypted(value) + } } } - formSwitchItem { - id("encryption") - enabled(enableFormElement) - title(stringProvider.getString(R.string.create_room_encryption_title)) - summary( - if (viewState.hsAdminHasDisabledE2E) { - stringProvider.getString(R.string.settings_hs_admin_e2e_disabled) - } else { - stringProvider.getString(R.string.create_room_encryption_description) - } - ) - switchChecked(viewState.isEncrypted) - - listener { value -> - listener?.setIsEncrypted(value) + formAdvancedToggleItem { + id("showAdvanced") + title(stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced)) + expanded(!viewState.showAdvanced) + listener { listener?.toggleShowAdvanced() } + } + if (viewState.showAdvanced) { + formSwitchItem { + id("federation") + enabled(enableFormElement) + title(stringProvider.getString(R.string.create_room_disable_federation_title, viewState.homeServerName)) + summary(stringProvider.getString(R.string.create_room_disable_federation_description)) + switchChecked(viewState.disableFederation) + showDivider(false) + listener { value -> listener?.setDisableFederation(value) } } } formSubmitButtonItem { @@ -162,9 +162,10 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin fun onNameChange(newName: String) fun onTopicChange(newTopic: String) fun setIsPublic(isPublic: Boolean) - fun setIsInRoomDirectory(isInRoomDirectory: Boolean) + fun setAliasLocalPart(aliasLocalPart: String) fun setIsEncrypted(isEncrypted: Boolean) - fun retry() + fun toggleShowAdvanced() + fun setDisableFederation(disableFederation: Boolean) fun submit() } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 88b8a65a1c..204a99929b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -18,10 +18,14 @@ package im.vector.app.features.roomdirectory.createroom import android.net.Uri import android.os.Bundle +import android.os.Parcelable import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success -import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper @@ -33,12 +37,20 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel +import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_create_room.* -import timber.log.Timber +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import javax.inject.Inject +@Parcelize +data class CreateRoomArgs( + val initialName: String +) : Parcelable + class CreateRoomFragment @Inject constructor( private val createRoomController: CreateRoomController, + val createRoomViewModelFactory: CreateRoomViewModel.Factory, colorProvider: ColorProvider ) : VectorBaseFragment(), CreateRoomController.Listener, @@ -46,7 +58,8 @@ class CreateRoomFragment @Inject constructor( OnBackPressed { private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel - private val viewModel: CreateRoomViewModel by activityViewModel() + private val viewModel: CreateRoomViewModel by fragmentViewModel() + private val args: CreateRoomArgs by args() private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) @@ -56,17 +69,31 @@ class CreateRoomFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) vectorBaseActivity.setSupportActionBar(createRoomToolbar) sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) + setupWaitingView() setupRecyclerView() createRoomClose.debouncedClicks { sharedActionViewModel.post(RoomDirectorySharedAction.Back) } viewModel.observeViewEvents { when (it) { - CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed() + CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed() + is CreateRoomViewEvents.Failure -> showFailure(it.throwable) }.exhaustive } } + override fun showFailure(throwable: Throwable) { + // Note: RoomAliasError are displayed directly in the form + if (throwable !is CreateRoomFailure.AliasError) { + super.showFailure(throwable) + } + } + + private fun setupWaitingView() { + waiting_view_status_text.isVisible = true + waiting_view_status_text.setText(R.string.create_room_in_progress) + } + override fun onDestroyView() { createRoomForm.cleanup() createRoomController.listener = null @@ -102,20 +129,23 @@ class CreateRoomFragment @Inject constructor( viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) } - override fun setIsInRoomDirectory(isInRoomDirectory: Boolean) { - viewModel.handle(CreateRoomAction.SetIsInRoomDirectory(isInRoomDirectory)) + override fun setAliasLocalPart(aliasLocalPart: String) { + viewModel.handle(CreateRoomAction.SetRoomAliasLocalPart(aliasLocalPart)) } override fun setIsEncrypted(isEncrypted: Boolean) { viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted)) } - override fun submit() { - viewModel.handle(CreateRoomAction.Create) + override fun toggleShowAdvanced() { + viewModel.handle(CreateRoomAction.ToggleShowAdvanced) } - override fun retry() { - Timber.v("Retry") + override fun setDisableFederation(disableFederation: Boolean) { + viewModel.handle(CreateRoomAction.DisableFederation(disableFederation)) + } + + override fun submit() { viewModel.handle(CreateRoomAction.Create) } @@ -139,6 +169,7 @@ class CreateRoomFragment @Inject constructor( override fun invalidate() = withState(viewModel) { state -> val async = state.asyncCreateRoomRequest + waiting_view.isVisible = async is Loading if (async is Success) { // Navigate to freshly created room navigator.openRoom(requireActivity(), async()) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt index 4ff4ee4bdf..af745ce5ff 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt @@ -22,5 +22,6 @@ import im.vector.app.core.platform.VectorViewEvents * Transient events for room creation screen */ sealed class CreateRoomViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : CreateRoomViewEvents() object Quit : CreateRoomViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index 57af95b107..216a016fbe 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -17,13 +17,13 @@ package im.vector.app.features.roomdirectory.createroom import androidx.core.net.toFile -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -31,7 +31,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault -import im.vector.app.features.roomdirectory.RoomDirectoryActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback @@ -53,9 +52,18 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr } init { + initHomeServerName() initAdminE2eByDefault() } + private fun initHomeServerName() { + setState { + copy( + homeServerName = session.myUserId.substringAfter(":") + ) + } + } + private var adminE2EByDefault = true private fun initAdminE2eByDefault() { @@ -68,7 +76,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr setState { copy( - isEncrypted = !isPublic && adminE2EByDefault, + isEncrypted = roomType is CreateRoomViewState.RoomType.Private && adminE2EByDefault, hsAdminHasDisabledE2E = !adminE2EByDefault ) } @@ -79,29 +87,43 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr @JvmStatic override fun create(viewModelContext: ViewModelContext, state: CreateRoomViewState): CreateRoomViewModel? { - val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity() + val fragment: CreateRoomFragment = (viewModelContext as FragmentViewModelContext).fragment() - return when (activity) { - is CreateRoomActivity -> activity.createRoomViewModelFactory.create(state) - is RoomDirectoryActivity -> activity.createRoomViewModelFactory.create(state) - else -> error("Wrong activity") - } + return fragment.createRoomViewModelFactory.create(state) } } override fun handle(action: CreateRoomAction) { when (action) { - is CreateRoomAction.SetAvatar -> setAvatar(action) - is CreateRoomAction.SetName -> setName(action) - is CreateRoomAction.SetTopic -> setTopic(action) - is CreateRoomAction.SetIsPublic -> setIsPublic(action) - is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action) - is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) - is CreateRoomAction.Create -> doCreateRoom() - CreateRoomAction.Reset -> doReset() + is CreateRoomAction.SetAvatar -> setAvatar(action) + is CreateRoomAction.SetName -> setName(action) + is CreateRoomAction.SetTopic -> setTopic(action) + is CreateRoomAction.SetIsPublic -> setIsPublic(action) + is CreateRoomAction.SetRoomAliasLocalPart -> setRoomAliasLocalPart(action) + is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) + is CreateRoomAction.Create -> doCreateRoom() + CreateRoomAction.Reset -> doReset() + CreateRoomAction.ToggleShowAdvanced -> toggleShowAdvanced() + is CreateRoomAction.DisableFederation -> disableFederation(action) }.exhaustive } + private fun disableFederation(action: CreateRoomAction.DisableFederation) { + setState { + copy(disableFederation = action.disableFederation) + } + } + + private fun toggleShowAdvanced() { + setState { + copy( + showAdvanced = !showAdvanced, + // Reset to false if advanced is hidden + disableFederation = disableFederation && !showAdvanced + ) + } + } + private fun doReset() { setState { // Delete temporary file with the avatar @@ -123,13 +145,35 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) } private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState { - copy( - isPublic = action.isPublic, - isEncrypted = !action.isPublic && adminE2EByDefault - ) + if (action.isPublic) { + copy( + roomType = CreateRoomViewState.RoomType.Public(""), + // Reset any error in the form about alias + asyncCreateRoomRequest = Uninitialized, + isEncrypted = false + ) + } else { + copy( + roomType = CreateRoomViewState.RoomType.Private, + isEncrypted = adminE2EByDefault + ) + } } - private fun setIsInRoomDirectory(action: CreateRoomAction.SetIsInRoomDirectory) = setState { copy(isInRoomDirectory = action.isInRoomDirectory) } + private fun setRoomAliasLocalPart(action: CreateRoomAction.SetRoomAliasLocalPart) { + withState { state -> + if (state.roomType is CreateRoomViewState.RoomType.Public) { + setState { + copy( + roomType = CreateRoomViewState.RoomType.Public(action.aliasLocalPart), + // Reset any error in the form about alias + asyncCreateRoomRequest = Uninitialized + ) + } + } + } + // Else ignore + } private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) } @@ -147,10 +191,23 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr name = state.roomName.takeIf { it.isNotBlank() } topic = state.roomTopic.takeIf { it.isNotBlank() } avatarUri = state.avatarUri - // Directory visibility - visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE - // Public room - preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT + when (state.roomType) { + is CreateRoomViewState.RoomType.Public -> { + // Directory visibility + visibility = RoomDirectoryVisibility.PUBLIC + // Preset + preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + roomAliasName = state.roomType.aliasLocalPart + } + is CreateRoomViewState.RoomType.Private -> { + // Directory visibility + visibility = RoomDirectoryVisibility.PRIVATE + // Preset + preset = CreateRoomPreset.PRESET_PRIVATE_CHAT + } + }.exhaustive + // Disabling federation + disableFederation = state.disableFederation // Encryption if (state.isEncrypted) { @@ -169,6 +226,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr setState { copy(asyncCreateRoomRequest = Fail(failure)) } + _viewEvents.post(CreateRoomViewEvents.Failure(failure)) } }) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index 433cc02cc9..4609693c8f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -20,20 +20,35 @@ import android.net.Uri import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.extensions.orTrue data class CreateRoomViewState( val avatarUri: Uri? = null, val roomName: String = "", val roomTopic: String = "", - val isPublic: Boolean = false, - val isInRoomDirectory: Boolean = false, + val roomType: RoomType = RoomType.Private, val isEncrypted: Boolean = false, + val showAdvanced: Boolean = false, + val disableFederation: Boolean = false, + val homeServerName: String = "", val hsAdminHasDisabledE2E: Boolean = false, val asyncCreateRoomRequest: Async = Uninitialized ) : MvRxState { + constructor(args: CreateRoomArgs) : this( + roomName = args.initialName + ) + /** * Return true if there is not important input from user */ - fun isEmpty() = avatarUri == null && roomName.isEmpty() && roomTopic.isEmpty() + fun isEmpty() = avatarUri == null + && roomName.isEmpty() + && roomTopic.isEmpty() + && (roomType as? RoomType.Public)?.aliasLocalPart?.isEmpty().orTrue() + + sealed class RoomType { + object Private : RoomType() + data class Public(val aliasLocalPart: String) : RoomType() + } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt new file mode 100644 index 0000000000..2a30545a47 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt @@ -0,0 +1,88 @@ +/* + * 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.roomdirectory.createroom + +import android.text.Editable +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe +import im.vector.app.core.platform.SimpleTextWatcher + +@EpoxyModelClass(layout = R.layout.item_room_alias_text_input) +abstract class RoomAliasEditItem : VectorEpoxyModel() { + + @EpoxyAttribute + var value: String? = null + + @EpoxyAttribute + var showBottomSeparator: Boolean = true + + @EpoxyAttribute + var errorMessage: String? = null + + @EpoxyAttribute + var homeServer: String? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var onTextChange: ((String) -> Unit)? = null + + private val onTextChangeListener = object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + onTextChange?.invoke(s.toString()) + } + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.textInputLayout.isEnabled = enabled + holder.textInputLayout.error = errorMessage + + // Update only if text is different and value is not null + holder.textInputEditText.setTextSafe(value) + holder.textInputEditText.isEnabled = enabled + holder.textInputEditText.addTextChangedListener(onTextChangeListener) + holder.homeServerText.text = homeServer + holder.bottomSeparator.isVisible = showBottomSeparator + } + + override fun shouldSaveViewState(): Boolean { + return false + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.textInputEditText.removeTextChangedListener(onTextChangeListener) + } + + class Holder : VectorEpoxyHolder() { + val textInputLayout by bind(R.id.itemRoomAliasTextInputLayout) + val textInputEditText by bind(R.id.itemRoomAliasTextInputEditText) + val homeServerText by bind(R.id.itemRoomAliasHomeServer) + val bottomSeparator by bind(R.id.itemRoomAliasDivider) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt new file mode 100644 index 0000000000..7a23a79ab3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt @@ -0,0 +1,36 @@ +/* + * 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.roomdirectory.createroom + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import javax.inject.Inject + +class RoomAliasErrorFormatter @Inject constructor( + private val stringProvider: StringProvider +) { + fun format(roomAliasError: RoomAliasError?): String? { + return when (roomAliasError) { + is RoomAliasError.AliasEmpty -> R.string.create_room_alias_empty + is RoomAliasError.AliasNotAvailable -> R.string.create_room_alias_already_in_use + is RoomAliasError.AliasInvalid -> R.string.create_room_alias_invalid + else -> null + } + ?.let { stringProvider.getString(it) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt index 8f57acee47..7b2e329b6a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt @@ -62,7 +62,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() holder.avatarView.isInvisible = directoryAvatarUrl.isNullOrBlank() && includeAllNetworks holder.nameView.text = directoryName - holder.descritionView.setTextOrHide(directoryDescription) + holder.descriptionView.setTextOrHide(directoryDescription) } class Holder : VectorEpoxyHolder() { @@ -70,6 +70,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() val avatarView by bind(R.id.itemRoomDirectoryAvatar) val nameView by bind(R.id.itemRoomDirectoryName) - val descritionView by bind(R.id.itemRoomDirectoryDescription) + val descriptionView by bind(R.id.itemRoomDirectoryDescription) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 2e91091443..e29c197ab8 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -79,6 +79,17 @@ class RoomMemberProfileController @Inject constructor( divider = false, action = { callback?.onIgnoreClicked() } ) + if (!state.isMine) { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + + buildProfileAction( + id = "direct", + editable = false, + title = stringProvider.getString(R.string.room_member_open_or_create_dm), + dividerColor = dividerColor, + action = { callback?.onOpenDmClicked() } + ) + } } private fun buildRoomMemberActions(state: RoomMemberProfileViewState) { diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index d60b5580fa..e994a3c3ec 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -294,12 +294,20 @@ class RoomMemberProfileFragment @Inject constructor( } private fun handleShareRoomMemberProfile(permalink: String) { - startSharePlainTextIntent( - fragment = this, - activityResultLauncher = null, - chooserTitle = null, - text = permalink - ) + val view = layoutInflater.inflate(R.layout.dialog_share_qr_code, null) + val qrCode = view.findViewById(R.id.itemShareQrCodeImage) + qrCode.setData(permalink) + AlertDialog.Builder(requireContext()) + .setView(view) + .setNeutralButton(R.string.ok, null) + .setPositiveButton(R.string.share_by_text) { _, _ -> + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = null, + text = permalink + ) + }.show() } private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt index 85bc8773a5..073d30ff8e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt @@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState sealed class RoomProfileAction : VectorViewModelAction { + object EnableEncryption : RoomProfileAction() object LeaveRoom : RoomProfileAction() data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() object ShareRoomProfile : RoomProfileAction() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 734620e378..696725d001 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -36,6 +36,7 @@ import im.vector.app.features.room.RequireActiveMembershipViewState import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment +import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import javax.inject.Inject @@ -46,10 +47,16 @@ class RoomProfileActivity : companion object { - fun newIntent(context: Context, roomId: String): Intent { + private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS" + + const val EXTRA_DIRECT_ACCESS_ROOM_ROOT = 0 + const val EXTRA_DIRECT_ACCESS_ROOM_SETTINGS = 1 + + fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent { val roomProfileArgs = RoomProfileArgs(roomId) return Intent(context, RoomProfileActivity::class.java).apply { putExtra(MvRx.KEY_ARG, roomProfileArgs) + putExtra(EXTRA_DIRECT_ACCESS, directAccess) } } } @@ -80,16 +87,23 @@ class RoomProfileActivity : sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) { + EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> { + addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs) + } + else -> addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + } } sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() - is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() - is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() - is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() + is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() + is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() + is RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() + is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() + is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() } } .disposeOnDestroy() @@ -123,6 +137,10 @@ class RoomProfileActivity : addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs) } + private fun openRoomAlias() { + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomAliasFragment::class.java, roomProfileArgs) + } + private fun openRoomMembers() { addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs) } 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 7dc744da31..891d15d04f 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 @@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject class RoomProfileController @Inject constructor( @@ -43,6 +44,7 @@ class RoomProfileController @Inject constructor( interface Callback { fun onLearnMoreClicked() + fun onEnableEncryptionClicked() fun onMemberListClicked() fun onBannedMemberListClicked() fun onNotificationsClicked() @@ -84,6 +86,7 @@ class RoomProfileController @Inject constructor( centered(false) text(stringProvider.getString(learnMoreSubtitle)) } + buildEncryptionAction(data.actionPermissions, roomSummary) // More buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) @@ -171,4 +174,29 @@ class RoomProfileController @Inject constructor( ) } } + + private fun buildEncryptionAction(actionPermissions: RoomProfileViewState.ActionPermissions, roomSummary: RoomSummary) { + if (!roomSummary.isEncrypted) { + if (actionPermissions.canEnableEncryption) { + buildProfileAction( + id = "enableEncryption", + title = stringProvider.getString(R.string.room_settings_enable_encryption), + dividerColor = dividerColor, + icon = R.drawable.ic_shield_black, + divider = false, + editable = false, + action = { callback?.onEnableEncryptionClicked() } + ) + } else { + buildProfileAction( + id = "enableEncryption", + title = stringProvider.getString(R.string.room_settings_enable_encryption_no_permission), + dividerColor = dividerColor, + icon = R.drawable.ic_shield_black, + divider = false, + editable = false + ) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 5bd121d49b..bab64aebe9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -49,6 +49,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.media.BigImageViewerActivity import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_matrix_profile.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.view_stub_room_profile_header.* import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.util.MatrixItem @@ -87,6 +88,7 @@ class RoomProfileFragment @Inject constructor( it.layoutResource = R.layout.view_stub_room_profile_header it.inflate() } + setupWaitingView() setupToolbar(matrixProfileToolbar) setupRecyclerView() appBarStateChangeListener = MatrixItemAppBarStateChangeListener( @@ -111,6 +113,11 @@ class RoomProfileFragment @Inject constructor( setupLongClicks() } + private fun setupWaitingView() { + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + } + private fun setupLongClicks() { roomProfileNameView.copyOnLongClick() roomProfileAliasView.copyOnLongClick() @@ -155,6 +162,8 @@ class RoomProfileFragment @Inject constructor( } override fun invalidate() = withState(roomProfileViewModel) { state -> + waiting_view.isVisible = state.isLoading + state.roomSummary()?.also { if (it.membership.isLeft()) { Timber.w("The room has been left") @@ -187,6 +196,17 @@ class RoomProfileFragment @Inject constructor( vectorBaseActivity.notImplemented() } + override fun onEnableEncryptionClicked() { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.room_settings_enable_encryption_dialog_title) + .setMessage(R.string.room_settings_enable_encryption_dialog_content) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ -> + roomProfileViewModel.handle(RoomProfileAction.EnableEncryption) + } + .show() + } + override fun onMemberListClicked() { roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt index 0052ddee99..83a610cf1b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt @@ -23,6 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction */ sealed class RoomProfileSharedAction : VectorSharedAction { object OpenRoomSettings : RoomProfileSharedAction() + object OpenRoomAliasesSettings : RoomProfileSharedAction() object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction() 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 e927ec9876..ec772ffcaa 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 @@ -28,12 +28,15 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider 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.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType 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.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.rx.RxRoom import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -65,6 +68,7 @@ class RoomProfileViewModel @AssistedInject constructor( val rxRoom = room.rx() observeRoomSummary(rxRoom) observeBannedRoomMembers(rxRoom) + observePermissions() } private fun observeRoomSummary(rxRoom: RxRoom) { @@ -82,8 +86,22 @@ class RoomProfileViewModel @AssistedInject constructor( } } + private fun observePermissions() { + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = RoomProfileViewState.ActionPermissions( + canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + ) + setState { copy(actionPermissions = permissions) } + } + .disposeOnClear() + } + override fun handle(action: RoomProfileAction) { when (action) { + is RoomProfileAction.EnableEncryption -> handleEnableEncryption() RoomProfileAction.LeaveRoom -> handleLeaveRoom() is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() @@ -91,6 +109,24 @@ class RoomProfileViewModel @AssistedInject constructor( }.exhaustive } + private fun handleEnableEncryption() { + postLoading(true) + + viewModelScope.launch { + val result = runCatching { room.enableEncryption() } + postLoading(false) + result.onFailure { failure -> + _viewEvents.post(RoomProfileViewEvents.Failure(failure)) + } + } + } + + private fun postLoading(isLoading: Boolean) { + setState { + copy(isLoading = isLoading) + } + } + private fun handleCreateShortcut() { viewModelScope.launch(Dispatchers.IO) { withState { state -> @@ -102,11 +138,13 @@ class RoomProfileViewModel @AssistedInject constructor( } private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) { - room.setRoomNotificationState(action.notificationState, object : MatrixCallback { - override fun onFailure(failure: Throwable) { + viewModelScope.launch { + try { + room.setRoomNotificationState(action.notificationState) + } catch (failure: Throwable) { _viewEvents.post(RoomProfileViewEvents.Failure(failure)) } - }) + } } private fun handleLeaveRoom() { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index 50723655bc..398982ede1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -26,8 +26,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary data class RoomProfileViewState( val roomId: String, val roomSummary: Async = Uninitialized, - val bannedMembership: Async> = Uninitialized + val bannedMembership: Async> = Uninitialized, + val actionPermissions: ActionPermissions = ActionPermissions(), + val isLoading: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + data class ActionPermissions( + val canEnableEncryption: Boolean = false + ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasAction.kt new file mode 100644 index 0000000000..80e1603453 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasAction.kt @@ -0,0 +1,39 @@ +/* + * Copyright 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.roomprofile.alias + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility + +sealed class RoomAliasAction : VectorViewModelAction { + // Canonical + object ToggleManualPublishForm : RoomAliasAction() + data class SetNewAlias(val alias: String) : RoomAliasAction() + object ManualPublishAlias : RoomAliasAction() + data class PublishAlias(val alias: String) : RoomAliasAction() + data class UnpublishAlias(val alias: String) : RoomAliasAction() + data class SetCanonicalAlias(val canonicalAlias: String?) : RoomAliasAction() + + // Room directory + data class SetRoomDirectoryVisibility(val roomDirectoryVisibility: RoomDirectoryVisibility) : RoomAliasAction() + + // Local + data class RemoveLocalAlias(val alias: String) : RoomAliasAction() + object ToggleAddLocalAliasForm : RoomAliasAction() + data class SetNewLocalAliasLocalPart(val aliasLocalPart: String) : RoomAliasAction() + object AddLocalAlias : RoomAliasAction() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt new file mode 100644 index 0000000000..0b695031c5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt @@ -0,0 +1,262 @@ +/* + * Copyright 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.roomprofile.alias + +import android.text.InputType +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.epoxy.profiles.buildProfileSection +import im.vector.app.core.epoxy.profiles.profileActionItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.discovery.settingsButtonItem +import im.vector.app.features.discovery.settingsContinueCancelItem +import im.vector.app.features.discovery.settingsInfoItem +import im.vector.app.features.form.formEditTextItem +import im.vector.app.features.form.formSwitchItem +import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import im.vector.app.features.roomdirectory.createroom.roomAliasEditItem +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import javax.inject.Inject + +class RoomAliasController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter, + private val colorProvider: ColorProvider, + private val roomAliasErrorFormatter: RoomAliasErrorFormatter +) : TypedEpoxyController() { + + interface Callback { + fun toggleManualPublishForm() + fun setNewAlias(alias: String) + fun addAlias() + fun setRoomDirectoryVisibility(roomDirectoryVisibility: RoomDirectoryVisibility) + fun toggleLocalAliasForm() + fun setNewLocalAliasLocalPart(aliasLocalPart: String) + fun addLocalAlias() + fun openAliasDetail(alias: String) + } + + var callback: Callback? = null + + init { + setData(null) + } + + override fun buildModels(data: RoomAliasViewState?) { + data ?: return + + // Published alias + buildPublishInfo(data) + // Room directory visibility + buildRoomDirectoryVisibility(data) + // Local alias + buildLocalInfo(data) + } + + private fun buildRoomDirectoryVisibility(data: RoomAliasViewState) { + when (data.roomDirectoryVisibility) { + Uninitialized -> Unit + is Loading -> Unit + is Success -> { + formSwitchItem { + id("roomVisibility") + title(stringProvider.getString(R.string.room_alias_publish_to_directory, data.homeServerName)) + showDivider(false) + switchChecked(data.roomDirectoryVisibility() == RoomDirectoryVisibility.PUBLIC) + listener { + if (it) { + callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PUBLIC) + } else { + callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PRIVATE) + } + } + } + } + is Fail -> { + errorWithRetryItem { + text(stringProvider.getString(R.string.room_alias_publish_to_directory_error, + errorFormatter.toHumanReadable(data.roomDirectoryVisibility.error))) + } + } + } + } + + private fun buildPublishInfo(data: RoomAliasViewState) { + buildProfileSection( + stringProvider.getString(R.string.room_alias_published_alias_title) + ) + settingsInfoItem { + id("publishedInfo") + helperTextResId(R.string.room_alias_published_alias_subtitle) + } + + data.canonicalAlias + ?.takeIf { it.isNotEmpty() } + ?.let { canonicalAlias -> + + profileActionItem { + id("canonical") + title(data.canonicalAlias) + subtitle(stringProvider.getString(R.string.room_alias_published_alias_main)) + listener { callback?.openAliasDetail(canonicalAlias) } + } + } + + if (data.alternativeAliases.isEmpty()) { + settingsInfoItem { + id("otherPublishedEmpty") + if (data.actionPermissions.canChangeCanonicalAlias) { + helperTextResId(R.string.room_alias_address_empty_can_add) + } else { + helperTextResId(R.string.room_alias_address_empty) + } + } + } else { + settingsInfoItem { + id("otherPublished") + helperTextResId(R.string.room_alias_published_other) + } + data.alternativeAliases.forEachIndexed { idx, altAlias -> + profileActionItem { + id("alt_$idx") + title(altAlias) + listener { callback?.openAliasDetail(altAlias) } + } + } + } + + if (data.actionPermissions.canChangeCanonicalAlias) { + buildPublishManuallyForm(data) + } + } + + private fun buildPublishManuallyForm(data: RoomAliasViewState) { + when (data.publishManuallyState) { + RoomAliasViewState.AddAliasState.Hidden -> Unit + RoomAliasViewState.AddAliasState.Closed -> { + settingsButtonItem { + id("publishManually") + colorProvider(colorProvider) + buttonTitleId(R.string.room_alias_published_alias_add_manually) + buttonClickListener { callback?.toggleManualPublishForm() } + } + } + is RoomAliasViewState.AddAliasState.Editing -> { + formEditTextItem { + id("publishManuallyEdit") + value(data.publishManuallyState.value) + showBottomSeparator(false) + hint(stringProvider.getString(R.string.room_alias_address_hint)) + inputType(InputType.TYPE_CLASS_TEXT) + onTextChange { text -> + callback?.setNewAlias(text) + } + } + settingsContinueCancelItem { + id("publishManuallySubmit") + continueText(stringProvider.getString(R.string.room_alias_published_alias_add_manually_submit)) + continueOnClick { callback?.addAlias() } + cancelOnClick { callback?.toggleManualPublishForm() } + } + } + } + } + + private fun buildLocalInfo(data: RoomAliasViewState) { + buildProfileSection( + stringProvider.getString(R.string.room_alias_local_address_title) + ) + settingsInfoItem { + id("localInfo") + helperText(stringProvider.getString(R.string.room_alias_local_address_subtitle, data.homeServerName)) + } + + when (val localAliases = data.localAliases) { + is Uninitialized -> { + loadingItem { + id("loadingAliases") + } + } + is Success -> { + if (localAliases().isEmpty()) { + settingsInfoItem { + id("locEmpty") + helperTextResId(R.string.room_alias_local_address_empty) + } + } else { + localAliases().forEachIndexed { idx, localAlias -> + profileActionItem { + id("loc_$idx") + title(localAlias) + listener { callback?.openAliasDetail(localAlias) } + } + } + } + } + is Fail -> { + errorWithRetryItem { + id("alt_error") + text(errorFormatter.toHumanReadable(localAliases.error)) + } + } + } + + // Add local + buildAddLocalAlias(data) + } + + private fun buildAddLocalAlias(data: RoomAliasViewState) { + when (data.newLocalAliasState) { + RoomAliasViewState.AddAliasState.Hidden -> Unit + RoomAliasViewState.AddAliasState.Closed -> { + settingsButtonItem { + id("newLocalAliasButton") + colorProvider(colorProvider) + buttonTitleId(R.string.room_alias_local_address_add) + buttonClickListener { callback?.toggleLocalAliasForm() } + } + } + is RoomAliasViewState.AddAliasState.Editing -> { + roomAliasEditItem { + id("newLocalAlias") + value(data.newLocalAliasState.value) + homeServer(":" + data.homeServerName) + showBottomSeparator(false) + errorMessage(roomAliasErrorFormatter.format((data.newLocalAliasState.asyncRequest as? Fail)?.error as? RoomAliasError)) + onTextChange { value -> + callback?.setNewLocalAliasLocalPart(value) + } + } + settingsContinueCancelItem { + id("newLocalAliasSubmit") + continueText(stringProvider.getString(R.string.action_add)) + continueOnClick { callback?.addLocalAlias() } + cancelOnClick { callback?.toggleLocalAliasForm() } + } + } + } + } +} 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 new file mode 100644 index 0000000000..56c3e76828 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -0,0 +1,193 @@ +/* + * Copyright 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.roomprofile.alias + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.dialogs.withColoredButton +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.shareText +import im.vector.app.core.utils.toast +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedAction +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel +import kotlinx.android.synthetic.main.fragment_room_setting_generic.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class RoomAliasFragment @Inject constructor( + val viewModelFactory: RoomAliasViewModel.Factory, + private val controller: RoomAliasController, + private val avatarRenderer: AvatarRenderer +) : + VectorBaseFragment(), + RoomAliasController.Callback { + + private val viewModel: RoomAliasViewModel by fragmentViewModel() + private lateinit var sharedActionViewModel: RoomAliasBottomSheetSharedActionViewModel + + private val roomProfileArgs: RoomProfileArgs by args() + + override fun getLayoutResId() = R.layout.fragment_room_setting_generic + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(RoomAliasBottomSheetSharedActionViewModel::class.java) + + controller.callback = this + setupToolbar(roomSettingsToolbar) + roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + + viewModel.observeViewEvents { + when (it) { + is RoomAliasViewEvents.Failure -> showFailure(it.throwable) + RoomAliasViewEvents.Success -> showSuccess() + }.exhaustive + } + + sharedActionViewModel + .observe() + .subscribe { handleAliasAction(it) } + .disposeOnDestroyView() + } + + private fun handleAliasAction(action: RoomAliasBottomSheetSharedAction?) { + when (action) { + is RoomAliasBottomSheetSharedAction.ShareAlias -> shareAlias(action.matrixTo) + is RoomAliasBottomSheetSharedAction.PublishAlias -> viewModel.handle(RoomAliasAction.PublishAlias(action.alias)) + is RoomAliasBottomSheetSharedAction.UnPublishAlias -> unpublishAlias(action.alias) + is RoomAliasBottomSheetSharedAction.DeleteAlias -> removeLocalAlias(action.alias) + is RoomAliasBottomSheetSharedAction.SetMainAlias -> viewModel.handle(RoomAliasAction.SetCanonicalAlias(action.alias)) + RoomAliasBottomSheetSharedAction.UnsetMainAlias -> viewModel.handle(RoomAliasAction.SetCanonicalAlias(canonicalAlias = null)) + null -> Unit + } + } + + private fun shareAlias(matrixTo: String) { + shareText(requireContext(), matrixTo) + } + + override fun showFailure(throwable: Throwable) { + if (throwable !is RoomAliasError) { + super.showFailure(throwable) + } + } + + private fun showSuccess() { + activity?.toast(R.string.room_settings_save_success) + } + + override fun onDestroyView() { + controller.callback = null + roomSettingsRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + waiting_view.isVisible = state.isLoading + controller.setData(state) + renderRoomSummary(state) + } + + private fun renderRoomSummary(state: RoomAliasViewState) { + state.roomSummary()?.let { + roomSettingsToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView) + } + } + + private fun unpublishAlias(alias: String) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_confirmation) + .setMessage(getString(R.string.room_alias_unpublish_confirmation, alias)) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.action_unpublish) { _, _ -> + viewModel.handle(RoomAliasAction.UnpublishAlias(alias)) + } + .show() + .withColoredButton(DialogInterface.BUTTON_POSITIVE) + } + + override fun toggleManualPublishForm() { + viewModel.handle(RoomAliasAction.ToggleManualPublishForm) + } + + override fun setNewAlias(alias: String) { + viewModel.handle(RoomAliasAction.SetNewAlias(alias)) + } + + override fun addAlias() { + viewModel.handle(RoomAliasAction.ManualPublishAlias) + } + + override fun setRoomDirectoryVisibility(roomDirectoryVisibility: RoomDirectoryVisibility) { + viewModel.handle(RoomAliasAction.SetRoomDirectoryVisibility(roomDirectoryVisibility)) + } + + override fun toggleLocalAliasForm() { + viewModel.handle(RoomAliasAction.ToggleAddLocalAliasForm) + } + + override fun setNewLocalAliasLocalPart(aliasLocalPart: String) { + viewModel.handle(RoomAliasAction.SetNewLocalAliasLocalPart(aliasLocalPart)) + } + + override fun addLocalAlias() { + viewModel.handle(RoomAliasAction.AddLocalAlias) + } + + override fun openAliasDetail(alias: String) = withState(viewModel) { state -> + RoomAliasBottomSheet + .newInstance( + alias = alias, + isPublished = alias in state.allPublishedAliases, + isMainAlias = alias == state.canonicalAlias, + isLocal = alias in state.localAliases().orEmpty(), + canEditCanonicalAlias = state.actionPermissions.canChangeCanonicalAlias + ) + .show(childFragmentManager, "ROOM_ALIAS_ACTIONS") + } + + private fun removeLocalAlias(alias: String) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_confirmation) + .setMessage(getString(R.string.room_alias_delete_confirmation, alias)) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.delete) { _, _ -> + viewModel.handle(RoomAliasAction.RemoveLocalAlias(alias)) + } + .show() + .withColoredButton(DialogInterface.BUTTON_POSITIVE) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewEvents.kt new file mode 100644 index 0000000000..bbd44741b5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewEvents.kt @@ -0,0 +1,28 @@ +/* + * Copyright 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.roomprofile.alias + +import im.vector.app.core.platform.VectorViewEvents + +/** + * Transient events for room settings screen + */ +sealed class RoomAliasViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : RoomAliasViewEvents() + object Success : RoomAliasViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt new file mode 100644 index 0000000000..5873d9ce8a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt @@ -0,0 +1,384 @@ +/* + * Copyright 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.roomprofile.alias + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +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 +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.rx.mapOptional +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: RoomAliasViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomAliasViewState): RoomAliasViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomAliasViewState): RoomAliasViewModel? { + val fragment: RoomAliasFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + initHomeServerName() + observeRoomSummary() + observePowerLevel() + observeRoomCanonicalAlias() + fetchRoomAlias() + fetchRoomDirectoryVisibility() + } + + private fun fetchRoomDirectoryVisibility() { + setState { + copy( + roomDirectoryVisibility = Loading() + ) + } + viewModelScope.launch { + runCatching { + session.getRoomDirectoryVisibility(room.roomId) + }.fold( + { + setState { + copy( + roomDirectoryVisibility = Success(it) + ) + } + }, + { + setState { + copy( + roomDirectoryVisibility = Fail(it) + ) + } + } + ) + } + } + + private fun initHomeServerName() { + setState { + copy( + homeServerName = session.myUserId.substringAfter(":") + ) + } + } + + private fun fetchRoomAlias() { + setState { + copy( + localAliases = Loading() + ) + } + + viewModelScope.launch { + runCatching { room.getRoomAliases() } + .fold( + { + setState { copy(localAliases = Success(it.sorted())) } + }, + { + setState { copy(localAliases = Fail(it)) } + } + ) + } + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { async -> + copy( + roomSummary = async + ) + } + } + + private fun observePowerLevel() { + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = RoomAliasViewState.ActionPermissions( + canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend( + userId = session.myUserId, + isState = true, + eventType = EventType.STATE_ROOM_CANONICAL_ALIAS + ) + ) + setState { + val newPublishManuallyState = if (permissions.canChangeCanonicalAlias) { + when (publishManuallyState) { + RoomAliasViewState.AddAliasState.Hidden -> RoomAliasViewState.AddAliasState.Closed + else -> publishManuallyState + } + } else { + RoomAliasViewState.AddAliasState.Hidden + } + copy( + actionPermissions = permissions, + publishManuallyState = newPublishManuallyState + ) + } + } + .disposeOnClear() + } + + /** + * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. + */ + private fun observeRoomCanonicalAlias() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_CANONICAL_ALIAS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + setState { + copy( + canonicalAlias = it.canonicalAlias, + alternativeAliases = it.alternativeAliases.orEmpty().sorted() + ) + } + } + .disposeOnClear() + } + + override fun handle(action: RoomAliasAction) { + when (action) { + RoomAliasAction.ToggleManualPublishForm -> handleToggleManualPublishForm() + is RoomAliasAction.SetNewAlias -> handleSetNewAlias(action) + is RoomAliasAction.ManualPublishAlias -> handleManualPublishAlias() + is RoomAliasAction.UnpublishAlias -> handleUnpublishAlias(action) + is RoomAliasAction.SetCanonicalAlias -> handleSetCanonicalAlias(action) + is RoomAliasAction.SetRoomDirectoryVisibility -> handleSetRoomDirectoryVisibility(action) + RoomAliasAction.ToggleAddLocalAliasForm -> handleToggleAddLocalAliasForm() + is RoomAliasAction.SetNewLocalAliasLocalPart -> handleSetNewLocalAliasLocalPart(action) + RoomAliasAction.AddLocalAlias -> handleAddLocalAlias() + is RoomAliasAction.RemoveLocalAlias -> handleRemoveLocalAlias(action) + is RoomAliasAction.PublishAlias -> handlePublishAlias(action) + }.exhaustive + } + + private fun handleSetRoomDirectoryVisibility(action: RoomAliasAction.SetRoomDirectoryVisibility) { + postLoading(true) + viewModelScope.launch { + runCatching { + session.setRoomDirectoryVisibility(room.roomId, action.roomDirectoryVisibility) + }.fold( + { + setState { + copy( + isLoading = false, + // Local echo, no need to fetch the data from the server again + roomDirectoryVisibility = Success(action.roomDirectoryVisibility) + ) + } + }, + { + postLoading(false) + _viewEvents.post(RoomAliasViewEvents.Failure(it)) + } + ) + } + } + + private fun handleToggleAddLocalAliasForm() { + setState { + copy( + newLocalAliasState = when (newLocalAliasState) { + RoomAliasViewState.AddAliasState.Hidden -> RoomAliasViewState.AddAliasState.Hidden + RoomAliasViewState.AddAliasState.Closed -> RoomAliasViewState.AddAliasState.Editing("", Uninitialized) + is RoomAliasViewState.AddAliasState.Editing -> RoomAliasViewState.AddAliasState.Closed + } + ) + } + } + + private fun handleToggleManualPublishForm() { + setState { + copy( + publishManuallyState = when (publishManuallyState) { + RoomAliasViewState.AddAliasState.Hidden -> RoomAliasViewState.AddAliasState.Hidden + RoomAliasViewState.AddAliasState.Closed -> RoomAliasViewState.AddAliasState.Editing("", Uninitialized) + is RoomAliasViewState.AddAliasState.Editing -> RoomAliasViewState.AddAliasState.Closed + } + ) + } + } + + private fun handleSetNewAlias(action: RoomAliasAction.SetNewAlias) { + setState { + copy( + publishManuallyState = RoomAliasViewState.AddAliasState.Editing(action.alias, Uninitialized) + ) + } + } + + private fun handleSetNewLocalAliasLocalPart(action: RoomAliasAction.SetNewLocalAliasLocalPart) { + setState { + copy( + newLocalAliasState = RoomAliasViewState.AddAliasState.Editing(action.aliasLocalPart, Uninitialized) + ) + } + } + + private fun handleManualPublishAlias() = withState { state -> + val newAlias = (state.publishManuallyState as? RoomAliasViewState.AddAliasState.Editing)?.value ?: return@withState + updateCanonicalAlias( + canonicalAlias = state.canonicalAlias, + alternativeAliases = state.alternativeAliases + newAlias, + closeForm = true + ) + } + + private fun handlePublishAlias(action: RoomAliasAction.PublishAlias) = withState { state -> + updateCanonicalAlias( + canonicalAlias = state.canonicalAlias, + alternativeAliases = state.alternativeAliases + action.alias, + closeForm = false + ) + } + + private fun handleUnpublishAlias(action: RoomAliasAction.UnpublishAlias) = withState { state -> + updateCanonicalAlias( + // We can also unpublish the canonical alias + canonicalAlias = state.canonicalAlias.takeIf { it != action.alias }, + alternativeAliases = state.alternativeAliases - action.alias, + closeForm = false + ) + } + + private fun handleSetCanonicalAlias(action: RoomAliasAction.SetCanonicalAlias) = withState { state -> + updateCanonicalAlias( + canonicalAlias = action.canonicalAlias, + // Ensure the previous canonical alias is moved to the alt aliases + alternativeAliases = state.allPublishedAliases, + closeForm = false + ) + } + + private fun updateCanonicalAlias(canonicalAlias: String?, alternativeAliases: List, closeForm: Boolean) { + postLoading(true) + room.updateCanonicalAlias(canonicalAlias, alternativeAliases, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + isLoading = false, + publishManuallyState = if (closeForm) RoomAliasViewState.AddAliasState.Closed else publishManuallyState + ) + } + } + + override fun onFailure(failure: Throwable) { + postLoading(false) + _viewEvents.post(RoomAliasViewEvents.Failure(failure)) + } + }) + } + + private fun handleAddLocalAlias() = withState { state -> + val previousState = (state.newLocalAliasState as? RoomAliasViewState.AddAliasState.Editing) ?: return@withState + + setState { + copy( + isLoading = true, + newLocalAliasState = previousState.copy(asyncRequest = Loading()) + ) + } + viewModelScope.launch { + runCatching { room.addAlias(previousState.value) } + .onFailure { + setState { + copy( + isLoading = false, + newLocalAliasState = previousState.copy(asyncRequest = Fail(it)) + ) + } + _viewEvents.post(RoomAliasViewEvents.Failure(it)) + } + .onSuccess { + setState { + copy( + isLoading = false, + newLocalAliasState = RoomAliasViewState.AddAliasState.Closed, + // Local echo + localAliases = Success((localAliases().orEmpty() + previousState.value).sorted()) + ) + } + fetchRoomAlias() + } + } + } + + private fun handleRemoveLocalAlias(action: RoomAliasAction.RemoveLocalAlias) { + postLoading(true) + viewModelScope.launch { + runCatching { session.deleteRoomAlias(action.alias) } + .onFailure { + setState { + copy(isLoading = false) + } + _viewEvents.post(RoomAliasViewEvents.Failure(it)) + } + .onSuccess { + // Local echo + setState { + copy( + isLoading = false, + // Local echo + localAliases = Success(localAliases().orEmpty() - action.alias) + ) + } + fetchRoomAlias() + } + } + } + + private fun postLoading(isLoading: Boolean) { + setState { + copy(isLoading = isLoading) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt new file mode 100644 index 0000000000..f6341f4f64 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt @@ -0,0 +1,54 @@ +/* + * Copyright 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.roomprofile.alias + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.roomprofile.RoomProfileArgs +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class RoomAliasViewState( + val roomId: String, + val homeServerName: String = "", + val roomSummary: Async = Uninitialized, + val actionPermissions: ActionPermissions = ActionPermissions(), + val roomDirectoryVisibility: Async = Uninitialized, + val isLoading: Boolean = false, + val canonicalAlias: String? = null, + val alternativeAliases: List = emptyList(), + val publishManuallyState: AddAliasState = AddAliasState.Hidden, + val localAliases: Async> = Uninitialized, + val newLocalAliasState: AddAliasState = AddAliasState.Closed +) : MvRxState { + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + val allPublishedAliases: List + get() = (alternativeAliases + listOfNotNull(canonicalAlias)).distinct() + + data class ActionPermissions( + val canChangeCanonicalAlias: Boolean = false + ) + + sealed class AddAliasState { + object Hidden : AddAliasState() + object Closed : AddAliasState() + data class Editing(val value: String, val asyncRequest: Async = Uninitialized) : AddAliasState() + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt new file mode 100644 index 0000000000..86702d1507 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt @@ -0,0 +1,107 @@ +/* + * 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.roomprofile.alias.detail + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import kotlinx.android.parcel.Parcelize +import javax.inject.Inject + +@Parcelize +data class RoomAliasBottomSheetArgs( + val alias: String, + val isPublished: Boolean, + val isMainAlias: Boolean, + val isLocal: Boolean, + val canEditCanonicalAlias: Boolean +) : Parcelable + +/** + * Bottom sheet fragment that shows room alias information with list of contextual actions + */ +class RoomAliasBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomAliasBottomSheetController.Listener { + + private lateinit var sharedActionViewModel: RoomAliasBottomSheetSharedActionViewModel + @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool + @Inject lateinit var roomAliasBottomSheetViewModelFactory: RoomAliasBottomSheetViewModel.Factory + @Inject lateinit var controller: RoomAliasBottomSheetController + + private val viewModel: RoomAliasBottomSheetViewModel by fragmentViewModel(RoomAliasBottomSheetViewModel::class) + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView + + override val showExpanded = true + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutResId() = R.layout.bottom_sheet_generic_list + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(RoomAliasBottomSheetSharedActionViewModel::class.java) + recyclerView.configureWith(controller, viewPool = sharedViewPool, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun onDestroyView() { + recyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { + controller.setData(it) + super.invalidate() + } + + override fun didSelectMenuAction(quickAction: RoomAliasBottomSheetSharedAction) { + sharedActionViewModel.post(quickAction) + + dismiss() + } + + companion object { + fun newInstance(alias: String, + isPublished: Boolean, + isMainAlias: Boolean, + isLocal: Boolean, + canEditCanonicalAlias: Boolean): RoomAliasBottomSheet { + return RoomAliasBottomSheet().apply { + setArguments(RoomAliasBottomSheetArgs( + alias = alias, + isPublished = isPublished, + isMainAlias = isMainAlias, + isLocal = isLocal, + canEditCanonicalAlias = canEditCanonicalAlias + )) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt new file mode 100644 index 0000000000..157037c13d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt @@ -0,0 +1,88 @@ +/* + * 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.roomprofile.alias.detail + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem +import im.vector.app.core.epoxy.dividerItem +import im.vector.app.core.epoxy.profiles.profileActionItem +import javax.inject.Inject + +/** + * Epoxy controller for room alias actions + */ +class RoomAliasBottomSheetController @Inject constructor() : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(state: RoomAliasBottomSheetState) { + profileActionItem { + id("alias") + title(state.alias) + subtitle(state.matrixToLink) + editable(false) + } + + // Notifications + dividerItem { + id("aliasSeparator") + } + + var idx = 0 + // Share + state.matrixToLink?.let { + RoomAliasBottomSheetSharedAction.ShareAlias(it).toBottomSheetItem(++idx) + } + + // Action on published alias + if (state.isPublished) { + // Published address + if (state.canEditCanonicalAlias) { + if (state.isMainAlias) { + RoomAliasBottomSheetSharedAction.UnsetMainAlias.toBottomSheetItem(++idx) + } else { + RoomAliasBottomSheetSharedAction.SetMainAlias(state.alias).toBottomSheetItem(++idx) + } + RoomAliasBottomSheetSharedAction.UnPublishAlias(state.alias).toBottomSheetItem(++idx) + } + } + + if (state.isLocal) { + // Local address + if (state.canEditCanonicalAlias && state.isPublished.not()) { + // Publish + RoomAliasBottomSheetSharedAction.PublishAlias(state.alias).toBottomSheetItem(++idx) + } + // Delete + RoomAliasBottomSheetSharedAction.DeleteAlias(state.alias).toBottomSheetItem(++idx) + } + } + + private fun RoomAliasBottomSheetSharedAction.toBottomSheetItem(index: Int) { + return bottomSheetActionItem { + id("action_$index") + iconRes(iconResId) + textRes(titleRes) + destructive(this@toBottomSheetItem.destructive) + listener(View.OnClickListener { listener?.didSelectMenuAction(this@toBottomSheetItem) }) + } + } + + interface Listener { + fun didSelectMenuAction(quickAction: RoomAliasBottomSheetSharedAction) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt new file mode 100644 index 0000000000..13909c401f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt @@ -0,0 +1,56 @@ +/* + * 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.roomprofile.alias.detail + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.app.R +import im.vector.app.core.platform.VectorSharedAction + +sealed class RoomAliasBottomSheetSharedAction( + @StringRes val titleRes: Int, + @DrawableRes val iconResId: Int = 0, + val destructive: Boolean = false) + : VectorSharedAction { + + data class ShareAlias(val matrixTo: String) : RoomAliasBottomSheetSharedAction( + R.string.share, + R.drawable.ic_material_share + ) + + data class PublishAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.room_alias_action_publish + ) + + data class UnPublishAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.room_alias_action_unpublish + ) + + data class DeleteAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.delete, + R.drawable.ic_trash_24, + true + ) + + data class SetMainAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.room_settings_set_main_address + ) + + object UnsetMainAlias : RoomAliasBottomSheetSharedAction( + R.string.room_settings_unset_main_address + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedActionViewModel.kt new file mode 100644 index 0000000000..5f71783515 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedActionViewModel.kt @@ -0,0 +1,25 @@ +/* + * 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.roomprofile.alias.detail + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +/** + * Activity shared view model to handle room alias quick actions + */ +class RoomAliasBottomSheetSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt new file mode 100644 index 0000000000..a61075cef6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt @@ -0,0 +1,37 @@ +/* + * 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.roomprofile.alias.detail + +import com.airbnb.mvrx.MvRxState + +data class RoomAliasBottomSheetState( + val alias: String, + val matrixToLink: String? = null, + val isPublished: Boolean, + val isMainAlias: Boolean, + val isLocal: Boolean, + val canEditCanonicalAlias: Boolean +) : MvRxState { + + constructor(args: RoomAliasBottomSheetArgs) : this( + alias = args.alias, + isPublished = args.isPublished, + isMainAlias = args.isMainAlias, + isLocal = args.isLocal, + canEditCanonicalAlias = args.canEditCanonicalAlias + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt new file mode 100644 index 0000000000..7f723cae53 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt @@ -0,0 +1,58 @@ +/* + * 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.roomprofile.alias.detail + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.session.Session + +class RoomAliasBottomSheetViewModel @AssistedInject constructor( + @Assisted initialState: RoomAliasBottomSheetState, + session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel? { + val fragment: RoomAliasBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.roomAliasBottomSheetViewModelFactory.create(state) + } + } + + init { + setState { + copy( + matrixToLink = session.permalinkService().createPermalink(alias) + ) + } + } + + override fun handle(action: EmptyAction) { + // No op + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListAction.kt similarity index 82% rename from vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListAction.kt index ca7d567d90..8f6f5afba1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListAction.kt @@ -19,8 +19,8 @@ package im.vector.app.features.roomprofile.banned import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -sealed class RoomBannedListMemberAction : VectorViewModelAction { - data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() - data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() - data class Filter(val filter: String) : RoomBannedListMemberAction() +sealed class RoomBannedMemberListAction : VectorViewModelAction { + data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedMemberListAction() + data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedMemberListAction() + data class Filter(val filter: String) : RoomBannedMemberListAction() } 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 81b977ac97..349321c87a 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 @@ -37,18 +37,18 @@ import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomBannedMemberListFragment @Inject constructor( - val viewModelFactory: RoomBannedListMemberViewModel.Factory, + val viewModelFactory: RoomBannedMemberListViewModel.Factory, private val roomMemberListController: RoomBannedMemberListController, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), RoomBannedMemberListController.Callback { - private val viewModel: RoomBannedListMemberViewModel by fragmentViewModel() + private val viewModel: RoomBannedMemberListViewModel by fragmentViewModel() private val roomProfileArgs: RoomProfileArgs by args() override fun getLayoutResId() = R.layout.fragment_room_setting_generic override fun onUnbanClicked(roomMember: RoomMemberSummary) { - viewModel.handle(RoomBannedListMemberAction.QueryInfo(roomMember)) + viewModel.handle(RoomBannedMemberListAction.QueryInfo(roomMember)) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -56,11 +56,11 @@ class RoomBannedMemberListFragment @Inject constructor( roomMemberListController.callback = this setupToolbar(roomSettingsToolbar) setupSearchView() - recyclerView.configureWith(roomMemberListController, hasFixedSize = true) + roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true) viewModel.observeViewEvents { when (it) { - is RoomBannedViewEvents.ShowBannedInfo -> { + is RoomBannedMemberListViewEvents.ShowBannedInfo -> { val canBan = withState(viewModel) { state -> state.canUserBan } AlertDialog.Builder(requireActivity()) .setTitle(getString(R.string.member_banned_by, it.bannedByUserId)) @@ -69,13 +69,13 @@ class RoomBannedMemberListFragment @Inject constructor( .apply { if (canBan) { setNegativeButton(R.string.room_participants_action_unban) { _, _ -> - viewModel.handle(RoomBannedListMemberAction.UnBanUser(it.roomMemberSummary)) + viewModel.handle(RoomBannedMemberListAction.UnBanUser(it.roomMemberSummary)) } } } .show() } - is RoomBannedViewEvents.ToastError -> { + is RoomBannedMemberListViewEvents.ToastError -> { requireActivity().toast(it.info) } } @@ -83,7 +83,7 @@ class RoomBannedMemberListFragment @Inject constructor( } override fun onDestroyView() { - recyclerView.cleanup() + roomSettingsRecyclerView.cleanup() super.onDestroyView() } @@ -96,7 +96,7 @@ class RoomBannedMemberListFragment @Inject constructor( } override fun onQueryTextChange(newText: String): Boolean { - viewModel.handle(RoomBannedListMemberAction.Filter(newText)) + viewModel.handle(RoomBannedMemberListAction.Filter(newText)) return true } }) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewEvents.kt similarity index 83% rename from vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedViewEvents.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewEvents.kt index 6b59debe96..4b1dc018ee 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewEvents.kt @@ -19,7 +19,7 @@ package im.vector.app.features.roomprofile.banned import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -sealed class RoomBannedViewEvents : VectorViewEvents { - data class ShowBannedInfo(val bannedByUserId: String, val banReason: String, val roomMemberSummary: RoomMemberSummary) : RoomBannedViewEvents() - data class ToastError(val info: String) : RoomBannedViewEvents() +sealed class RoomBannedMemberListViewEvents : VectorViewEvents { + data class ShowBannedInfo(val bannedByUserId: String, val banReason: String, val roomMemberSummary: RoomMemberSummary) : RoomBannedMemberListViewEvents() + data class ToastError(val info: String) : RoomBannedMemberListViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt similarity index 84% rename from vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt index 1cce2f96cb..0cecd22fa0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt @@ -42,14 +42,14 @@ import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap -class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initialState: RoomBannedMemberListViewState, +class RoomBannedMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomBannedMemberListViewState, private val stringProvider: StringProvider, private val session: Session) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { - fun create(initialState: RoomBannedMemberListViewState): RoomBannedListMemberViewModel + fun create(initialState: RoomBannedMemberListViewState): RoomBannedMemberListViewModel } private val room = session.getRoom(initialState.roomId)!! @@ -78,24 +78,24 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia }.disposeOnClear() } - companion object : MvRxViewModelFactory { + companion object : MvRxViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomBannedMemberListViewState): RoomBannedListMemberViewModel? { + override fun create(viewModelContext: ViewModelContext, state: RoomBannedMemberListViewState): RoomBannedMemberListViewModel? { val fragment: RoomBannedMemberListFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.viewModelFactory.create(state) } } - override fun handle(action: RoomBannedListMemberAction) { + override fun handle(action: RoomBannedMemberListAction) { when (action) { - is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary) - is RoomBannedListMemberAction.UnBanUser -> unBanUser(action.roomMemberSummary) - is RoomBannedListMemberAction.Filter -> handleFilter(action) + is RoomBannedMemberListAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary) + is RoomBannedMemberListAction.UnBanUser -> unBanUser(action.roomMemberSummary) + is RoomBannedMemberListAction.Filter -> handleFilter(action) }.exhaustive } - private fun handleFilter(action: RoomBannedListMemberAction.Filter) { + private fun handleFilter(action: RoomBannedMemberListAction.Filter) { setState { copy( filter = action.filter @@ -114,7 +114,7 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia val reason = content.reason val bannedBy = bannedEvent?.senderId ?: return - _viewEvents.post(RoomBannedViewEvents.ShowBannedInfo(bannedBy, reason ?: "", roomMemberSummary)) + _viewEvents.post(RoomBannedMemberListViewEvents.ShowBannedInfo(bannedBy, reason ?: "", roomMemberSummary)) } private fun unBanUser(roomMemberSummary: RoomMemberSummary) { @@ -127,7 +127,7 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia room.unban(roomMemberSummary.userId, null, it) } } catch (failure: Throwable) { - _viewEvents.post(RoomBannedViewEvents.ToastError(stringProvider.getString(R.string.failed_to_unban))) + _viewEvents.post(RoomBannedMemberListViewEvents.ToastError(stringProvider.getString(R.string.failed_to_unban))) } finally { setState { copy( 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 1b3e33a161..fb42b8ce27 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 @@ -57,7 +57,7 @@ class RoomMemberListFragment @Inject constructor( setupToolbar(roomSettingsToolbar) setupSearchView() setupInviteUsersButton() - recyclerView.configureWith(roomMemberListController, hasFixedSize = true) + roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true) } private fun setupInviteUsersButton() { @@ -65,7 +65,7 @@ class RoomMemberListFragment @Inject constructor( navigator.openInviteUsersToRoom(requireContext(), roomProfileArgs.roomId) } // Hide FAB when list is scrolling - recyclerView.addOnScrollListener( + roomSettingsRecyclerView.addOnScrollListener( object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { when (newState) { @@ -99,7 +99,7 @@ class RoomMemberListFragment @Inject constructor( } override fun onDestroyView() { - recyclerView.cleanup() + roomSettingsRecyclerView.cleanup() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt index 80bb8813cf..867c605030 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt @@ -17,15 +17,17 @@ package im.vector.app.features.roomprofile.settings import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules sealed class RoomSettingsAction : VectorViewModelAction { data class SetAvatarAction(val avatarAction: RoomSettingsViewState.AvatarAction) : RoomSettingsAction() data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() - data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction() - object EnableEncryption : RoomSettingsAction() + data class SetRoomJoinRule(val roomJoinRule: RoomJoinRules?, val roomGuestAccess: GuestAccess?) : RoomSettingsAction() + object Save : RoomSettingsAction() object Cancel : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 5231cc6b06..bf3c1f87f8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -26,10 +26,8 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter -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.RoomHistoryVisibilityContent -import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -44,11 +42,11 @@ class RoomSettingsController @Inject constructor( // Delete the avatar, or cancel an avatar change fun onAvatarDelete() fun onAvatarChange() - fun onEnableEncryptionClicked() fun onNameChanged(name: String) fun onTopicChanged(topic: String) fun onHistoryVisibilityClicked() - fun onAliasChanged(alias: String) + fun onRoomAliasesClicked() + fun onJoinRuleClicked() } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -62,20 +60,17 @@ class RoomSettingsController @Inject constructor( override fun buildModels(data: RoomSettingsViewState?) { val roomSummary = data?.roomSummary?.invoke() ?: return - val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: "" - val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) } - formEditableAvatarItem { id("avatar") enabled(data.actionPermissions.canChangeAvatar) when (val avatarAction = data.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> { + RoomSettingsViewState.AvatarAction.None -> { // Use the current value avatarRenderer(avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) } - RoomSettingsViewState.AvatarAction.DeleteAvatar -> + RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null) is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri) @@ -110,57 +105,48 @@ class RoomSettingsController @Inject constructor( } } - formEditTextItem { - id("alias") - enabled(data.actionPermissions.canChangeCanonicalAlias) - value(data.newCanonicalAlias ?: roomSummary.canonicalAlias) - hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address)) - - onTextChange { text -> - callback?.onAliasChanged(text) - } - } + buildProfileAction( + id = "alias", + title = stringProvider.getString(R.string.room_settings_alias_title), + subtitle = stringProvider.getString(R.string.room_settings_alias_subtitle), + dividerColor = dividerColor, + divider = true, + editable = true, + action = { callback?.onRoomAliasesClicked() } + ) buildProfileAction( id = "historyReadability", title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), - subtitle = newHistoryVisibility ?: historyVisibility, + subtitle = roomHistoryVisibilityFormatter.getSetting(data.newHistoryVisibility ?: data.currentHistoryVisibility), dividerColor = dividerColor, - divider = false, - editable = data.actionPermissions.canChangeHistoryReadability, - action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() } + divider = true, + editable = data.actionPermissions.canChangeHistoryVisibility, + action = { if (data.actionPermissions.canChangeHistoryVisibility) callback?.onHistoryVisibilityClicked() } ) - buildEncryptionAction(data.actionPermissions, roomSummary) + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_title), + subtitle = data.getJoinRuleWording(), + dividerColor = dividerColor, + divider = false, + editable = data.actionPermissions.canChangeJoinRule, + action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } + ) } - private fun buildEncryptionAction(actionPermissions: RoomSettingsViewState.ActionPermissions, roomSummary: RoomSummary) { - if (!actionPermissions.canEnableEncryption) { - return - } - if (roomSummary.isEncrypted) { - buildProfileAction( - id = "encryption", - title = stringProvider.getString(R.string.room_settings_addresses_e2e_enabled), - dividerColor = dividerColor, - divider = false, - editable = false - ) + private fun RoomSettingsViewState.getJoinRuleWording(): String { + val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules + val guestAccess = newRoomJoinRules.newGuestAccess ?: currentGuestAccess + return stringProvider.getString(if (joinRule == RoomJoinRules.INVITE) { + R.string.room_settings_room_access_entry_only_invited } else { - buildProfileAction( - id = "encryption", - title = stringProvider.getString(R.string.room_settings_enable_encryption), - subtitle = stringProvider.getString(R.string.room_settings_enable_encryption_warning), - dividerColor = dividerColor, - divider = false, - editable = true, - action = { callback?.onEnableEncryptionClicked() } - ) - } - } - - private fun formatRoomHistoryVisibilityEvent(event: Event): String? { - val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null - return roomHistoryVisibilityFormatter.format(historyVisibility) + if (guestAccess == GuestAccess.CanJoin) { + R.string.room_settings_room_access_entry_anyone_with_link_including_guest + } else { + R.string.room_settings_room_access_entry_anyone_with_link_apart_guest + } + }) } } 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 57521f7d80..d8c8c41936 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 @@ -37,13 +37,15 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.toast import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.RoomProfileSharedAction +import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel import kotlinx.android.synthetic.main.fragment_room_setting_generic.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.util.toMatrixItem import java.util.UUID import javax.inject.Inject @@ -51,7 +53,6 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, - private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, colorProvider: ColorProvider, private val avatarRenderer: AvatarRenderer ) : @@ -61,6 +62,10 @@ class RoomSettingsFragment @Inject constructor( GalleryOrCameraDialogHelper.Listener { private val viewModel: RoomSettingsViewModel by fragmentViewModel() + private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel + private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel + private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel + private val roomProfileArgs: RoomProfileArgs by args() private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) @@ -70,9 +75,12 @@ class RoomSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) + setupRoomHistoryVisibilitySharedActionViewModel() + setupRoomJoinRuleSharedActionViewModel() controller.callback = this setupToolbar(roomSettingsToolbar) - recyclerView.configureWith(controller, hasFixedSize = true) + roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.isVisible = true @@ -88,12 +96,33 @@ class RoomSettingsFragment @Inject constructor( } } + private fun setupRoomJoinRuleSharedActionViewModel() { + roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) + roomJoinRuleSharedActionViewModel + .observe() + .subscribe { action -> + viewModel.handle(RoomSettingsAction.SetRoomJoinRule(action.roomJoinRule, action.roomGuestAccess)) + } + .disposeOnDestroyView() + } + + private fun setupRoomHistoryVisibilitySharedActionViewModel() { + roomHistoryVisibilitySharedActionViewModel = activityViewModelProvider.get(RoomHistoryVisibilitySharedActionViewModel::class.java) + roomHistoryVisibilitySharedActionViewModel + .observe() + .subscribe { action -> + viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(action.roomHistoryVisibility)) + } + .disposeOnDestroyView() + } + private fun showSuccess() { activity?.toast(R.string.room_settings_save_success) } override fun onDestroyView() { - recyclerView.cleanup() + controller.callback = null + roomSettingsRecyclerView.cleanup() super.onDestroyView() } @@ -127,17 +156,6 @@ class RoomSettingsFragment @Inject constructor( invalidateOptionsMenu() } - override fun onEnableEncryptionClicked() { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.room_settings_enable_encryption_dialog_title) - .setMessage(R.string.room_settings_enable_encryption_dialog_content) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ -> - viewModel.handle(RoomSettingsAction.EnableEncryption) - } - .show() - } - override fun onNameChanged(name: String) { viewModel.handle(RoomSettingsAction.SetRoomName(name)) } @@ -147,35 +165,20 @@ class RoomSettingsFragment @Inject constructor( } override fun onHistoryVisibilityClicked() = withState(viewModel) { state -> - val historyVisibilities = arrayOf( - RoomHistoryVisibility.SHARED, - RoomHistoryVisibility.INVITED, - RoomHistoryVisibility.JOINED, - RoomHistoryVisibility.WORLD_READABLE - ) - val currentHistoryVisibility = - state.newHistoryVisibility ?: state.historyVisibilityEvent?.getClearContent().toModel()?.historyVisibility - val currentHistoryVisibilityIndex = historyVisibilities.indexOf(currentHistoryVisibility) - - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.room_settings_room_read_history_rules_pref_title) - setSingleChoiceItems( - historyVisibilities - .map { roomHistoryVisibilityFormatter.format(it) } - .toTypedArray(), - currentHistoryVisibilityIndex) { dialog, which -> - if (which != currentHistoryVisibilityIndex) { - viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(historyVisibilities[which])) - } - dialog.cancel() - } - show() - } - return@withState + val currentHistoryVisibility = state.newHistoryVisibility ?: state.currentHistoryVisibility + RoomHistoryVisibilityBottomSheet.newInstance(currentHistoryVisibility) + .show(childFragmentManager, "RoomHistoryVisibilityBottomSheet") } - override fun onAliasChanged(alias: String) { - viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias)) + override fun onRoomAliasesClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings) + } + + override fun onJoinRuleClicked() = withState(viewModel) { state -> + val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules + val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess + RoomJoinRuleBottomSheet.newInstance(currentJoinRule, currentGuestAccess) + .show(childFragmentManager, "RoomJoinRuleBottomSheet") } override fun onImageReady(uri: Uri?) { @@ -192,10 +195,10 @@ class RoomSettingsFragment @Inject constructor( override fun onAvatarDelete() { withState(viewModel) { when (it.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> { + RoomSettingsViewState.AvatarAction.None -> { viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar)) } - RoomSettingsViewState.AvatarAction.DeleteAvatar -> { + RoomSettingsViewState.AvatarAction.DeleteAvatar -> { /* Should not happen */ Unit } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 4e540f867e..48ff38f92e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -27,13 +27,15 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Completable import io.reactivex.Observable -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull 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 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.api.session.room.model.RoomGuestAccessContent +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.rx.mapOptional import org.matrix.android.sdk.rx.rx @@ -61,6 +63,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: init { observeRoomSummary() + observeRoomHistoryVisibility() + observeJoinRule() + observeGuestAccess() observeRoomAvatar() observeState() } @@ -69,14 +74,14 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: selectSubscribe( RoomSettingsViewState::avatarAction, RoomSettingsViewState::newName, - RoomSettingsViewState::newCanonicalAlias, RoomSettingsViewState::newTopic, RoomSettingsViewState::newHistoryVisibility, + RoomSettingsViewState::newRoomJoinRules, RoomSettingsViewState::roomSummary) { avatarAction, newName, - newCanonicalAlias, newTopic, newHistoryVisibility, + newJoinRule, asyncSummary -> val summary = asyncSummary() setState { @@ -84,8 +89,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None || summary?.name != newName || summary?.topic != newTopic - || summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() } - || newHistoryVisibility != null + || (newHistoryVisibility != null && newHistoryVisibility != currentHistoryVisibility) + || newJoinRule.hasChanged() ) } } @@ -97,11 +102,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: .execute { async -> val roomSummary = async.invoke() copy( - historyVisibilityEvent = room.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY), roomSummary = async, newName = roomSummary?.name, - newTopic = roomSummary?.topic, - newCanonicalAlias = roomSummary?.canonicalAlias + newTopic = roomSummary?.topic ) } @@ -114,17 +117,57 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR), canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME), canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC), - canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_CANONICAL_ALIAS), - canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_HISTORY_VISIBILITY), - canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_ROOM_JOIN_RULES) + && powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_ROOM_GUEST_ACCESS) ) setState { copy(actionPermissions = permissions) } } .disposeOnClear() } + private fun observeRoomHistoryVisibility() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + it.historyVisibility?.let { + setState { copy(currentHistoryVisibility = it) } + } + } + .disposeOnClear() + } + + private fun observeGuestAccess() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + it.joinRules?.let { + setState { copy(currentRoomJoinRules = it) } + } + } + .disposeOnClear() + } + + private fun observeJoinRule() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + it.guestAccess?.let { + setState { copy(currentGuestAccess = it) } + } + } + .disposeOnClear() + } + /** * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. */ @@ -141,17 +184,25 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: RoomSettingsAction) { when (action) { - is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() is RoomSettingsAction.SetAvatarAction -> handleSetAvatarAction(action) is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) } - is RoomSettingsAction.SetRoomCanonicalAlias -> setState { copy(newCanonicalAlias = action.newCanonicalAlias) } + is RoomSettingsAction.SetRoomJoinRule -> handleSetRoomJoinRule(action) is RoomSettingsAction.Save -> saveSettings() is RoomSettingsAction.Cancel -> cancel() }.exhaustive } + private fun handleSetRoomJoinRule(action: RoomSettingsAction.SetRoomJoinRule) = withState { state -> + setState { + copy(newRoomJoinRules = RoomSettingsViewState.NewJoinRule( + action.roomJoinRule.takeIf { it != state.currentRoomJoinRules }, + action.roomGuestAccess.takeIf { it != state.currentGuestAccess } + )) + } + } + private fun handleSetAvatarAction(action: RoomSettingsAction.SetAvatarAction) { setState { deletePendingAvatar(this) @@ -194,15 +245,14 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } - if (state.newCanonicalAlias != null && summary?.canonicalAlias != state.newCanonicalAlias.takeIf { it.isNotEmpty() }) { - operationList.add(room.rx().addRoomAlias(state.newCanonicalAlias)) - operationList.add(room.rx().updateCanonicalAlias(state.newCanonicalAlias)) - } - if (state.newHistoryVisibility != null) { operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility)) } + if (state.newRoomJoinRules.hasChanged()) { + operationList.add(room.rx().updateJoinRule(state.newRoomJoinRules.newJoinRules, state.newRoomJoinRules.newGuestAccess)) + } + Observable .fromIterable(operationList) .concatMapCompletable { it } @@ -213,7 +263,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: deletePendingAvatar(this) copy( avatarAction = RoomSettingsViewState.AvatarAction.None, - newHistoryVisibility = null + newHistoryVisibility = null, + newRoomJoinRules = RoomSettingsViewState.NewJoinRule() ) } _viewEvents.post(RoomSettingsViewEvents.Success) @@ -225,21 +276,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: ) } - private fun handleEnableEncryption() { - postLoading(true) - - room.enableEncryption(callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - postLoading(false) - _viewEvents.post(RoomSettingsViewEvents.Failure(failure)) - } - - override fun onSuccess(data: Unit) { - postLoading(false) - } - }) - } - private fun postLoading(isLoading: Boolean) { setState { copy(isLoading = isLoading) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index f913bed382..7403917d48 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -21,13 +21,17 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.GuestAccess 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.RoomSummary data class RoomSettingsViewState( val roomId: String, - val historyVisibilityEvent: Event? = null, + // Default value: https://matrix.org/docs/spec/client_server/r0.6.1#id88 + val currentHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.SHARED, + val currentRoomJoinRules: RoomJoinRules = RoomJoinRules.INVITE, + val currentGuestAccess: GuestAccess? = null, val roomSummary: Async = Uninitialized, val isLoading: Boolean = false, val currentRoomAvatarUrl: String? = null, @@ -35,7 +39,7 @@ data class RoomSettingsViewState( val newName: String? = null, val newTopic: String? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, - val newCanonicalAlias: String? = null, + val newRoomJoinRules: NewJoinRule = NewJoinRule(), val showSaveAction: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { @@ -46,9 +50,8 @@ data class RoomSettingsViewState( val canChangeAvatar: Boolean = false, val canChangeName: Boolean = false, val canChangeTopic: Boolean = false, - val canChangeCanonicalAlias: Boolean = false, - val canChangeHistoryReadability: Boolean = false, - val canEnableEncryption: Boolean = false + val canChangeHistoryVisibility: Boolean = false, + val canChangeJoinRule: Boolean = false ) sealed class AvatarAction { @@ -57,4 +60,11 @@ data class RoomSettingsViewState( data class UpdateAvatar(val newAvatarUri: Uri, val newAvatarFileName: String) : AvatarAction() } + + data class NewJoinRule( + val newJoinRules: RoomJoinRules? = null, + val newGuestAccess: GuestAccess? = null + ) { + fun hasChanged() = newJoinRules != null || newGuestAccess != null + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityAction.kt new file mode 100644 index 0000000000..3c989a7dbe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityAction.kt @@ -0,0 +1,33 @@ +/* + * 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.roomprofile.settings.historyvisibility + +import androidx.annotation.DrawableRes +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericAction +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility + +class RoomHistoryVisibilityAction( + val roomHistoryVisibility: RoomHistoryVisibility, + title: String, + @DrawableRes iconResId: Int, + isSelected: Boolean +) : BottomSheetGenericAction( + title = title, + iconResId = iconResId, + isSelected = isSelected, + destructive = false +) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt new file mode 100644 index 0000000000..c12dc621a9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt @@ -0,0 +1,70 @@ +/* + * 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.roomprofile.settings.historyvisibility + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.ui.bottomsheet.BottomSheetGeneric +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import javax.inject.Inject + +@Parcelize +data class RoomHistoryVisibilityBottomSheetArgs( + val currentRoomHistoryVisibility: RoomHistoryVisibility +) : Parcelable + +class RoomHistoryVisibilityBottomSheet : BottomSheetGeneric() { + + private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel + @Inject lateinit var controller: RoomHistoryVisibilityController + private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getController(): BottomSheetGenericController = controller + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + roomHistoryVisibilitySharedActionViewModel = activityViewModelProvider.get(RoomHistoryVisibilitySharedActionViewModel::class.java) + } + + override fun didSelectAction(action: RoomHistoryVisibilityAction) { + roomHistoryVisibilitySharedActionViewModel.post(action) + dismiss() + } + + override fun invalidate() = withState(viewModel) { + controller.setData(it) + super.invalidate() + } + + companion object { + fun newInstance(currentRoomHistoryVisibility: RoomHistoryVisibility): RoomHistoryVisibilityBottomSheet { + return RoomHistoryVisibilityBottomSheet().apply { + setArguments(RoomHistoryVisibilityBottomSheetArgs(currentRoomHistoryVisibility)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityController.kt new file mode 100644 index 0000000000..a4899711f7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityController.kt @@ -0,0 +1,51 @@ +/* + * 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.roomprofile.settings.historyvisibility + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import javax.inject.Inject + +class RoomHistoryVisibilityController @Inject constructor( + private val historyVisibilityFormatter: RoomHistoryVisibilityFormatter, + private val stringProvider: StringProvider +) : BottomSheetGenericController() { + + override fun getTitle() = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_dialog_title) + + override fun getSubTitle() = stringProvider.getString(R.string.room_settings_room_read_history_dialog_subtitle) + + override fun getActions(state: RoomHistoryVisibilityState): List { + return listOf( + RoomHistoryVisibility.WORLD_READABLE, + RoomHistoryVisibility.SHARED, + RoomHistoryVisibility.INVITED, + RoomHistoryVisibility.JOINED + ) + .map { roomHistoryVisibility -> + RoomHistoryVisibilityAction( + roomHistoryVisibility = roomHistoryVisibility, + title = historyVisibilityFormatter.getSetting(roomHistoryVisibility), + iconResId = 0, + isSelected = roomHistoryVisibility == state.currentRoomHistoryVisibility + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt new file mode 100644 index 0000000000..31c1c2631c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt @@ -0,0 +1,23 @@ +/* + * 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.roomprofile.settings.historyvisibility + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class RoomHistoryVisibilitySharedActionViewModel @Inject constructor() + : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityState.kt new file mode 100644 index 0000000000..0b651d5664 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityState.kt @@ -0,0 +1,27 @@ +/* + * 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.roomprofile.settings.historyvisibility + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericState +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility + +data class RoomHistoryVisibilityState( + val currentRoomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.SHARED +) : BottomSheetGenericState() { + + constructor(args: RoomHistoryVisibilityBottomSheetArgs) : this(currentRoomHistoryVisibility = args.currentRoomHistoryVisibility) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt new file mode 100644 index 0000000000..c2a8ae967f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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.roomprofile.settings.historyvisibility + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel + +class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState) + : BottomSheetGenericViewModel(initialState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAction.kt new file mode 100644 index 0000000000..6f71669002 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAction.kt @@ -0,0 +1,35 @@ +/* + * 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.roomprofile.settings.joinrule + +import androidx.annotation.DrawableRes +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericAction +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules + +class RoomJoinRuleAction( + val roomJoinRule: RoomJoinRules, + val roomGuestAccess: GuestAccess?, + title: String, + @DrawableRes iconResId: Int, + isSelected: Boolean +) : BottomSheetGenericAction( + title = title, + iconResId = iconResId, + isSelected = isSelected, + destructive = false +) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt new file mode 100644 index 0000000000..66c6be6086 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt @@ -0,0 +1,72 @@ +/* + * 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.roomprofile.settings.joinrule + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.ui.bottomsheet.BottomSheetGeneric +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +@Parcelize +data class RoomJoinRuleBottomSheetArgs( + val currentRoomJoinRule: RoomJoinRules, + val currentGuestAccess: GuestAccess? +) : Parcelable + +class RoomJoinRuleBottomSheet : BottomSheetGeneric() { + + private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel + @Inject lateinit var controller: RoomJoinRuleController + private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getController(): BottomSheetGenericController = controller + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) + } + + override fun didSelectAction(action: RoomJoinRuleAction) { + roomJoinRuleSharedActionViewModel.post(action) + dismiss() + } + + override fun invalidate() = withState(viewModel) { + controller.setData(it) + super.invalidate() + } + + companion object { + fun newInstance(currentRoomJoinRule: RoomJoinRules, currentGuestAccess: GuestAccess?): RoomJoinRuleBottomSheet { + return RoomJoinRuleBottomSheet().apply { + setArguments(RoomJoinRuleBottomSheetArgs(currentRoomJoinRule, currentGuestAccess)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt new file mode 100644 index 0000000000..ab00396dbe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt @@ -0,0 +1,57 @@ +/* + * 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.roomprofile.settings.joinrule + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +class RoomJoinRuleController @Inject constructor( + private val stringProvider: StringProvider +) : BottomSheetGenericController() { + + override fun getTitle() = stringProvider.getString(R.string.room_settings_room_access_rules_pref_dialog_title) + + override fun getActions(state: RoomJoinRuleState): List { + return listOf( + RoomJoinRuleAction( + roomJoinRule = RoomJoinRules.INVITE, + roomGuestAccess = null, + title = stringProvider.getString(R.string.room_settings_room_access_entry_only_invited), + iconResId = 0, + isSelected = state.currentRoomJoinRule == RoomJoinRules.INVITE + ), + RoomJoinRuleAction( + roomJoinRule = RoomJoinRules.PUBLIC, + roomGuestAccess = GuestAccess.Forbidden, + title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_apart_guest), + iconResId = 0, + isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC && state.currentGuestAccess == GuestAccess.Forbidden + ), + RoomJoinRuleAction( + roomJoinRule = RoomJoinRules.PUBLIC, + roomGuestAccess = GuestAccess.CanJoin, + title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_including_guest), + iconResId = 0, + isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC && state.currentGuestAccess == GuestAccess.CanJoin + ) + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt new file mode 100644 index 0000000000..934b0dfc76 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt @@ -0,0 +1,23 @@ +/* + * 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.roomprofile.settings.joinrule + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class RoomJoinRuleSharedActionViewModel @Inject constructor() + : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt new file mode 100644 index 0000000000..ec16b02d60 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt @@ -0,0 +1,32 @@ +/* + * 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.roomprofile.settings.joinrule + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericState +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules + +data class RoomJoinRuleState( + val currentRoomJoinRule: RoomJoinRules = RoomJoinRules.INVITE, + val currentGuestAccess: GuestAccess? = null +) : BottomSheetGenericState() { + + constructor(args: RoomJoinRuleBottomSheetArgs) : this( + currentRoomJoinRule = args.currentRoomJoinRule, + currentGuestAccess = args.currentGuestAccess + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt new file mode 100644 index 0000000000..4305bfa72d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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.roomprofile.settings.joinrule + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel + +class RoomJoinRuleViewModel(initialState: RoomJoinRuleState) + : BottomSheetGenericViewModel(initialState) 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 76b1a9e0c3..763eed5474 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 @@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.getFileUrl -import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx @@ -90,9 +89,7 @@ class RoomUploadsViewModel @AssistedInject constructor( viewModelScope.launch { try { - val result = awaitCallback { - room.getUploads(20, token, it) - } + val result = room.getUploads(20, token) token = result.nextToken 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 295bb01265..9d6ed0246c 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 @@ -24,6 +24,7 @@ import com.squareup.seismic.ShakeDetector import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.features.disclaimer.SHARED_PREF_KEY import im.vector.app.features.homeserver.ServerUrlsRepository import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.extensions.tryOrNull @@ -164,6 +165,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { // Security const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE" const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG" + const val SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG = "SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG" private const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG" private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG" const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" @@ -248,6 +250,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { // theme keysToKeep.add(ThemeUtils.APPLICATION_THEME_KEY) + // Disclaimer dialog + keysToKeep.add(SHARED_PREF_KEY) + // get all the existing keys val keys = defaultPrefs.all.keys diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index 67b5c03638..8d9f8d7170 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -15,12 +15,13 @@ */ package im.vector.app.features.settings +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import im.vector.app.R import im.vector.app.core.preference.PushRulePreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.utils.toast -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind import javax.inject.Inject @@ -50,29 +51,25 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor() if (newRule != null) { displayLoadingView() - session.updatePushRuleActions( - ruleAndKind.kind, - preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule, - newRule, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - if (!isAdded) { - return - } - preference.setPushRule(ruleAndKind.copy(pushRule = newRule)) - hideLoadingView() - } - - override fun onFailure(failure: Throwable) { - if (!isAdded) { - return - } - hideLoadingView() - // Restore the previous value - refreshDisplay() - activity?.toast(errorFormatter.toHumanReadable(failure)) - } - }) + lifecycleScope.launch { + val result = runCatching { + session.updatePushRuleActions(ruleAndKind.kind, + preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule, + newRule) + } + if (!isAdded) { + return@launch + } + hideLoadingView() + result.onSuccess { + preference.setPushRule(ruleAndKind.copy(pushRule = newRule)) + } + result.onFailure { failure -> + // Restore the previous value + refreshDisplay() + activity?.toast(errorFormatter.toHumanReadable(failure)) + } + } } false } 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 b1ccabfb76..5a7ceb4084 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 @@ -58,7 +58,6 @@ 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.NoOpMatrixCallback 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 @@ -214,7 +213,9 @@ class VectorSettingsGeneralFragment @Inject constructor( it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> // Disable it while updating the state, will be re-enabled by the account data listener. it.isEnabled = false - session.integrationManagerService().setIntegrationEnabled(newValue as Boolean, NoOpMatrixCallback()) + lifecycleScope.launch { + session.integrationManagerService().setIntegrationEnabled(newValue as Boolean) + } true } } 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 861e0dea1f..c9160b8ebc 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 @@ -16,15 +16,13 @@ package im.vector.app.features.settings -import android.content.Intent -import android.net.Uri -import android.provider.Settings 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.copyToClipboard import im.vector.app.core.utils.displayInWebView +import im.vector.app.core.utils.openAppSettingsPage import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.version.VersionProvider import im.vector.app.openOssLicensesMenuActivity @@ -42,18 +40,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor( // preference to start the App info screen, to facilitate App permissions access findPreference(APP_INFO_LINK_PREFERENCE_KEY)!! .onPreferenceClickListener = Preference.OnPreferenceClickListener { - activity?.let { - val intent = Intent().apply { - action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - val uri = Uri.fromParts("package", requireContext().packageName, null) - - data = uri - } - it.applicationContext.startActivity(intent) - } - + activity?.let { openAppSettingsPage(it) } true } 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 4bee1ac0c8..47868eed51 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 @@ -23,6 +23,7 @@ import android.media.RingtoneManager import android.net.Uri import android.os.Parcelable import android.widget.Toast +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.app.R @@ -37,6 +38,7 @@ import im.vector.app.core.utils.isIgnoringBatteryOptimizations 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 @@ -318,24 +320,22 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( .find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL } ?.let { // Trick, we must enable this room to disable notifications - pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, - it, - !switchPref.isChecked, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - // Push rules will be updated from the sync - } + lifecycleScope.launch { + try { + pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, + it, + !switchPref.isChecked) + // Push rules will be updated from the sync + } catch (failure: Throwable) { + if (!isAdded) { + return@launch + } - 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() - } - }) + // 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/VectorSettingsPinFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt index 37465258f6..1a04dab950 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt @@ -21,6 +21,7 @@ import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.preference.VectorPreference import im.vector.app.features.navigation.Navigator import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.pin.PinCodeStore @@ -41,6 +42,10 @@ class VectorSettingsPinFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!! } + private val changePinCodePref by lazy { + findPreference(VectorPreferences.SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG)!! + } + private val useCompleteNotificationPref by lazy { findPreference(VectorPreferences.SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG)!! } @@ -74,6 +79,17 @@ class VectorSettingsPinFragment @Inject constructor( } true } + + changePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (hasPinCode) { + navigator.openPinCode( + requireContext(), + pinActivityResultLauncher, + PinMode.MODIFY + ) + } + true + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index ebeac5aca1..f21ec2e8f4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -68,12 +68,12 @@ class CrossSigningSettingsFragment @Inject constructor( } private fun setupRecyclerView() { - recyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + genericRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) controller.interactionListener = this } override fun onDestroyView() { - recyclerView.cleanup() + genericRecyclerView.cleanup() controller.interactionListener = null super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index ae45989a81..a317536d5d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -61,7 +61,7 @@ class VectorSettingsDevicesFragment @Inject constructor( waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.isVisible = true devicesController.callback = this - recyclerView.configureWith(devicesController, showDivider = true) + genericRecyclerView.configureWith(devicesController, showDivider = true) viewModel.observeViewEvents { when (it) { is DevicesViewEvents.Loading -> showLoading(it.message) @@ -97,7 +97,7 @@ class VectorSettingsDevicesFragment @Inject constructor( override fun onDestroyView() { devicesController.callback = null - recyclerView.cleanup() + genericRecyclerView.cleanup() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt index 07508f41a2..40b910c1ab 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt @@ -57,13 +57,13 @@ class AccountDataFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) epoxyController.interactionListener = this } override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericRecyclerView.cleanup() epoxyController.interactionListener = null } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index 0ceb8e148d..af8881ba92 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -50,13 +50,13 @@ class GossipingEventsPaperTrailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) epoxyController.interactionListener = this } override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericRecyclerView.cleanup() epoxyController.interactionListener = null } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt index 35f46d9c74..6e205ceceb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt @@ -45,11 +45,11 @@ class IncomingKeyRequestListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) } override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericRecyclerView.cleanup() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt index a82b5dd6c9..20132d8047 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt @@ -41,13 +41,13 @@ class OutgoingKeyRequestListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) // epoxyController.interactionListener = this } override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericRecyclerView.cleanup() // epoxyController.interactionListener = null } } diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt index 2588eef59b..5ad7258cec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt @@ -49,7 +49,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor( waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.isVisible = true ignoredUsersController.callback = this - recyclerView.configureWith(ignoredUsersController) + genericRecyclerView.configureWith(ignoredUsersController) viewModel.observeViewEvents { when (it) { is IgnoredUsersViewEvents.Loading -> showLoading(it.message) @@ -60,7 +60,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor( override fun onDestroyView() { ignoredUsersController.callback = null - recyclerView.cleanup() + genericRecyclerView.cleanup() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt index e6e9ce3753..0075d8ef5a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt @@ -59,11 +59,11 @@ class PushGatewaysFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) } override fun onDestroyView() { - recyclerView.cleanup() + genericRecyclerView.cleanup() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt index c361e21254..c5ad04380b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt @@ -43,11 +43,11 @@ class PushRulesFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController, showDivider = true) + genericRecyclerView.configureWith(epoxyController, showDivider = true) } override fun onDestroyView() { - recyclerView.cleanup() + genericRecyclerView.cleanup() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index 81033281d8..12ff51dcbd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -54,7 +54,7 @@ class ThreePidsSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.configureWith(epoxyController) + genericRecyclerView.configureWith(epoxyController) epoxyController.interactionListener = this viewModel.observeViewEvents { @@ -73,7 +73,7 @@ class ThreePidsSettingsFragment @Inject constructor( override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericRecyclerView.cleanup() epoxyController.interactionListener = null } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt index 0c3390d0b0..b78dba07f5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt @@ -20,7 +20,8 @@ import androidx.activity.result.ActivityResultLauncher import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleKind import javax.inject.Inject @@ -48,16 +49,12 @@ class TestAccountSettings @Inject constructor(private val stringProvider: String override fun doFix() { if (manager?.diagStatus == TestStatus.RUNNING) return // wait before all is finished - session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - manager?.retry(activityResultLauncher) - } - - override fun onFailure(failure: Throwable) { - manager?.retry(activityResultLauncher) - } - }) + GlobalScope.launch { + runCatching { + session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled) + } + manager?.retry(activityResultLauncher) + } } } status = TestStatus.FAILED diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index 64b71356ec..dbd5028401 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -70,12 +70,12 @@ class SoftLogoutFragment @Inject constructor( } private fun setupRecyclerView() { - recyclerView.configureWith(softLogoutController) + genericRecyclerView.configureWith(softLogoutController) softLogoutController.listener = this } override fun onDestroyView() { - recyclerView.cleanup() + genericRecyclerView.cleanup() softLogoutController.listener = null super.onDestroyView() } @@ -121,7 +121,7 @@ class SoftLogoutFragment @Inject constructor( } private fun cleanupUi() { - recyclerView.hideKeyboard() + genericRecyclerView.hideKeyboard() } override fun forgetPasswordClicked() { diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt index df822807ee..89d6e970cc 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt @@ -28,8 +28,6 @@ import im.vector.app.core.extensions.exhaustive 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.terms.GetTermsResponse -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber class ReviewTermsViewModel @AssistedInject constructor( @@ -94,15 +92,12 @@ class ReviewTermsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { - session.agreeToTerms( - termsArgs.type, - termsArgs.baseURL, - agreedUrls, - termsArgs.token, - it - ) - } + session.agreeToTerms( + termsArgs.type, + termsArgs.baseURL, + agreedUrls, + termsArgs.token + ) _viewEvents.post(ReviewTermsViewEvents.Success) } catch (failure: Throwable) { Timber.e(failure, "Failed to agree to terms") @@ -122,9 +117,7 @@ class ReviewTermsViewModel @AssistedInject constructor( viewModelScope.launch { try { - val data = awaitCallback { - session.getTerms(termsArgs.type, termsArgs.baseURL, it) - } + val data = session.getTerms(termsArgs.type, termsArgs.baseURL) val terms = data.serverResponse.getLocalizedTerms(action.preferredLanguageCode).map { Term(it.localizedUrl ?: "", it.localizedName ?: "", 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 3aba6a4dad..847caeab4c 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 @@ -24,23 +24,19 @@ import im.vector.app.R * Note that style for light theme is default and is declared in the Android Manifest */ sealed class ActivityOtherThemes(@StyleRes val dark: Int, - @StyleRes val black: Int, - @StyleRes val status: Int) { + @StyleRes val black: Int) { object Default : ActivityOtherThemes( R.style.AppTheme_Dark, - R.style.AppTheme_Black, - R.style.AppTheme_Status + R.style.AppTheme_Black ) object AttachmentsPreview : ActivityOtherThemes( - R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview ) object VectorAttachmentsPreview : ActivityOtherThemes( - R.style.AppTheme_Transparent, R.style.AppTheme_Transparent, R.style.AppTheme_Transparent ) diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index 18faa07954..bba6b9c253 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -24,6 +24,7 @@ import android.view.Menu import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.core.content.ContextCompat +import androidx.core.content.edit import androidx.core.graphics.drawable.DrawableCompat import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences @@ -41,7 +42,6 @@ object ThemeUtils { private const val THEME_DARK_VALUE = "dark" private const val THEME_LIGHT_VALUE = "light" private const val THEME_BLACK_VALUE = "black" - private const val THEME_STATUS_VALUE = "status" private var currentTheme = AtomicReference(null) @@ -58,9 +58,8 @@ object ThemeUtils { */ fun isLightTheme(context: Context): Boolean { return when (getApplicationTheme(context)) { - THEME_LIGHT_VALUE, - THEME_STATUS_VALUE -> true - else -> false + THEME_LIGHT_VALUE -> true + else -> false } } @@ -73,8 +72,13 @@ object ThemeUtils { fun getApplicationTheme(context: Context): String { val currentTheme = this.currentTheme.get() return if (currentTheme == null) { - val themeFromPref = DefaultSharedPreferences.getInstance(context) - .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + val prefs = DefaultSharedPreferences.getInstance(context) + var themeFromPref = prefs.getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + if (themeFromPref == "status") { + // Migrate to light theme, which is the closest theme + themeFromPref = THEME_LIGHT_VALUE + prefs.edit { putString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) } + } this.currentTheme.set(themeFromPref) themeFromPref } else { @@ -92,7 +96,6 @@ object ThemeUtils { when (aTheme) { THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) - THEME_STATUS_VALUE -> context.setTheme(R.style.AppTheme_Status) else -> context.setTheme(R.style.AppTheme_Light) } @@ -109,7 +112,6 @@ object ThemeUtils { when (getApplicationTheme(activity)) { THEME_DARK_VALUE -> activity.setTheme(otherThemes.dark) THEME_BLACK_VALUE -> activity.setTheme(otherThemes.black) - THEME_STATUS_VALUE -> activity.setTheme(otherThemes.status) } mColorByAttr.clear() diff --git a/vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt b/vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt new file mode 100644 index 0000000000..178a283d2c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt @@ -0,0 +1,85 @@ +/* + * 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.usercode + +import android.graphics.Bitmap +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.LuminanceSource +import com.google.zxing.MultiFormatReader +import com.google.zxing.RGBLuminanceSource +import com.google.zxing.ReaderException +import com.google.zxing.Result +import com.google.zxing.common.HybridBinarizer + +// Some helper code from BinaryEye +object QRCodeBitmapDecodeHelper { + + private val multiFormatReader = MultiFormatReader() + private val decoderHints = mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)) + + fun decodeQRFromBitmap(bitmap: Bitmap): Result? = + decode(bitmap, false) ?: decode(bitmap, true) + + private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? { + val pixels = IntArray(bitmap.width * bitmap.height) + return decode(pixels, bitmap, invert) + } + + private fun decode( + pixels: IntArray, + bitmap: Bitmap, + invert: Boolean = false + ): Result? { + val width = bitmap.width + val height = bitmap.height + if (bitmap.config != Bitmap.Config.ARGB_8888) { + bitmap.copy(Bitmap.Config.ARGB_8888, true) + } else { + bitmap + }.getPixels(pixels, 0, width, 0, 0, width, height) + return decodeLuminanceSource( + RGBLuminanceSource(width, height, pixels), + invert + ) + } + + private fun decodeLuminanceSource( + source: LuminanceSource, + invert: Boolean + ): Result? { + return decodeLuminanceSource( + if (invert) { + source.invert() + } else { + source + } + ) + } + + private fun decodeLuminanceSource(source: LuminanceSource): Result? { + val bitmap = BinaryBitmap(HybridBinarizer(source)) + return try { + multiFormatReader.decode(bitmap, decoderHints) + } catch (e: ReaderException) { + null + } finally { + multiFormatReader.reset() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt new file mode 100644 index 0000000000..782d7e1c04 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt @@ -0,0 +1,148 @@ +/* + * 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.usercode + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.airbnb.mvrx.activityViewModel +import com.google.zxing.Result +import com.google.zxing.ResultMetadataType +import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.lib.multipicker.MultiPicker +import im.vector.lib.multipicker.utils.ImageUtils +import kotlinx.android.synthetic.main.fragment_qr_code_scanner_with_button.* +import me.dm7.barcodescanner.zxing.ZXingScannerView +import org.matrix.android.sdk.api.extensions.tryOrNull +import javax.inject.Inject + +class ScanUserCodeFragment @Inject constructor() + : VectorBaseFragment(), + ZXingScannerView.ResultHandler { + + override fun getLayoutResId() = R.layout.fragment_qr_code_scanner_with_button + + val sharedViewModel: UserCodeSharedViewModel by activityViewModel() + + var autoFocus = true + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + userCodeMyCodeButton.debouncedClicks { + sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) + } + + userCodeOpenGalleryButton.debouncedClicks { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) + } + } + + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + if (allGranted) { + startCamera() + } else { + // For now just go back + sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) + } + } + + private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(requireActivity(), activityResult.data) + .firstOrNull() + ?.contentUri + ?.let { uri -> + // try to see if it is a valid matrix code + val bitmap = ImageUtils.getBitmap(requireContext(), uri) + ?: return@let Unit.also { + Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show() + } + handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) }) + } + } + } + + private fun startCamera() { + userCodeScannerView.startCamera() + userCodeScannerView.setAutoFocus(autoFocus) + userCodeScannerView.debouncedClicks { + this.autoFocus = !autoFocus + userCodeScannerView.setAutoFocus(autoFocus) + } + } + + override fun onStart() { + super.onStart() + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { + startCamera() + } + } + + override fun onResume() { + super.onResume() + // Register ourselves as a handler for scan results. + userCodeScannerView.setResultHandler(this) + if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)) { + startCamera() + } + } + + override fun onPause() { + super.onPause() + userCodeScannerView.setResultHandler(null) + // Stop camera on pause + userCodeScannerView.stopCamera() + } + + override fun handleResult(result: Result?) { + if (result === null) { + Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() + requireActivity().finish() + } else { + val rawBytes = getRawBytes(result) + val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) + val value = rawBytesStr ?: result.text + sharedViewModel.handle(UserCodeActions.DecodedQRCode(value)) + } + } + + // Copied from https://github.com/markusfisch/BinaryEye/blob/ + // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434 + private fun getRawBytes(result: Result): ByteArray? { + val metadata = result.resultMetadata ?: return null + val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null + var bytes = ByteArray(0) + @Suppress("UNCHECKED_CAST") + for (seg in segments as Iterable) { + bytes += seg + } + // byte segments can never be shorter than the text. + // Zxing cuts off content prefixes like "WIFI:" + return if (bytes.size >= result.text.length) bytes else null + } +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt new file mode 100644 index 0000000000..db6d636b9a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt @@ -0,0 +1,87 @@ +/* + * 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.usercode + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.core.utils.startSharePlainTextIntent +import im.vector.app.features.home.AvatarRenderer +import kotlinx.android.synthetic.main.fragment_user_code_show.* +import javax.inject.Inject + +class ShowUserCodeFragment @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_user_code_show + + val sharedViewModel: UserCodeSharedViewModel by activityViewModel() + + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + if (allGranted) { + doOpenQRCodeScanner() + } else { + sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + showUserCodeClose.debouncedClicks { + sharedViewModel.handle(UserCodeActions.DismissAction) + } + showUserCodeScanButton.debouncedClicks { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { + doOpenQRCodeScanner() + } + } + showUserCodeShareButton.debouncedClicks { + sharedViewModel.handle(UserCodeActions.ShareByText) + } + + sharedViewModel.observeViewEvents { + if (it is UserCodeShareViewEvents.SharePlainText) { + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = it.title, + text = it.text, + extraTitle = it.richPlainText + ) + } + } + } + + private fun doOpenQRCodeScanner() { + sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SCAN)) + } + + override fun invalidate() = withState(sharedViewModel) { state -> + state.matrixItem?.let { avatarRenderer.render(it, showUserCodeAvatar) } + state.shareLink?.let { showUserCodeQRImage.setData(it) } + showUserCodeCardNameText.setTextOrHide(state.matrixItem?.displayName) + showUserCodeCardUserIdText.setTextOrHide(state.matrixItem?.id) + } +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt new file mode 100644 index 0000000000..3411fe3d7f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt @@ -0,0 +1,29 @@ +/* + * 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.usercode + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.util.MatrixItem + +sealed class UserCodeActions : VectorViewModelAction { + object DismissAction : UserCodeActions() + data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions() + data class DecodedQRCode(val code: String) : UserCodeActions() + data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions() + object CameraPermissionNotGranted : UserCodeActions() + object ShareByText : UserCodeActions() +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt new file mode 100644 index 0000000000..547e2d939f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -0,0 +1,128 @@ +/* + * 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.usercode + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.utils.onPermissionDeniedSnackbar +import im.vector.app.features.matrixto.MatrixToBottomSheet +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.activity_simple.* +import javax.inject.Inject +import kotlin.reflect.KClass + +class UserCodeActivity + : VectorBaseActivity(), UserCodeSharedViewModel.Factory, MatrixToBottomSheet.InteractionListener { + + @Inject lateinit var viewModelFactory: UserCodeSharedViewModel.Factory + + val sharedViewModel: UserCodeSharedViewModel by viewModel() + + @Parcelize + data class Args( + val userId: String + ) : Parcelable + + override fun getLayoutRes() = R.layout.activity_simple + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFirstCreation()) { + // should be there early for shared element transition + showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) + } + + sharedViewModel.selectSubscribe(this, UserCodeState::mode) { mode -> + when (mode) { + UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) + UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) + is UserCodeState.Mode.RESULT -> { + showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) + MatrixToBottomSheet.withLink(mode.rawLink, this).show(supportFragmentManager, "MatrixToBottomSheet") + } + } + } + + sharedViewModel.observeViewEvents { + when (it) { + UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this) + UserCodeShareViewEvents.ShowWaitingScreen -> simpleActivityWaitingView.isVisible = true + UserCodeShareViewEvents.HideWaitingScreen -> simpleActivityWaitingView.isVisible = false + is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() + is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId) + UserCodeShareViewEvents.CameraPermissionNotGranted -> onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) + else -> { + } + } + } + } + + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + replace(R.id.simpleFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } + } + + override fun navigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) + } + + override fun onBackPressed() = withState(sharedViewModel) { + when (it.mode) { + UserCodeState.Mode.SHOW -> super.onBackPressed() + is UserCodeState.Mode.RESULT, + UserCodeState.Mode.SCAN -> sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) + }.exhaustive + } + + override fun create(initialState: UserCodeState) = + viewModelFactory.create(initialState) + + companion object { + fun newIntent(context: Context, userId: String): Intent { + return Intent(context, UserCodeActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, Args(userId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt new file mode 100644 index 0000000000..67a1ab8a6c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt @@ -0,0 +1,29 @@ +/* + * 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.usercode + +import im.vector.app.core.platform.VectorViewEvents + +sealed class UserCodeShareViewEvents : VectorViewEvents { + object Dismiss : UserCodeShareViewEvents() + object ShowWaitingScreen : UserCodeShareViewEvents() + object HideWaitingScreen : UserCodeShareViewEvents() + data class ToastMessage(val message: String) : UserCodeShareViewEvents() + data class NavigateToRoom(val roomId: String) : UserCodeShareViewEvents() + object CameraPermissionNotGranted : UserCodeShareViewEvents() + data class SharePlainText(val text: String, val title: String, val richPlainText: String) : UserCodeShareViewEvents() +} 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 new file mode 100644 index 0000000000..45b6f0ee65 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -0,0 +1,174 @@ +/* + * 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.usercode + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isE2EByDefault +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +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.room.model.create.CreateRoomParams +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, + private val session: Session, + private val stringProvider: StringProvider, + private val rawService: RawService) : VectorViewModel(initialState) { + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: UserCodeState): UserCodeSharedViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + val user = session.getUser(initialState.userId) + setState { + copy( + matrixItem = user?.toMatrixItem(), + shareLink = session.permalinkService().createPermalink(initialState.userId) + ) + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: UserCodeState): UserCodeSharedViewModel + } + + override fun handle(action: UserCodeActions) { + when (action) { + UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss) + is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } + is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) + is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) + UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted) + UserCodeActions.ShareByText -> handleShareByText() + } + } + + private fun handleShareByText() { + session.permalinkService().createPermalink(session.myUserId)?.let { permalink -> + val text = stringProvider.getString(R.string.invite_friends_text, permalink) + _viewEvents.post(UserCodeShareViewEvents.SharePlainText( + text, + stringProvider.getString(R.string.invite_friends), + stringProvider.getString(R.string.invite_friends_rich_title) + )) + } + } + + private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) { + val mxId = withUser.matrixItem.id + val existing = session.getExistingDirectRoomWithUser(mxId) + setState { + copy(mode = UserCodeState.Mode.SHOW) + } + if (existing != null) { + // navigate to this room + _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing)) + } else { + // we should create the room then navigate + _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) + viewModelScope.launch(Dispatchers.IO) { + val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) + ?.isE2EByDefault() + ?: true + + val roomParams = CreateRoomParams() + .apply { + invitedUserIds.add(mxId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault + } + + val roomId = + try { + awaitCallback { session.createRoom(roomParams, it) } + } catch (failure: Throwable) { + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure))) + return@launch + } finally { + _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen) + } + _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId)) + } + } + } + + private fun handleQrCodeDecoded(action: UserCodeActions.DecodedQRCode) { + val linkedId = PermalinkParser.parse(action.code) + if (linkedId is PermalinkData.FallbackLink) { + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_a_valid_qr_code))) + return + } + _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) + viewModelScope.launch(Dispatchers.IO) { + when (linkedId) { + is PermalinkData.RoomLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } + is PermalinkData.UserLink -> { + val user = tryOrNull { + awaitCallback { + session.resolveUser(linkedId.userId, it) + } + } + // Create raw Uxid in case the user is not searchable + ?: User(linkedId.userId, null, null) + + setState { + copy( + mode = UserCodeState.Mode.RESULT(user.toMatrixItem(), action.code) + ) + } + } + is PermalinkData.GroupLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } + is PermalinkData.FallbackLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } + } + _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt new file mode 100644 index 0000000000..c26da7c0a4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt @@ -0,0 +1,37 @@ +/* + * 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.usercode + +import com.airbnb.mvrx.MvRxState +import org.matrix.android.sdk.api.util.MatrixItem + +data class UserCodeState( + val userId: String, + val matrixItem: MatrixItem? = null, + val shareLink: String? = null, + val mode: Mode = Mode.SHOW +) : MvRxState { + sealed class Mode { + object SHOW : Mode() + object SCAN : Mode() + data class RESULT(val matrixItem: MatrixItem, val rawLink: String) : Mode() + } + + constructor(args: UserCodeActivity.Args) : this( + userId = args.userId + ) +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt new file mode 100644 index 0000000000..afbc523db2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt @@ -0,0 +1,54 @@ +/* + * 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.userdirectory + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.utils.DebouncedClickListener + +@EpoxyModelClass(layout = R.layout.item_contact_action) +abstract class ActionItem : VectorEpoxyModel() { + + @EpoxyAttribute var title: CharSequence? = null + @EpoxyAttribute @DrawableRes var actionIconRes: Int? = null + @EpoxyAttribute var clickAction: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.setOnClickListener(clickAction?.let { DebouncedClickListener(it) }) + // If name is empty, use userId as name and force it being centered + holder.actionTitleText.setTextOrHide(title) + if (actionIconRes != null) { + holder.actionTitleImageView.setImageResource(actionIconRes!!) + } else { + holder.actionTitleImageView.setImageDrawable(null) + } + } + + class Holder : VectorEpoxyHolder() { + val actionTitleText by bind(R.id.actionTitleText) + val actionTitleImageView by bind(R.id.actionIconImageView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt new file mode 100644 index 0000000000..ee96c34f45 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt @@ -0,0 +1,47 @@ +/* + * 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.userdirectory + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_contact_detail) +abstract class ContactDetailItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var threePid: String + @EpoxyAttribute var matrixId: String? = null + @EpoxyAttribute var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.onClick(clickListener) + holder.nameView.text = threePid + holder.matrixIdView.setTextOrHide(matrixId) + } + + class Holder : VectorEpoxyHolder() { + val nameView by bind(R.id.contactDetailName) + val matrixIdView by bind(R.id.contactDetailMatrixId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt new file mode 100644 index 0000000000..d9f424d961 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt @@ -0,0 +1,46 @@ +/* + * 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.userdirectory + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.contacts.MappedContact +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_contact_main) +abstract class ContactItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var mappedContact: MappedContact + + override fun bind(holder: Holder) { + super.bind(holder) + // If name is empty, use userId as name and force it being centered + holder.nameView.text = mappedContact.displayName + avatarRenderer.render(mappedContact, holder.avatarImageView) + } + + class Holder : VectorEpoxyHolder() { + val nameView by bind(R.id.contactDisplayName) + val avatarImageView by bind(R.id.contactAvatar) + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/DirectoryUsersController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/DirectoryUsersController.kt deleted file mode 100644 index e68d9855dd..0000000000 --- a/vector/src/main/java/im/vector/app/features/userdirectory/DirectoryUsersController.kt +++ /dev/null @@ -1,139 +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.userdirectory - -import com.airbnb.epoxy.EpoxyController -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import im.vector.app.R -import im.vector.app.core.epoxy.errorWithRetryItem -import im.vector.app.core.epoxy.loadingItem -import im.vector.app.core.epoxy.noResultItem -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.MatrixPatterns -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.user.model.User -import org.matrix.android.sdk.api.util.toMatrixItem -import javax.inject.Inject - -class DirectoryUsersController @Inject constructor(private val session: Session, - private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter) : EpoxyController() { - - private var state: UserDirectoryViewState? = null - - var callback: Callback? = null - - init { - requestModelBuild() - } - - fun setData(state: UserDirectoryViewState) { - this.state = state - requestModelBuild() - } - - override fun buildModels() { - val currentState = state ?: return - val hasSearch = currentState.directorySearchTerm.isNotBlank() - when (val asyncUsers = currentState.directoryUsers) { - is Uninitialized -> renderEmptyState(false) - is Loading -> renderLoading() - is Success -> renderSuccess( - computeUsersList(asyncUsers(), currentState.directorySearchTerm), - currentState.getSelectedMatrixId(), - hasSearch - ) - is Fail -> renderFailure(asyncUsers.error) - } - } - - /** - * Eventually add the searched terms, if it is a userId, and if not already present in the result - */ - private fun computeUsersList(directoryUsers: List, searchTerms: String): List { - return directoryUsers + - searchTerms - .takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } } - ?.let { listOf(User(it)) } - .orEmpty() - } - - private fun renderLoading() { - loadingItem { - id("loading") - } - } - - private fun renderFailure(failure: Throwable) { - errorWithRetryItem { - id("error") - text(errorFormatter.toHumanReadable(failure)) - listener { callback?.retryDirectoryUsersRequest() } - } - } - - private fun renderSuccess(users: List, - selectedUsers: List, - hasSearch: Boolean) { - if (users.isEmpty()) { - renderEmptyState(hasSearch) - } else { - renderUsers(users, selectedUsers) - } - } - - private fun renderUsers(users: List, selectedUsers: List) { - for (user in users) { - if (user.userId == session.myUserId) { - continue - } - val isSelected = selectedUsers.contains(user.userId) - userDirectoryUserItem { - id(user.userId) - selected(isSelected) - matrixItem(user.toMatrixItem()) - avatarRenderer(avatarRenderer) - clickListener { _ -> - callback?.onItemClick(user) - } - } - } - } - - private fun renderEmptyState(hasSearch: Boolean) { - val noResultRes = if (hasSearch) { - R.string.no_result_placeholder - } else { - R.string.direct_room_start_search - } - noResultItem { - id("noResult") - text(stringProvider.getString(noResultRes)) - } - } - - interface Callback { - fun onItemClick(user: User) - fun retryDirectoryUsersRequest() - } -} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt deleted file mode 100644 index 4fbb9bbb41..0000000000 --- a/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt +++ /dev/null @@ -1,122 +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.userdirectory - -import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.paging.PagedListEpoxyController -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Uninitialized -import im.vector.app.R -import im.vector.app.core.epoxy.EmptyItem_ -import im.vector.app.core.epoxy.loadingItem -import im.vector.app.core.epoxy.noResultItem -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.createUIHandler -import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.user.model.User -import org.matrix.android.sdk.api.util.toMatrixItem -import javax.inject.Inject - -class KnownUsersController @Inject constructor(private val session: Session, - private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider) : PagedListEpoxyController( - modelBuildingHandler = createUIHandler() -) { - - private var selectedUsers: List = emptyList() - private var users: Async> = Uninitialized - private var isFiltering: Boolean = false - - var callback: Callback? = null - - init { - requestModelBuild() - } - - fun setData(state: UserDirectoryViewState) { - this.isFiltering = !state.filterKnownUsersValue.isEmpty() - val newSelection = state.getSelectedMatrixId() - this.users = state.knownUsers - if (newSelection != selectedUsers) { - this.selectedUsers = newSelection - requestForcedModelBuild() - } - submitList(state.knownUsers()) - } - - override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> { - return if (item == null) { - EmptyItem_().id(currentPosition) - } else { - val isSelected = selectedUsers.contains(item.userId) - UserDirectoryUserItem_() - .id(item.userId) - .selected(isSelected) - .matrixItem(item.toMatrixItem()) - .avatarRenderer(avatarRenderer) - .clickListener { _ -> - callback?.onItemClick(item) - } - } - } - - override fun addModels(models: List>) { - if (users is Incomplete) { - renderLoading() - } else if (models.isEmpty()) { - renderEmptyState() - } else { - var lastFirstLetter: String? = null - for (model in models) { - if (model is UserDirectoryUserItem) { - if (model.matrixItem.id == session.myUserId) continue - val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName() - val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter - lastFirstLetter = currentFirstLetter - - UserDirectoryLetterHeaderItem_() - .id(currentFirstLetter) - .letter(currentFirstLetter) - .addIf(showLetter, this) - - model.addTo(this) - } else { - continue - } - } - } - } - - private fun renderLoading() { - loadingItem { - id("loading") - } - } - - private fun renderEmptyState() { - noResultItem { - id("noResult") - text(stringProvider.getString(R.string.direct_room_no_known_users)) - } - } - - interface Callback { - fun onItemClick(user: User) - } -} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt deleted file mode 100644 index 8787946bf4..0000000000 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt +++ /dev/null @@ -1,95 +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.userdirectory - -import android.os.Bundle -import android.view.View -import com.airbnb.mvrx.activityViewModel -import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.app.R -import im.vector.app.core.extensions.cleanup -import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.setupAsSearch -import im.vector.app.core.extensions.showKeyboard -import im.vector.app.core.platform.VectorBaseFragment -import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.recyclerView -import kotlinx.android.synthetic.main.fragment_user_directory.* -import org.matrix.android.sdk.api.session.user.model.User -import javax.inject.Inject - -class UserDirectoryFragment @Inject constructor( - private val directRoomController: DirectoryUsersController -) : VectorBaseFragment(), DirectoryUsersController.Callback { - - override fun getLayoutResId() = R.layout.fragment_user_directory - private val viewModel: UserDirectoryViewModel by activityViewModel() - - private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) - setupRecyclerView() - setupSearchByMatrixIdView() - setupCloseView() - } - - override fun onDestroyView() { - recyclerView.cleanup() - directRoomController.callback = null - super.onDestroyView() - } - - private fun setupRecyclerView() { - directRoomController.callback = this - recyclerView.configureWith(directRoomController) - } - - private fun setupSearchByMatrixIdView() { - userDirectorySearchById.setupAsSearch(searchIconRes = 0) - userDirectorySearchById - .textChanges() - .subscribe { - viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString())) - } - .disposeOnDestroyView() - userDirectorySearchById.showKeyboard(andRequestFocus = true) - } - - private fun setupCloseView() { - userDirectoryClose.debouncedClicks { - sharedActionViewModel.post(UserDirectorySharedAction.GoBack) - } - } - - override fun invalidate() = withState(viewModel) { - directRoomController.setData(it) - } - - override fun onItemClick(user: User) { - view?.hideKeyboard() - viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user))) - sharedActionViewModel.post(UserDirectorySharedAction.GoBack) - } - - override fun retryDirectoryUsersRequest() { - val currentSearch = userDirectorySearchById.text.toString() - viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch)) - } -} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewModel.kt deleted file mode 100644 index 0a24b85ce2..0000000000 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewModel.kt +++ /dev/null @@ -1,153 +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.userdirectory - -import androidx.fragment.app.FragmentActivity -import arrow.core.Option -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.extensions.toggle -import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.createdirect.CreateDirectRoomActivity -import im.vector.app.features.invite.InviteUsersToRoomActivity -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.rx.rx -import java.util.concurrent.TimeUnit - -private typealias KnowUsersFilter = String -private typealias DirectoryUsersSearch = String - -class UserDirectoryViewModel @AssistedInject constructor(@Assisted - initialState: UserDirectoryViewState, - private val session: Session) - : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel - } - - private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) - private val directoryUsersSearch = BehaviorRelay.create() - - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? { - return when (viewModelContext) { - is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state) - is ActivityViewModelContext -> { - when (viewModelContext.activity()) { - is CreateDirectRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state) - is InviteUsersToRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state) - else -> error("Wrong activity or fragment") - } - } - else -> error("Wrong activity or fragment") - } - } - } - - init { - observeKnownUsers() - observeDirectoryUsers() - } - - override fun handle(action: UserDirectoryAction) { - when (action) { - is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) - is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) - is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) - is UserDirectoryAction.SelectPendingInvitee -> handleSelectUser(action) - is UserDirectoryAction.RemovePendingInvitee -> handleRemoveSelectedUser(action) - }.exhaustive - } - - private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state -> - val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee) - setState { - copy( - pendingInvitees = selectedUsers, - existingDmRoomId = getExistingDmRoomId(selectedUsers) - ) - } - } - - private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state -> - // Reset the filter asap - directoryUsersSearch.accept("") - val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee) - setState { - copy( - pendingInvitees = selectedUsers, - existingDmRoomId = getExistingDmRoomId(selectedUsers) - ) - } - } - - private fun getExistingDmRoomId(selectedUsers: Set): String? { - return selectedUsers - .takeIf { it.size == 1 } - ?.filterIsInstance(PendingInvitee.UserPendingInvitee::class.java) - ?.firstOrNull() - ?.let { invitee -> session.getExistingDirectRoomWithUser(invitee.user.userId) } - } - - private fun observeDirectoryUsers() = withState { state -> - directoryUsersSearch - .debounce(300, TimeUnit.MILLISECONDS) - .switchMapSingle { search -> - val stream = if (search.isBlank()) { - Single.just(emptyList()) - } else { - session.rx() - .searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet()) - .map { users -> - users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } - } - } - stream.toAsync { - copy(directoryUsers = it, directorySearchTerm = search) - } - } - .subscribe() - .disposeOnClear() - } - - private fun observeKnownUsers() = withState { state -> - knownUsersFilter - .throttleLast(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { - session.rx().livePagedUsers(it.orNull(), state.excludedUserIds) - } - .execute { async -> - copy( - knownUsers = async, - filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() - ) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryAction.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt index f4f3fb8cd4..0c2c4b1f4b 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt @@ -18,10 +18,10 @@ package im.vector.app.features.userdirectory import im.vector.app.core.platform.VectorViewModelAction -sealed class UserDirectoryAction : VectorViewModelAction { - data class FilterKnownUsers(val value: String) : UserDirectoryAction() - data class SearchDirectoryUsers(val value: String) : UserDirectoryAction() - object ClearFilterKnownUsers : UserDirectoryAction() - data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction() - data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction() +sealed class UserListAction : VectorViewModelAction { + data class SearchUsers(val value: String) : UserListAction() + object ClearSearchUsers : UserListAction() + data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction() + data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction() + object ComputeMatrixToLinkForSharing : UserListAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt new file mode 100644 index 0000000000..3e1523d0cc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt @@ -0,0 +1,197 @@ +/* + * 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.userdirectory + +import android.view.View +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.epoxy.noResultItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class UserListController @Inject constructor(private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter) : EpoxyController() { + + private var state: UserListViewState? = null + + var callback: Callback? = null + + fun setData(state: UserListViewState) { + this.state = state + requestModelBuild() + } + + override fun buildModels() { + val currentState = state ?: return + + // Build generic items + if (currentState.searchTerm.isBlank()) { + // For now we remove this option if in invite to existing room flow (and not create DM) + if (currentState.pendingInvitees.isEmpty() + // For now we remove this option if in invite to existing room flow (and not create DM) + && currentState.existingRoomId == null) { + actionItem { + id(R.drawable.ic_share) + title(stringProvider.getString(R.string.invite_friends)) + actionIconRes(R.drawable.ic_share) + clickAction(View.OnClickListener { + callback?.onInviteFriendClick() + }) + } + } + actionItem { + id(R.drawable.ic_baseline_perm_contact_calendar_24) + title(stringProvider.getString(R.string.contacts_book_title)) + actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24) + clickAction(View.OnClickListener { + callback?.onContactBookClick() + }) + } + if (currentState.pendingInvitees.isEmpty() + // For now we remove this option if in invite to existing room flow (and not create DM) + && currentState.existingRoomId == null) { + actionItem { + id(R.drawable.ic_qr_code_add) + title(stringProvider.getString(R.string.qr_code)) + actionIconRes(R.drawable.ic_qr_code_add) + clickAction(View.OnClickListener { + callback?.onUseQRCode() + }) + } + } + } + + when (currentState.knownUsers) { + is Uninitialized -> renderEmptyState() + is Loading -> renderLoading() + is Fail -> renderFailure(currentState.knownUsers.error) + is Success -> buildKnownUsers(currentState, currentState.getSelectedMatrixId()) + } + + when (val asyncUsers = currentState.directoryUsers) { + is Uninitialized -> { + } + is Loading -> renderLoading() + is Fail -> renderFailure(asyncUsers.error) + is Success -> buildDirectoryUsers( + asyncUsers(), + currentState.getSelectedMatrixId(), + currentState.searchTerm, + // to avoid showing twice same user in known and suggestions + currentState.knownUsers.invoke()?.map { it.userId }.orEmpty() + ) + } + } + + private fun buildKnownUsers(currentState: UserListViewState, selectedUsers: List) { + currentState.knownUsers()?.let { userList -> + userListHeaderItem { + id("known_header") + header(stringProvider.getString(R.string.direct_room_user_list_known_title)) + } + + if (userList.isEmpty()) { + renderEmptyState() + return + } + userList.forEach { item -> + val isSelected = selectedUsers.contains(item.userId) + userDirectoryUserItem { + id(item.userId) + selected(isSelected) + matrixItem(item.toMatrixItem()) + avatarRenderer(avatarRenderer) + clickListener { _ -> + callback?.onItemClick(item) + } + } + } + } + } + + private fun buildDirectoryUsers(directoryUsers: List, selectedUsers: List, searchTerms: String, ignoreIds: List) { + val toDisplay = directoryUsers.filter { !ignoreIds.contains(it.userId) } + if (toDisplay.isEmpty() && searchTerms.isBlank()) { + return + } + userListHeaderItem { + id("suggestions") + header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title)) + } + if (toDisplay.isEmpty()) { + renderEmptyState() + } else { + toDisplay.forEach { user -> + if (user.userId != session.myUserId) { + val isSelected = selectedUsers.contains(user.userId) + userDirectoryUserItem { + id(user.userId) + selected(isSelected) + matrixItem(user.toMatrixItem()) + avatarRenderer(avatarRenderer) + clickListener { _ -> + callback?.onItemClick(user) + } + } + } + } + } + } + + private fun renderLoading() { + loadingItem { + id("loading") + } + } + + private fun renderEmptyState() { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } + + private fun renderFailure(failure: Throwable) { + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(failure)) + } + } + + interface Callback { + fun onInviteFriendClick() + fun onContactBookClick() + fun onUseQRCode() + fun onItemClick(user: User) + fun onMatrixIdClick(matrixId: String) + fun onThreePidClick(threePid: ThreePid) + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt similarity index 57% rename from vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragment.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index 0ca46cd154..4568878446 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -36,53 +36,64 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setupAsSearch import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.DimensionConverter +import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel -import kotlinx.android.synthetic.main.fragment_known_users.* +import kotlinx.android.synthetic.main.fragment_user_list.* +import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User import javax.inject.Inject -class KnownUsersFragment @Inject constructor( - val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory, - private val knownUsersController: KnownUsersController, +class UserListFragment @Inject constructor( + private val userListController: UserListController, private val dimensionConverter: DimensionConverter, val homeServerCapabilitiesViewModelFactory: HomeServerCapabilitiesViewModel.Factory -) : VectorBaseFragment(), KnownUsersController.Callback { +) : VectorBaseFragment(), UserListController.Callback { - private val args: KnownUsersFragmentArgs by args() + private val args: UserListFragmentArgs by args() + private val viewModel: UserListViewModel by activityViewModel() + private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel() + private lateinit var sharedActionViewModel: UserListSharedActionViewModel - override fun getLayoutResId() = R.layout.fragment_known_users + override fun getLayoutResId() = R.layout.fragment_user_list override fun getMenuRes() = args.menuResId - private val viewModel: UserDirectoryViewModel by activityViewModel() - private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel() - - private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) + userListTitle.text = args.title + vectorBaseActivity.setSupportActionBar(userListToolbar) - knownUsersTitle.text = args.title - vectorBaseActivity.setSupportActionBar(knownUsersToolbar) setupRecyclerView() - setupFilterView() - setupAddByMatrixIdView() - setupAddFromPhoneBookView() + setupSearchView() setupCloseView() homeServerCapabilitiesViewModel.subscribe { - knownUsersE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault + userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault } - viewModel.selectSubscribe(this, UserDirectoryViewState::pendingInvitees) { + viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) { renderSelectedUsers(it) } + + viewModel.observeViewEvents { + when (it) { + is UserListViewEvents.OpenShareMatrixToLing -> { + val text = getString(R.string.invite_friends_text, it.link) + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = getString(R.string.invite_friends), + text = text, + extraTitle = getString(R.string.invite_friends_rich_title) + ) + } + } + } } override fun onDestroyView() { - knownUsersController.callback = null - recyclerView.cleanup() + userListRecyclerView.cleanup() super.onDestroyView() } @@ -91,69 +102,52 @@ class KnownUsersFragment @Inject constructor( val showMenuItem = it.pendingInvitees.isNotEmpty() menu.forEach { menuItem -> menuItem.isVisible = showMenuItem - if (args.isCreatingRoom) { - menuItem.setTitle(if (it.existingDmRoomId != null) R.string.action_open else R.string.create_room_action_create) - } } } super.onPrepareOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { - sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected( - item.itemId, - it.pendingInvitees, - it.existingDmRoomId - )) + sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees)) return@withState true } - private fun setupAddByMatrixIdView() { - addByMatrixId.debouncedClicks { - sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory) - } - } - - private fun setupAddFromPhoneBookView() { - addFromPhoneBook.debouncedClicks { - // TODO handle Permission first - sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook) - } - } - private fun setupRecyclerView() { - knownUsersController.callback = this + userListController.callback = this // Don't activate animation as we might have way to much item animation when filtering - recyclerView.configureWith(knownUsersController, disableItemAnimation = true) + userListRecyclerView.configureWith(userListController, disableItemAnimation = true) } - private fun setupFilterView() { - knownUsersFilter + private fun setupSearchView() { + withState(viewModel) { + userListSearch.hint = getString(R.string.user_directory_search_hint) + } + userListSearch .textChanges() - .startWith(knownUsersFilter.text) + .startWith(userListSearch.text) .subscribe { text -> - val filterValue = text.trim() - val action = if (filterValue.isBlank()) { - UserDirectoryAction.ClearFilterKnownUsers + val searchValue = text.trim() + val action = if (searchValue.isBlank()) { + UserListAction.ClearSearchUsers } else { - UserDirectoryAction.FilterKnownUsers(filterValue.toString()) + UserListAction.SearchUsers(searchValue.toString()) } viewModel.handle(action) } .disposeOnDestroyView() - knownUsersFilter.setupAsSearch() - knownUsersFilter.requestFocus() + userListSearch.setupAsSearch() + userListSearch.requestFocus() } private fun setupCloseView() { - knownUsersClose.debouncedClicks { + userListClose.debouncedClicks { requireActivity().finish() } } override fun invalidate() = withState(viewModel) { - knownUsersController.setData(it) + userListController.setData(it) } private fun renderSelectedUsers(invitees: Set) { @@ -183,12 +177,35 @@ class KnownUsersFragment @Inject constructor( chip.isCloseIconVisible = true chipGroup.addView(chip) chip.setOnCloseIconClickListener { - viewModel.handle(UserDirectoryAction.RemovePendingInvitee(pendingInvitee)) + viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee)) } } + override fun onInviteFriendClick() { + viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing) + } + + override fun onContactBookClick() { + sharedActionViewModel.post(UserListSharedAction.OpenPhoneBook) + } + override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user))) + viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user))) + } + + override fun onMatrixIdClick(matrixId: String) { + view?.hideKeyboard() + viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) + } + + override fun onThreePidClick(threePid: ThreePid) { + view?.hideKeyboard() + viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) + } + + override fun onUseQRCode() { + view?.hideKeyboard() + sharedActionViewModel.post(UserListSharedAction.AddByQrCode) } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt similarity index 91% rename from vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragmentArgs.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt index c20aedb803..041f29a77a 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt @@ -20,9 +20,9 @@ import android.os.Parcelable import kotlinx.android.parcel.Parcelize @Parcelize -data class KnownUsersFragmentArgs( +data class UserListFragmentArgs( val title: String, val menuResId: Int, val excludedUserIds: Set? = null, - val isCreatingRoom: Boolean = false + val existingRoomId: String? = null ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt new file mode 100644 index 0000000000..82fa4a4d6f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt @@ -0,0 +1,39 @@ +/* + * 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.userdirectory + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_user_list_header) +abstract class UserListHeaderItem : VectorEpoxyModel() { + + @EpoxyAttribute var header: String = "" + + override fun bind(holder: Holder) { + super.bind(holder) + holder.headerTextView.text = header + } + + class Holder : VectorEpoxyHolder() { + val headerTextView by bind(R.id.userListHeaderView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt similarity index 59% rename from vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedAction.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt index 14daa67f25..b2cdee3e63 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt @@ -18,12 +18,10 @@ package im.vector.app.features.userdirectory import im.vector.app.core.platform.VectorSharedAction -sealed class UserDirectorySharedAction : VectorSharedAction { - object OpenUsersDirectory : UserDirectorySharedAction() - object OpenPhoneBook : UserDirectorySharedAction() - object Close : UserDirectorySharedAction() - object GoBack : UserDirectorySharedAction() - data class OnMenuItemSelected(val itemId: Int, - val invitees: Set, - val existingDmRoomId: String?) : UserDirectorySharedAction() +sealed class UserListSharedAction : VectorSharedAction { + object Close : UserListSharedAction() + object GoBack : UserListSharedAction() + data class OnMenuItemSelected(val itemId: Int, val invitees: Set) : UserListSharedAction() + object OpenPhoneBook : UserListSharedAction() + object AddByQrCode : UserListSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedActionViewModel.kt similarity index 85% rename from vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedActionViewModel.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedActionViewModel.kt index b63682e57a..05ebc73cff 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedActionViewModel.kt @@ -19,4 +19,4 @@ package im.vector.app.features.userdirectory import im.vector.app.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +class UserListSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewEvents.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt similarity index 85% rename from vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewEvents.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt index bfbdc657ef..95c6729fad 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt @@ -21,4 +21,6 @@ import im.vector.app.core.platform.VectorViewEvents /** * Transient events for invite users to room screen */ -sealed class UserDirectoryViewEvents : VectorViewEvents +sealed class UserListViewEvents : VectorViewEvents { + data class OpenShareMatrixToLing(val link: String) : UserListViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt new file mode 100644 index 0000000000..f8eabbaed0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -0,0 +1,180 @@ +/* + * 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.userdirectory + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.toggle +import im.vector.app.core.platform.VectorViewModel +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.rx.rx +import java.util.concurrent.TimeUnit + +private typealias KnownUsersSearch = String +private typealias DirectoryUsersSearch = String + +class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState, + private val session: Session) + : VectorViewModel(initialState) { + + private val knownUsersSearch = BehaviorRelay.create() + private val directoryUsersSearch = BehaviorRelay.create() + + private var currentUserSearchDisposable: Disposable? = null + + @AssistedInject.Factory + interface Factory { + fun create(initialState: UserListViewState): UserListViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: UserListViewState): UserListViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + setState { + copy( + myUserId = session.myUserId, + existingRoomId = initialState.existingRoomId + ) + } + observeUsers() + } + + override fun handle(action: UserListAction) { + when (action) { + is UserListAction.SearchUsers -> handleSearchUsers(action.value) + is UserListAction.ClearSearchUsers -> handleClearSearchUsers() + is UserListAction.SelectPendingInvitee -> handleSelectUser(action) + is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action) + UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink() + }.exhaustive + } + + private fun handleSearchUsers(searchTerm: String) { + setState { + copy(searchTerm = searchTerm) + } + knownUsersSearch.accept(searchTerm) + directoryUsersSearch.accept(searchTerm) + } + + private fun handleShareMyMatrixToLink() { + session.permalinkService().createPermalink(session.myUserId)?.let { + _viewEvents.post(UserListViewEvents.OpenShareMatrixToLing(it)) + } + } + + private fun handleClearSearchUsers() { + knownUsersSearch.accept("") + directoryUsersSearch.accept("") + setState { + copy(searchTerm = "") + } + } + + private fun observeUsers() = withState { state -> + knownUsersSearch + .throttleLast(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + session.rx().livePagedUsers(it, state.excludedUserIds) + } + .execute { async -> + copy(knownUsers = async) + } + + currentUserSearchDisposable?.dispose() + + directoryUsersSearch + .debounce(300, TimeUnit.MILLISECONDS) + .switchMapSingle { search -> + val stream = if (search.isBlank()) { + Single.just(emptyList()) + } else { + val searchObservable = session.rx() + .searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet()) + .map { users -> + users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } + } + // If it's a valid user id try to use Profile API + // because directory only returns users that are in public rooms or share a room with you, where as + // profile will work other federations + if (!MatrixPatterns.isUserId(search)) { + searchObservable + } else { + val profileObservable = session.rx().getProfileInfo(search) + .map { json -> + User( + userId = search, + displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String, + avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String + ).toOptional() + } + .onErrorReturn { Optional.empty() } + + Single.zip(searchObservable, profileObservable, { searchResults, optionalProfile -> + val profile = optionalProfile.getOrNull() ?: return@zip searchResults + val searchContainsProfile = searchResults.indexOfFirst { it.userId == profile.userId } != -1 + if (searchContainsProfile) { + searchResults + } else { + listOf(profile) + searchResults + } + }) + } + } + stream.toAsync { + copy(directoryUsers = it) + } + } + .subscribe() + .disposeOnClear() + } + + private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state -> + val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee) + setState { copy(pendingInvitees = selectedUsers) } + } + + private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state -> + val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee) + setState { copy(pendingInvitees = selectedUsers) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewState.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt similarity index 75% rename from vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewState.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt index fe79a8ab37..69135f912d 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewState.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt @@ -17,30 +17,33 @@ package im.vector.app.features.userdirectory import androidx.paging.PagedList -import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.app.core.contacts.MappedContact import org.matrix.android.sdk.api.session.user.model.User -data class UserDirectoryViewState( +data class UserListViewState( val excludedUserIds: Set? = null, val knownUsers: Async> = Uninitialized, val directoryUsers: Async> = Uninitialized, + val filteredMappedContacts: List = emptyList(), val pendingInvitees: Set = emptySet(), val createAndInviteState: Async = Uninitialized, - val directorySearchTerm: String = "", - val filterKnownUsersValue: Option = Option.empty(), - val existingDmRoomId: String? = null + val searchTerm: String = "", + val myUserId: String = "", + val existingRoomId: String? = null ) : MvRxState { - constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds) + constructor(args: UserListFragmentArgs) : this( + existingRoomId = args.existingRoomId + ) fun getSelectedMatrixId(): List { return pendingInvitees .mapNotNull { when (it) { - is PendingInvitee.UserPendingInvitee -> it.user.userId + is PendingInvitee.UserPendingInvitee -> it.user.userId is PendingInvitee.ThreePidPendingInvitee -> null } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt index cb40e5672b..eb588ec9ae 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.net.URL @@ -106,14 +105,11 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in if (state.permissionData()?.isWebviewWidget.orFalse()) { WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false) } else { - awaitCallback { - session.integrationManagerService().setNativeWidgetDomainAllowed( - state.permissionData.invoke()?.widget?.type?.preferred ?: "", - state.permissionData.invoke()?.widgetDomain ?: "", - false, - it - ) - } + session.integrationManagerService().setNativeWidgetDomainAllowed( + state.permissionData.invoke()?.widget?.type?.preferred ?: "", + state.permissionData.invoke()?.widgetDomain ?: "", + false + ) } } catch (failure: Throwable) { Timber.v("Failure revoking widget: ${state.widgetId}") @@ -131,14 +127,11 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in if (state.permissionData()?.isWebviewWidget.orFalse()) { WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true) } else { - awaitCallback { - session.integrationManagerService().setNativeWidgetDomainAllowed( - state.permissionData.invoke()?.widget?.type?.preferred ?: "", - state.permissionData.invoke()?.widgetDomain ?: "", - true, - it - ) - } + session.integrationManagerService().setNativeWidgetDomainAllowed( + state.permissionData.invoke()?.widget?.type?.preferred ?: "", + state.permissionData.invoke()?.widgetDomain ?: "", + true + ) } } catch (failure: Throwable) { Timber.v("Failure allowing widget: ${state.widgetId}") diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt index 871e73592d..5664609a99 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt @@ -19,7 +19,6 @@ package im.vector.app.features.widgets.permissions import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.widgets.WidgetService -import org.matrix.android.sdk.internal.util.awaitCallback class WidgetPermissionsHelper(private val integrationManagerService: IntegrationManagerService, private val widgetService: WidgetService) { @@ -30,8 +29,6 @@ class WidgetPermissionsHelper(private val integrationManagerService: Integration widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE) ).firstOrNull() val eventId = widget?.event?.eventId ?: return - awaitCallback { - integrationManagerService.setWidgetAllowed(eventId, allow, it) - } + integrationManagerService.setWidgetAllowed(eventId, allow) } } diff --git a/vector/src/main/res/drawable-nodpi/empty_state_dm.png b/vector/src/main/res/drawable-nodpi/empty_state_dm.png new file mode 100644 index 0000000000..12eedc803f Binary files /dev/null and b/vector/src/main/res/drawable-nodpi/empty_state_dm.png differ diff --git a/vector/src/main/res/drawable-nodpi/empty_state_room.png b/vector/src/main/res/drawable-nodpi/empty_state_room.png new file mode 100644 index 0000000000..4fd93b2893 Binary files /dev/null and b/vector/src/main/res/drawable-nodpi/empty_state_room.png differ diff --git a/vector/src/main/res/drawable/ic_add_people.xml b/vector/src/main/res/drawable/ic_add_people.xml new file mode 100644 index 0000000000..3ec60095ff --- /dev/null +++ b/vector/src/main/res/drawable/ic_add_people.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_baseline_perm_contact_calendar_24.xml b/vector/src/main/res/drawable/ic_baseline_perm_contact_calendar_24.xml new file mode 100644 index 0000000000..ba2ca10744 --- /dev/null +++ b/vector/src/main/res/drawable/ic_baseline_perm_contact_calendar_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_book.xml b/vector/src/main/res/drawable/ic_book.xml new file mode 100644 index 0000000000..3cd7357248 --- /dev/null +++ b/vector/src/main/res/drawable/ic_book.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_fab_add_by_mxid.xml b/vector/src/main/res/drawable/ic_fab_add_by_mxid.xml new file mode 100644 index 0000000000..50768871ab --- /dev/null +++ b/vector/src/main/res/drawable/ic_fab_add_by_mxid.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml b/vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml new file mode 100644 index 0000000000..50768871ab --- /dev/null +++ b/vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_invite_people.xml b/vector/src/main/res/drawable/ic_invite_people.xml new file mode 100644 index 0000000000..3ec60095ff --- /dev/null +++ b/vector/src/main/res/drawable/ic_invite_people.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_picture_icon.xml b/vector/src/main/res/drawable/ic_picture_icon.xml new file mode 100644 index 0000000000..c978a714ab --- /dev/null +++ b/vector/src/main/res/drawable/ic_picture_icon.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_qr_code_add.xml b/vector/src/main/res/drawable/ic_qr_code_add.xml new file mode 100644 index 0000000000..133af41083 --- /dev/null +++ b/vector/src/main/res/drawable/ic_qr_code_add.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_trash_24.xml b/vector/src/main/res/drawable/ic_trash_24.xml index 266855d50c..27ad2e29d7 100644 --- a/vector/src/main/res/drawable/ic_trash_24.xml +++ b/vector/src/main/res/drawable/ic_trash_24.xml @@ -1,41 +1,47 @@ - - - - - + + + + + diff --git a/vector/src/main/res/layout/activity.xml b/vector/src/main/res/layout/activity.xml index b5203cd589..9e56d9e605 100644 --- a/vector/src/main/res/layout/activity.xml +++ b/vector/src/main/res/layout/activity.xml @@ -1,26 +1,32 @@ - - + android:layout_height="match_parent"> - + - + - + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_simple.xml b/vector/src/main/res/layout/activity_simple.xml index 0eda46a67d..d7382d173d 100644 --- a/vector/src/main/res/layout/activity_simple.xml +++ b/vector/src/main/res/layout/activity_simple.xml @@ -1,7 +1,7 @@ - @@ -10,4 +10,21 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml new file mode 100644 index 0000000000..d051bd7c98 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/dialog_share_qr_code.xml b/vector/src/main/res/layout/dialog_share_qr_code.xml new file mode 100644 index 0000000000..04613023a7 --- /dev/null +++ b/vector/src/main/res/layout/dialog_share_qr_code.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/vector/src/main/res/layout/fragment_contacts_book.xml b/vector/src/main/res/layout/fragment_contacts_book.xml index eb90da1bbe..1f8566e05e 100644 --- a/vector/src/main/res/layout/fragment_contacts_book.xml +++ b/vector/src/main/res/layout/fragment_contacts_book.xml @@ -93,6 +93,27 @@ app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" tools:visibility="visible" /> + + + + + app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" /> + + diff --git a/vector/src/main/res/layout/fragment_generic_recycler.xml b/vector/src/main/res/layout/fragment_generic_recycler.xml index bef10073fd..4aded532e8 100644 --- a/vector/src/main/res/layout/fragment_generic_recycler.xml +++ b/vector/src/main/res/layout/fragment_generic_recycler.xml @@ -7,7 +7,7 @@ android:background="?riotx_background"> @@ -43,13 +44,13 @@ android:id="@+id/homeDrawerUsernameView" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="24dp" - android:layout_marginEnd="@dimen/layout_horizontal_margin" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" android:maxLines="1" android:singleLine="true" android:textColor="?riotx_text_primary" android:textSize="15sp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/homeDrawerQRCodeButton" app:layout_constraintStart_toStartOf="@+id/homeDrawerHeaderAvatarView" app:layout_constraintTop_toBottomOf="@+id/homeDrawerHeaderAvatarView" tools:text="@sample/matrix.json/data/displayName" /> @@ -58,18 +59,67 @@ android:id="@+id/homeDrawerUserIdView" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/layout_horizontal_margin" - android:layout_marginBottom="17dp" + android:layout_marginEnd="8dp" android:maxLines="1" android:singleLine="true" android:textColor="?riotx_text_secondary" android:textSize="15sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@+id/homeDrawerInviteFriendButton" + app:layout_constraintEnd_toStartOf="@+id/homeDrawerQRCodeButton" app:layout_constraintStart_toStartOf="@+id/homeDrawerHeaderAvatarView" app:layout_constraintTop_toBottomOf="@+id/homeDrawerUsernameView" tools:text="@sample/matrix.json/data/mxid" /> + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/fragment_matrix_profile.xml b/vector/src/main/res/layout/fragment_matrix_profile.xml index c935ab5cee..c10185b2f3 100644 --- a/vector/src/main/res/layout/fragment_matrix_profile.xml +++ b/vector/src/main/res/layout/fragment_matrix_profile.xml @@ -95,7 +95,6 @@ - + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_progress.xml b/vector/src/main/res/layout/fragment_progress.xml new file mode 100644 index 0000000000..a7a2076209 --- /dev/null +++ b/vector/src/main/res/layout/fragment_progress.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/vector/src/main/res/layout/fragment_qr_code_scanner.xml b/vector/src/main/res/layout/fragment_qr_code_scanner.xml index 589b7c73d4..135a856f4a 100644 --- a/vector/src/main/res/layout/fragment_qr_code_scanner.xml +++ b/vector/src/main/res/layout/fragment_qr_code_scanner.xml @@ -15,4 +15,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_qr_code_scanner_with_button.xml b/vector/src/main/res/layout/fragment_qr_code_scanner_with_button.xml new file mode 100644 index 0000000000..f35c1c91e2 --- /dev/null +++ b/vector/src/main/res/layout/fragment_qr_code_scanner_with_button.xml @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index cc3efbf4f4..33f462c0d1 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -82,7 +82,7 @@ + android:overScrollMode="always" + tools:listitem="@layout/item_room" /> - 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 07744436ea..7ff63f3ce5 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -62,7 +62,7 @@ app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_user_directory.xml b/vector/src/main/res/layout/fragment_user_directory.xml index b682d181cd..31a34512cf 100644 --- a/vector/src/main/res/layout/fragment_user_directory.xml +++ b/vector/src/main/res/layout/fragment_user_directory.xml @@ -90,7 +90,7 @@ app:layout_constraintTop_toBottomOf="@+id/userDirectorySearchByIdContainer" /> - @@ -67,7 +67,7 @@ android:layout_marginEnd="@dimen/layout_horizontal_margin" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar" + app:layout_constraintTop_toBottomOf="@+id/userListToolbar" app:maxHeight="64dp"> + app:layout_constraintTop_toBottomOf="@+id/userListSearch" /> - + android:layout_margin="16dp" + android:drawablePadding="8dp" + android:text="@string/settings_hs_admin_e2e_disabled" + android:textColor="?riotx_text_secondary" + android:textSize="14sp" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/userListFilterDivider" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@+id/userListE2EbyDefaultDisabled" + tools:listitem="@layout/item_known_user" /> - - - + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml index 8b5716cd8e..7456f50670 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -13,6 +13,7 @@ android:paddingEnd="@dimen/layout_horizontal_margin" android:paddingBottom="8dp"> + + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_room_actions_notifications_all" /> - + app:layout_constraintStart_toEndOf="@id/actionIcon" + app:layout_constraintTop_toTopOf="parent"> + + + - + tools:ignore="MissingPrefix" + tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_bottom_sheet_title.xml b/vector/src/main/res/layout/item_bottom_sheet_title.xml new file mode 100644 index 0000000000..5113c43f39 --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_title.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/item_checkbox.xml b/vector/src/main/res/layout/item_checkbox.xml new file mode 100644 index 0000000000..78dde9734b --- /dev/null +++ b/vector/src/main/res/layout/item_checkbox.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_contact_action.xml b/vector/src/main/res/layout/item_contact_action.xml new file mode 100644 index 0000000000..7a9a751257 --- /dev/null +++ b/vector/src/main/res/layout/item_contact_action.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_form_advanced_toggle.xml b/vector/src/main/res/layout/item_form_advanced_toggle.xml new file mode 100644 index 0000000000..46609b64ca --- /dev/null +++ b/vector/src/main/res/layout/item_form_advanced_toggle.xml @@ -0,0 +1,34 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_form_switch.xml b/vector/src/main/res/layout/item_form_switch.xml index 24425a5eb4..cc662680bb 100644 --- a/vector/src/main/res/layout/item_form_switch.xml +++ b/vector/src/main/res/layout/item_form_switch.xml @@ -15,8 +15,6 @@ android:layout_marginStart="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin" android:duplicateParentState="true" - android:ellipsize="end" - android:maxLines="1" android:textColor="?riotx_text_primary" android:textSize="15sp" app:layout_constraintBottom_toTopOf="@+id/formSwitchSummary" diff --git a/vector/src/main/res/layout/item_form_text_input.xml b/vector/src/main/res/layout/item_form_text_input.xml index 594bfc1788..f7ce8e1c9f 100644 --- a/vector/src/main/res/layout/item_form_text_input.xml +++ b/vector/src/main/res/layout/item_form_text_input.xml @@ -20,10 +20,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> + diff --git a/vector/src/main/res/layout/item_room_alias_text_input.xml b/vector/src/main/res/layout/item_room_alias_text_input.xml new file mode 100644 index 0000000000..fd7a99f0f0 --- /dev/null +++ b/vector/src/main/res/layout/item_room_alias_text_input.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_settings_three_pid.xml b/vector/src/main/res/layout/item_settings_three_pid.xml index a175788d86..0040840ce9 100644 --- a/vector/src/main/res/layout/item_settings_three_pid.xml +++ b/vector/src/main/res/layout/item_settings_three_pid.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?riotx_background" android:minHeight="64dp" android:paddingStart="@dimen/layout_horizontal_margin" android:paddingEnd="@dimen/layout_horizontal_margin"> @@ -12,19 +13,20 @@ android:id="@+id/item_settings_three_pid_icon" android:layout_width="16dp" android:layout_height="16dp" + android:layout_marginEnd="8dp" android:scaleType="center" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/item_settings_three_pid_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/ic_phone" app:tint="?riotx_text_secondary" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_phone" /> + android:layout_height="wrap_content"> - + android:layout_height="wrap_content"> - + - + + + + + + + + + + + + + + + + + + + + + + + + android:layout_below="@+id/creationTile" + android:layout_marginTop="8dp"> + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_verification_qr_code.xml b/vector/src/main/res/layout/item_verification_qr_code.xml index b57470fdc1..413b94013a 100644 --- a/vector/src/main/res/layout/item_verification_qr_code.xml +++ b/vector/src/main/res/layout/item_verification_qr_code.xml @@ -11,6 +11,6 @@ android:layout_height="200dp" android:layout_gravity="center_horizontal" android:contentDescription="@string/a11y_qr_code_for_verification" - tools:src="@color/riotx_header_panel_background_black" /> + tools:src="@drawable/ic_qr_code_add" /> diff --git a/vector/src/main/res/layout/motion_fab_menu_merge.xml b/vector/src/main/res/layout/motion_notifs_fab_menu_merge.xml similarity index 98% rename from vector/src/main/res/layout/motion_fab_menu_merge.xml rename to vector/src/main/res/layout/motion_notifs_fab_menu_merge.xml index e564d16aaf..8130ea0637 100644 --- a/vector/src/main/res/layout/motion_fab_menu_merge.xml +++ b/vector/src/main/res/layout/motion_notifs_fab_menu_merge.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - app:layoutDescription="@xml/motion_scene_fab_menu" + app:layoutDescription="@xml/motion_scene_notifs_fab_menu" tools:motionProgress="0.65" tools:parentTag="androidx.constraintlayout.motion.widget.MotionLayout" tools:showPaths="true"> diff --git a/vector/src/main/res/layout/view_state.xml b/vector/src/main/res/layout/view_state.xml index 2d11daadbc..11f176e405 100644 --- a/vector/src/main/res/layout/view_state.xml +++ b/vector/src/main/res/layout/view_state.xml @@ -20,7 +20,9 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:orientation="vertical" - android:padding="@dimen/layout_horizontal_margin"> + android:padding="@dimen/layout_horizontal_margin" + android:visibility="gone" + tools:visibility="visible"> - - + + - - + app:layout_constraintTop_toBottomOf="@id/emptyImageView" + app:layout_constraintVertical_chainStyle="packed" + tools:text="@string/room_list_people_empty_title" /> + app:layout_constraintTop_toBottomOf="@+id/emptyTitleView" + app:layout_constraintVertical_chainStyle="packed" + tools:text="@string/room_list_people_empty_body" /> diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index acd689387d..c62f245d88 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -2194,4 +2194,8 @@ Тема Тема на стаята (опция) Име на стая + Експортирай одит + Директно съобщение + Изпрати историята на заявките за споделяне на ключове + Няма повече резултати \ No newline at end of file diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index d711d778ca..ed6128702b 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -1,69 +1,63 @@ - + - ca ES - Tema clar Tema fosc Tema negre - - S\'està sincronitzant… - Escolta esdeveniments - Notificacions sorolloses + Sincronitzant… + Escoltant esdeveniments + Notificacions amb so Notificacions silencioses - Missatges Sala Configuració - Detalls dels participants + Detalls del participant Historial Informe d\'errors Detalls de la comunitat - D\'acord Cancel·la Desa - Abandona la sala + Marxa Envia Reenvia - Suprimeix + Elimina Cita Comparteix Més tard Reenvia Enllaç permanent - Mostra el codi + Visualitza el codi font Visualitza el codi font desencriptat Elimina - Reanomena + Canvia el nom Informa del contingut Trucada activa - Conferència en curs. -\nUniu-vos hi per %1$s o %2$s. + Videoconferència en curs. +\nUneix-te amb %1$s o %2$s Veu Vídeo - La trucada no es pot iniciar, prova-ho més tard - Pot ser que algunes funcions no apareguin per manca de permisos… - Es necessiten permisos per convidar a iniciar una conferència en aquesta sala + No es pot iniciar la trucada, prova-ho més tard + Pot ser que algunes funcions no apareguin per falta de permisos… + Necessites permisos d\'invitació per poder iniciar una conferència en aquesta sala No es pot iniciar la trucada - Informació del dispositiu - No es poden fer conferències en sales encriptades + Informació de la sessió + No s\'admeten conferències en sales xifrades Envia igualment o Convida Fora de línia - - Desconnecta - Trucada de veu - Vídeotrucada + Tanca la sessió + Trucada + Videotrucada Cerca global Marca-ho tot com a llegit Historial @@ -72,91 +66,76 @@ Tanca S\'ha copiat al porta-retalls Desactiva - Confirmació Avís - Inici Preferits Persones Sales - - Filtrar noms de sales - Filtrar preferits - Filtrar persones - Filtrar per noms de sala - Filtrar per nom de comunitats - + Filtra noms de sala + Filtra preferits + Filtra persones + Filtra noms de sala + Filtra noms de comunitat - Convida + Invitacions Prioritat baixa - Converses Llibreta d\'adreces local - Directori de l\'usuari + Directori d\'usuari Només contactes de Matrix - Cap conversa - No vau donar permís al Element per accedir als contactes locals - Cap resultat - + Sense converses + No has donat permís a Element perquè pugui accedir als teus contactes locals + Sense resultats Sales - Directori de sales - No hi ha cap sala - No hi ha cap sala pública disponible + Directori de sala + No hi ha sales + No hi ha sales públiques disponibles - 1 usuari + %d usuari %d usuaris - Convida Comunitats - No hi ha cap grup - + No hi ha grups Envia els registres Envia els registres de fallada Envia una captura de pantalla Informa d\'un error - Descriviu l\'error. Què heu fet? Què esperàveu que passés? Què ha passat realment? - Descriviu el problema aquí - Els registres d\'aquest client s\'enviaran amb aquest informe d\'error per tal de diagnosticar problemes. Aquest informe d\'errors, així com també els registres i la captura de pantalla, no seran visibles de forma pública. Desmarqueu si preferiu enviar només el text: - Sembla que esteu prou frustrat per a estar sacsejant el telèfon. Voleu enviar un informe d\'error? - En l\'última execució l\'aplicació va fallar. Voleu enviar un informe d\'error? - - L\'informe d\'error s\'ha enviat correctament - No s\'ha pogut enviar l\'informe d\'error (%s) + Descriu l\'error. Què has fet\? Què esperaves que passés\? Què ha passat realment\? + Descriu el problema aquí + Per tal de diagnosticar problemes, els registres d\'aquest client s\'enviaran juntament amb l\'informe d\'errors. Aquest informe d\'errors, així com també els registres i la captura de pantalla, no seran visibles públicament. Si prefereixes enviar només el text de dalt, desmarca: + Sembla que estàs sacsejant el telèfon amb frustració. Vols enviar un informe d\'errors\? + En l\'última execució l\'aplicació ha fallat. Vols obrir la pantalla d\'informe de fallada\? + L\'informe d\'errors s\'ha enviat correctament + No s\'ha pogut enviar l\'informe d\'errors (%s) En curs (%s%%) - Envia a Llegit - Uneix-te a la sala Nom d\'usuari Crear un compte - Entra - Desconnecta + Inicia sessió + Tanca la sessió URL del servidor URL del servidor d\'identitat Cerca - - Inicia un xat nou - Inicia una trucada de veu - Inicia una vídeotrucada - - Esteu segur que voleu començar una conversa amb %s? - Esteu segur que voleu començar una trucada de veu? - Esteu segur que voleu començar una trucada de vídeo? - + Inicia un nou xat + Inicia una trucada + Inicia una videotrucada + Estàs segur que vols iniciar un nou xat amb %s\? + Estàs segur que vols iniciar una trucada\? + Estàs segur que vols iniciar una videotrucada\? Envia fitxers Fes una foto o un vídeo Fes una foto Fes un vídeo - Entra Crea un compte @@ -189,7 +168,9 @@ Heu oblidat la contrasenya? Usa les opcions personalitzades del servidor (avançat) Comproveu el correu electrònic per continuar el procés de registre - Registrar-se amb el correu elecrònic i el número de telefon alhora no es podrà fer fins que existeixi l\'api. Es farà el registre amb el número de telefon.\n\nPodreu afegir el correu electrònic en els paràmetres del perfil. + Registrar-se amb correu electrònic i número de telèfon alhora no es podrà fer fins que existeixi l\'api. Només es tindrà en compte el número de telèfon. +\n +\nPots afegir el correu electrònic en la configuració de perfil. Aquest servidor es vol assegurar que no sou un robot Aquest nom d\'usuari ja existeix Servidor: @@ -201,7 +182,6 @@ S\'ha enviat un correu electrònic a %s. Seguiu l\'enllaç que conté i feu clic a sota. No s\'ha pogut verificar l\'adreça del correu electrònic: assegureu-vos que heu fet clic a l\'enllaç del correu electrònic La contrasenya s\'ha reiniciat.\n\nLes sessions de tots els dispositius han estat tancades i no rebreu més notificacions automàtiques. Per tal de reactivar les notificacions, torneu a entrar en cada dispositiu. - La URL ha de començar per http[s]:// No s\'ha pogut iniciar la sessió: error de xarxa @@ -210,7 +190,6 @@ No s\'ha pogut fer el registre No s\'ha pogut fer el registre: s\'ha produït una errada en la propietat del correu electrònic Introduïu una URL vàlida - El nom d\'usuari/contrasenya és invàlid No s\'ha reconegut el testimoni d\'accés JSON mal format @@ -218,35 +197,27 @@ S\'han enviat massa peticions Aquest nom d\'usuari ja està en ús L\'enllaç del correu electrònic que encara no heu fet clic - - Llegit per - - Envia com Original Gran Mitjana Petita - "Voleu cancel·lar la baixada? Voleu cancel·lar la pujada? %d s %1$dm %2$ds - Ahir Avui - Nom de la sala Tema de la sala - Truca Trucada establerta @@ -255,18 +226,15 @@ Trucant… Trucada d\'entrada Vídeotrucada d\'entrada - Trucada de veu d\'entrada + Trucada entrant Trucada en curs… - No s\'està responent a la trucada. Ha fallat la connexió de mitjans No es pot iniciar la càmera s\'ha contestat la trucada des d\'un altre lloc - Fes una foto o un vídeo" No es poden gravar vídeos" - Informació Per poder enviar i desar adjunts, el Element necessita permís d\'accés a la galeria de fotos i vídeos.\n\nA la següent finestra emergent, doneu-li el permís d\'accés i així podreu enviar fitxers des del telefon. @@ -279,97 +247,84 @@ \n \nSi accepteu compartir la vostra agenda de contactes amb aquesta finalitat, si us plau permeteu l\'accés de la següent finestra emergent. Per tal de trobar altres usuaris de Matrix a partir dels seus correus electrònics o dels seus números de telefon, el Element necessita permís d\'accés a l\'agenda de contactes.\n\nPermeteu que Element accedeixi als vostres contactes? - No s\'ha realitzat l\'acció per falta de permisos - Desat Desar a baixades? NO Continua - Elimina Uneix-te Previsualitza Rebutja - - Salta al primer missatge no llegit. - + Salta fins al primer missatge no llegit. - L\'usuari de nom %s t\'ha convidat a unir-te a aquesta sala - La invitació s\'ha enviat a però aquest correu electrònic no està associat a aquest compte %s.\nPotser voleu iniciar una sessió amb un compte diferent o afegir aquest correu electrònic a aquest compte. - Esteu intentant accedir a %s. Voldríeu unir-vos per tal de participar en la discussió? + L\'usuari %s t\'ha convidat a unir-te a aquesta sala + Aquesta invitació s\'ha enviat a %s, que no està associat amb aquest compte. +\nPotser hauries d\'iniciar sessió amb un compte diferent o afegir aquest correu electrònic al teu compte. + Estàs intentant accedir a %s. Vols unir-te per poder participar en la discussió\? una sala Aquesta és una previsualització de la sala. Les interacions de la sala estan deshabilitades. - - Xat nou + Nou xat Afegeix un participant 1 participant - Abandona la sala Esteu segurs que voleu sortir de la sala\? Esteu segur que voleu eliminar %s d\'aques xat? Crea - En línia Fora de línia Inactiu - EINES D\'ADMINISTRACIÓ TRUCADA XATS DIRECTES DISPOSITIUS - Convida Deixa aquesta sala Elimina d\'aquesta sala Veta Treu el vet - Fes-lo usuari normal + Retorna\'l a usuari normal Fes-lo moderador Fes-lo administrador - Amaga tots els missatges d\'aquest usuari - Mostra tots els missatges d\'aquest usuari - ID de l\'usuari, nom o correu electrònic + Ignora + Deixa d\'ignorar + ID d\'usuari, nom o correu electrònic Menciona Mostra la llista de dispositius - No podreu desfer aquest canvi ja que esteu donant a aquest usuari els mateixos permisos que teniu.\nN\'esteu segur? - + No podràs desfer aquest canvi ja que estàs donant a l\'usuari el mateix nivell d\'autoritat que el teu. +\nN\'estàs segur\? "Esteu segur que voleu convidar a %s a aquest xat?" - Esteu segur que voleu vetar aquest usuari en aquesta conversa? - + Si vetes un usuari, se l\'expulsarà d\'aquesta sala i no podrà tornar a unir-s\'hi. Convida per ID CONTACTES LOCALS (%d) DIRECTORI D\'USUARI (%s) Només usuaris de Matrix - Convida un usuari per ID Entreu un o més correus electrònics o IDs de Matrix Correu electrònic o ID de Matrix - Cerca %s està escrivint… %1$s & %2$s estan escrivint… %1$s & %2$s & altres estan escrivint… Envia un missatge xifrat… - Envia un missatge (desxifrat)… - La connexió amb el servidor s\'ha perdut. - Els missatges no s\'han enviat. %1$s o %2$s ara? - Els missatges no s\'han enviat perquè hi ha disposistius desconeguts. %1$s o %2$s ara? + Envia un missatge (no encriptat)… + La connectivitat amb el servidor s\'ha perdut. + Missatges no enviats. %1$s o %2$s ara\? + Missatges no enviats ja que hi ha sessions desconegudes. %1$s o %2$s ara\? Reenvia-ho tot Cancel·la-ho tot Reenvia els missatges no enviats Elimina els missatges no enviats No s\'ha trobat el fitxer - No teniu permís per escriure en aquesta sala - + No tens permís per publicar res en aquesta sala Confia No hi confiïs @@ -382,7 +337,6 @@ El certificat ha canviat respecte aquell en el qual el telefon confia. Això NO ÉS GENS HABITUAL. Es recomana que NO ACCEPTEU el certificat nou. El certificat en el que confiàveu ha canviat per un en el que no confieu. El servidor pot haver renovat el certificat. Contacteu amb l\'administrador del servidor per saber l\'empremta digital esperada. Només accepteu el certificat si l\'administrador del servidor ha publicat una empremta digital que coincideixi amb l\'anterior. - Detalls de la sala Participants @@ -391,15 +345,13 @@ L\'ID és incorrecte. Ha de ser una adreça de correu electrònic o un identificador de Matrix com \'@partlocal:domini\' CONVIDATS S\'HAN UNIT - Motiu per informar d\'aquest contingut - Voleu amagar tots els missatges d\'aquest usuari? - -Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar una estona. + Vols amagar tots els missatges d\'aquest usuari\? +\n +\nTingues en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una estona. Cancel·la la pujada Cancel·la la baixada - Cerca Filtra els participants de la sala @@ -408,7 +360,6 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar MISSATGES PARTICIPANTS FITXERS - UNEIX-TE DIRECTORI @@ -421,30 +372,25 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Uneix-te a la sala Uneix-te a una sala Escriviu un id de sala o un àlies de sala - Navega pel directori S\'està cercant al directori… - Preferit Treu prioritat Xat directe Deixa la conversa Oblida - Afegeix una drecera de la pantalla d\'inici - + Afegeix a la pantalla d\'inici Missatges Configuració Versió Termes i condicions - Avisos de terceres parts + Avisos de tercers Copyright Política de privacitat - - Foto del perfil Nom a mostrar Correu electrònic @@ -453,12 +399,10 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Afegeix un número de telèfon Mostra la informació de l\'aplicació als paràmetres del sistema. Informació de l\'aplicació - So de les notificacions Habilita les notificacions d\'aquest compte Habilita les notificacions d\'aquest dispositiu Encén la pantalla durant 3 segons - Missatges que contenen el meu nom Missatges que contenen el meu nom d\'usuari Missatges en xats entre dos @@ -466,13 +410,11 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Quan em convidin a una sala Invitacions de trucada Missatges enviats per un bot - Inicia en arrencar Sincronització en segon pla Habilita la sincronització en segon pla Temps màxim d\'espera de la petició de sincronització Retard entre cada petició - Versió Versió d\'OLM Termes i condicions @@ -482,7 +424,6 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Esborra la memòria cau Esborra la memòria cau multimèdia Manté els elements multimèdia - Configuració de l\'usuari Notificacions Usuaris ignorats @@ -494,19 +435,16 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Permís dels contactes País de l\'agenda de telèfons Pantalla d\'inici - Destaca les sales amb notificacions sense llegir + Fixa les sales amb notificacions no llegides Destaca les sales amb missatges sense llegir Dispositius Previsualitzacions dels URL en línia Mostra sempre l\'hora a tots els missatges Mostra l\'hora en el format de 12 hores Vibra quan mencionin un usuari - Analítiques - Mode d\'estalvi de dades - Detalls del dispositiu ID Nom @@ -517,22 +455,18 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Autenticació Contrasenya: Tramet - Registrat com a Servidor Servidor d\'identitat - Interfície d\'usuari Llengua Seleccioneu una llengua - Verificació pendent Mireu el correu electrònic feu clic en l\'enllaç que s\'ha enviat. Un cop fet això, feu clic per continuar. No s\'ha pogut verificar l\'adreça de correu electrònic. Comproveu el correu electrònic i feu clic en l\'enllaç que s\'ha enviat. Una vegada fet això, feu clic per continuar. Aquest correu electrònic ja està en ús. No s\'ha trobat aquesta adreça de correu electrònic. Aquest número de telèfon ja està en ús. - Canvia la contrasenya Contrasenya actual Contrasenya nova @@ -542,13 +476,9 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Mostra tots els missatges des de %s? Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar una estona. - Esteu segur que voleu esborrar aquesta notificació? - Esteu segur que voleu esborrar el %1$s %2$s? - Escull un país - País Escolliu un país Número de telèfon @@ -558,30 +488,23 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Introduïu un codi d\'activació S\'ha produït un error mentre es validava el número de telèfon Codi - - - Estil - + Insígnia Tres dies Una setmana Un mes Per sempre - - Foto de la sala Nom de la sala Tema Etiqueta de la sala Etiquetat com a: - Preferit Prioritat baixa Cap - Accés i visibilitat Mostra aquesta sala al directori de sales @@ -589,22 +512,18 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Permisos de lectura de l\'historial de la sala Qui pot llegir l\'historial\? Qui pot accedir a la sala? - Ningú - Només els participants (a partir del moment en què es selecciona aquesta opció) - Només els participants (des de que són convidats) - Només els participants (des de que s\'uneixen a la sala) - + Només participants (a partir del moment en què es seleccioni aquesta opció) + Només participants (des de que són convidats) + Només participants (des de que s\'uneixen a la sala) La sala ha de tenir una adreça per tal d\'unir-s\'hi. - Només les persones convidades - Qualsevol que conegui l\'enllaç de la sala, excepte els convidats - Qualsevol que conegui l\'enllaç de la sala, inclosos els convidats - + Només persones que hagin estat convidades + Qualsevol que tingui l\'enllaç de la sala, a part dels convidats + Qualsevol que tingui l\'enllaç de la sala, inclosos els convidats Usuaris vetats - Avançat ID intern d\'aquesta sala @@ -614,43 +533,33 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Encriptació d\'extrem a extrem L\'encriptació d\'extrem a extrem està acitva Necessiteu desconnectar-vos per poder habilitar l\'encriptació. - Encripta només per a dispositius verificats - No enviïs missatges encriptats en aquesta sala des d\'aquest dispositiu a dispositius no verificats. - + Encripta només a sessions verificades + No enviïs mai, des d\'aquesta sessió, missatges encriptats a sessions no verificades d\'aquesta sala. Aquesta sala no té adreces locals - Adreça nova (per exemple #foo:matrix.org") - - Aquesta sala no pertany a cap comunitat - Nova ID de comunitat (per exemple +foo:matrix.org") + Adreça nova (p.e. #foo:matrix.org) + Aquesta sala no mostra insígnies per a cap comunitat + Nou ID de comunitat (p.e. +foo:matrix.org) l\'ID de comunitat és invalid - \'%s\' no és una ID de comunitat vàlida - - + \'%s\' no és un ID de comunitat vàlid Format d\'àlies invàlid \'%s\' no és un format d\'àlies vàlid - No tindreu especificada una adreça principal per aquesta sala. + No tindràs cap adreça principal especificada per a aquesta sala. Avisos de l\'adreça principal - Estableix com a adreça principal Treu com a adreça principal Copia l\'ID de la sala Copia l\'adreça de la sala - L\'encriptació està activada en aquesta sala. L\'encriptació està desactivada en aquesta sala. Activa l\'encriptació \n(avís: no es podrà tornar a desactivar!) - Directori Tema - %s ha intentat carregar un moment concret de la línia de temps d\'aquesta sala però no l\'ha trobat. - Informació de l\'encriptació d\'extrem a extrem - Informació d\'esdeveniment Id de l\'usuari Clau de la identitat Curve25519 @@ -658,15 +567,13 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Algoritme ID de la sessió Error de desencriptació - Informació del dispositiu que envia Nom del dispositiu Nom - ID del dispositiu + ID de sessió Clau del dispositiu Verificació Empremta digital Ed25519 - Exporta les claus de la sala E2E Exporta les claus de la sala Exporta les claus a un fitxer local @@ -676,35 +583,32 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Les claus E2E de la sala s\'han desat a \'%s\' Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació. - Importa les claus E2E de la sala Importa les claus de la sala Importa les claus de la sala des d\'un fitxer local Importa Encripta només per a dispositius verificats No enviïs mai missatges encriptats a dispositius no verificats des d\'aquest dispositiu. - NO verificat Verificat Bloquejat - dispositiu desconegut cap - Verifica No verifiquis Bloqueja Deixa de bloquejar - Verifica el dispositiu Per tal de verificar que es pot confiar amb aquest dispositiu, contacteu amb el seu propietari per algun altre mitjà (per exemple en persona o trucant-lo) i pregunteu-li si la clau que veu a les seves preferències d\'usuari coincideix amb la clau següent: Si coincideix, premeu el botó per verificar. Si no coincideix, algú està interceptant aquest dispositiu i probablement voldreu prémer el botó per bloquejar-lo. En un futur aquest procés de verificació serà més sofisticat. Verifica que les claus coincideixen - - La sala conté dispositius desconeguts - Aquesta sala conté dispositius desconeguts que no han estat verificats.\nAixò vol dir que no hi ha cap garantia que aquests dispositius siguin dels usuaris corresponents.\nEs recomana que feu el procés de verificació per a cada dispositiu abans de continuar, tot i que, si ho preferiu, podeu reenviar el missatge sense verificar.\n\nDispositius desconeguts: - + La sala conté sessions desconegudes + Aquesta sala conté sessions desconegudes que no han estat verificades. +\nAixò vol dir que no hi ha garanties de que aquestes sessions pertanyin als usuaris que diuen ser. +\nRecomanem que, abans de continuar, duguis a terme el procés de verificació de cadascuna de les sessions. Però, si ho prefereixes, pots reenviar el missatge sense la verificació. +\n +\nDispositius desconeguts: Escolliu un directori de sale És possible que el servidor no estigui disponible o que estigui sobrecarregat @@ -712,10 +616,8 @@ Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació.URL del servidor base Totes les sales del servidor %s Totes les sales natives de %s - Busca a l\'historial - Mida de la font Molt petita @@ -725,46 +627,38 @@ Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació.Molt gran Més gran Enorme - - No teniu permisos per a gestionar ginys en aquesta sala + Necessites permisos per gestionar ginys en aquesta sala Ha fallat la creació del giny Fes conferències amb jitsi Confirmeu que voleu esborrar el giny d\'aquesta sala? - No s\'ha pogut crear el giny. No s\'ha pogut enviar la sol·licitud. El nivell de potència ha de ser un enter positiu. - No us trobeu en aquesta sala. - No teniu el permis per fer això en aquesta sala. + No et trobes en aquesta sala. + No tens permís per fer això en aquesta sala. Falta l\'ID de la sala en la sol·licitud. Falta l\'ID d\'usuari en la sol·licitud. La sala %s no és visible. Afegeix aplicacions de Matrix Utilitza la càmera nativa - El nou dispositiu \'%s\' que heu afegit, sol·licita les claus d\'encriptació. El vostre dispositiu \'%s\' sense verificar, sol·licita les claus d\'encriptació. Inicia la verificació Comparteix sense verificar Ignora la sol·licitut - Avís! - Les trucades per a conferències estan en desenvolupament i poden no ser fiables. - + Les conferències estan en desenvolupament i pot ser que no funcionin bé. Error de comandament Ordre no reconegut: %s - Apagat - Sorollós - - Missatge encriptat - + Amb so + Missatge xifrat Crea Crea una comunitat @@ -772,58 +666,49 @@ Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació.Exemple ID de la comunitat exemple - Inici Usuaris Sales Sense usuaris - Sales S\'hi ha unit Ha sigut convidat Filtre de participants de grups Filtra els grups de sales - L\'administrador de la comunitat no ha fet una descripció llarga per aquesta comunitat. - - %2$s l\'ha fet fora de la sala %1$s + %2$s t\'ha expulsat de %1$s %2$s l\'ha expulsat de la sala %1$s Raó: %1$s Tornar-hi a entrar Oblida la sala Carregant… - Surt Comunitats - Llista de grups - - Tots els missatges (sorollós) + Tots els missatges (amb so) Tots els missatges Només mencions Silencia Notificacions - Sacseja el dispositiu amb ràbia per a informar d\'un error - + Sacseja el dispositiu amb ràbia per informar d\'un error Accions Llista de membres Sincronitzant… - 1 membre actiu + %d membre actiu %d membres actius - 1 membre + %d membre %d membres - 1 nou missatge - %d nous missatges + %d missatge nou + %d missatges nous - - 1 sala + %d sala %d sales @@ -831,93 +716,76 @@ Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació.%1$s sales trobades per %2$s - 1 sala + %d sala %d sales Obre la capçalera - 1 missatge de notificació sense llegir - %d missatges de notificació sense llegir + %d missatge notificat no llegit + %d missatges notificats no llegits - 1 canvi de membres + %d canvi de membres %d canvis de membres - - 1 missatge de notificació sense llegir - %d missatges de notificació sense llegir + %d missatge notificat no llegit + %d missatges notificats no llegits %1$s a %2$s - - 1 complement actiu - %d complements actius + %d giny actiu + %d ginys actius - Envia un adhesiu - Envia un adhesiu Llicències de tercers - - Descarregar - Parlar - Netejar - Enviar veu - + Baixa + Parla + Esborra + Envia veu seguir amb… Ho sento, no s\'ha trobat cap aplicació externa per completar l\'acció. - Tornar a demanar les claus d\'encriptació als teus altres dispositius. - Petició de clau enviada. - Sol·licitud enviada Si us plau, engega Element a un altre dispositiu que pugui desencriptar el missatge de manera que pugui enviar la clau a aquest dispositiu. - Normal Tema Status.im - - Manquen permisos per a dur a terme aquesta acció. + No es pot dur a terme aquesta acció per falta de permisos. Error - Alertes de sistema - - Si és possible, escriviu si us plau la descripció en anglès. - Actualment no teniu cap conjunt d\'adhesius activat. - -En voleu afegir algun? - + Si és possible, escriu la descripció en anglès. + Encara no tens cap paquet d\'adhesius activat. +\n +\nEn vols afegir algun\? - 1s + %ds %ds - 1m + %dm %dm - 1h + %dh %dh - 1d + %dd %dd - - Ara %1$s + %1$s, ara %1$s fa %2$s - "%1$s,· " %1$s i %2$s %1$s %2$s - Envieu una resposta encriptada… - Envieu una resposta (sense encriptar)… + Envia una resposta (no encriptada)… - 1 escollit - %d escollits + %d seleccionat + %d seleccionats Versió %s Notificació de privacitat @@ -928,34 +796,26 @@ En voleu afegir algun? • El contingut dels missatges de les notificacions s\'obté de forma segura des del servidor de Matrix • Les notificacions contenen meta dades i dades de missatges • Les notificacions no mostraran el contingut dels missatges - Mostra el contingut multimèdia abans d\'enviar-lo - Desactivar el compte Desactivar el meu compte - Notificació de privacitat El Element pot funcionar en segon pla per gestionar les vostres notificacions de forma segura i privada. Això podria afectar el consum de bateria. Concedir permís Escolliu una altra opció - Envia dades d\'anàlisi Element recopila dades d\'anàlisi anònimes per tal de permetre\'ns millorar l\'aplicació. Si us plau, activeu les dades d\'anàlisi per ajudar-nos a millorar Element. Sí, vull ajudar! - - No sou ara mateix membre de cap comunitat. - + Ara mateix no ets membre de cap comunitat. Creeu una frase de pas per xifrar les claus exportades. Haureu d\'introduir la mateixa frase de pas per poder importar les claus. Crea frase de pas Les frases de pas han de coincidir Escriviu aquí… - Falta un paràmetre necessari. Un paràmetre no és vàlid. Premeu \"Enter\" per enviar el missatge Envia missatges de veu - Mostra l\'acció Veta l\'usuari amb l\'ID proporcionat Permeteu de nou l\'usuari amb l\'ID proporcionat @@ -965,74 +825,57 @@ En voleu afegir algun? Entreu a la sala amb l\'àlies donat Sortir de la sala Definir el motiu de la sala - Fer fora l\'usuari amb l\'ID proporcionat + Expulsa l\'usuari amb l\'ID proporcionat Canvia l\'àlies que es mostra Activa/Desactiva el markdown Arreglar la gestió de les Apps de Matrix - - 1 membre + %d membre %d membres - - 1 sala + %d sala %d sales Imatge de perfil - Per poder continuar usant el servidor %1$s heu de revisar i acceptar les clàusules i condicions. Revisa ara - Desactiva el compte - Això farà que no pugueu usar més el vostre compte. No podreu iniciar la sessió i ningú podrà donar-se d\'alta amb el mateix identificador d\'usuari. Això provocarà que el vostre compte abandoni totes les sales a les que estigui participant i eliminarà les vostres dades del compte que hi hagi al vostre servidor d\'identitats. Aquesta acció és irreversible. + Això farà que no puguis utilitzar més el teu compte. No podràs iniciar sessió i ningú podrà tornar-se a registrar amb el mateix ID d\'usuari. Això farà que el teu compte surti de totes les sales a les que estigui participant i eliminarà les dades del compte del teu servidor d\'identitat. Aquesta acció és irreversible. \n -\nDesactivar el compte no implica que s\'eliminin els missatges que heu enviat. Si voleu eliminar-los, marqueu la casella a continuació. +\nDesactivar el compte no implica que s\'oblidin els missatges que has enviat. Si vols que ens n\'oblidem, marca la casella a continuació. \n -\nLa visibilitat dels missatges a Matrix és similar a la del correu electrònic. Que eliminem els vostres missatges significa que els missatges que hagueu enviat no seran accessibles per a nous usuaris o usuaris no registrats, però els usuaris registrats que ja hi tinguin accés, en conservaran una còpia. +\nLa visibilitat dels missatges a Matrix és similar a la del correu electrònic. Que oblidem els teus missatges vol dir que els missatges que hagis enviat no seran accessibles per a nous usuaris o usuaris no registrats, però els usuaris registrats que ja hi tinguin accés, en conservaran una còpia. Si us plau elimina tots els missatges que he enviat mentre es desactiva el meu compte (Avís: això provocarà que els usuaris futurs tinguin una vista incompleta de les converses) Per a continuar, si us plau escriviu la vostra contrasenya: Desactivar el compte - Si us plau escriviu la vostra contrasenya. - Aquesta sala s\'ha substituït i ja no es troba activa + Aquesta sala s\'ha substituït i ja no està activa La conversa segueix aquí - Aquesta sala és la continuació d\'una altra conversa + Aquesta sala és un continuació d\'una altra conversa Feu clic aquí per veure els missatges antics - S\'ha sobrepassat el límit de recursos Contacta amb l\'administrador - Contacteu amb l\'administrador del servei - Aquest servidor base ha sobrepassat un dels seus límits de recursos, així que alguns usuaris no podran identificar-s\'hi. Aquest servidor base ha sobrepassat un dels seus límits de recursos. - Aquest servidor base ha assolit el seu límit màxim mensual d\'activitat d\'usuaris i alguns usuaris no podran identificar-s\'hi. Aquest servidor base ha assolit el seu límit mensual d\'activitat d\'usuaris. - Si us plau %s per tal d\'incrementar aquest límit. - "Si us plau %s per continuar usant aquest servei." - - Torna a carregar els membres de la sala - Milloreu el rendiment carregant només els membres de la sala a primera vista. - El vostre servidor base encara no suporta la càrrega en diferit de membres d\'una sala. Proveu-ho més tard. - + Si us plau %s per continuar utilitzant aquest servei. + Carrega en diferit els participants de la sala + Millora el rendiment carregant només els participants de la sala a primera vista. + El teu servidor encara no és compatible amb la càrrega en diferit dels participants d\'una sala. Prova-ho més tard. Ho sentim, s\'ha produït un error - desplega plega - - Acceptar - + Accepta Trucada - Useu el to de Element per defecte per les trucades entrants + Utilitza el to de trucada d\'Element predeterminat per trucades entrants To de trucada entrant - Escolliu el to per les trucades: - - Expulsar + Tria el to per les trucades: + Expulsa Motiu - Mostra la vista prèvia dels enllaços dins del xat en cas que el vostre servidor base suporti aquesta funcionalitat. Envia notificacions d\'escriptura Feu saber a altres usuaris que esteu escrivint. @@ -1040,42 +883,34 @@ En voleu afegir algun? Doneu format a missatges usant la sintaxi Markdown abans d\'enviar-los. Això us permetrà l\'ús de format avançat com ara usar asteriscs per mostrar text en cursiva. No afecta invitacions, expulsions i bloquejos. Mostra els esdeveniments del compte - Truca de totes maneres + Truca igualment Reviseu i accepteu les polítiques d\'aquest servidor base: - - Trucada de vídeo en procés… - + Videotrucada en procés… Executa les proves S\'està executant… (%1$d de %2$d) - El diagnòstic bàsic és correcte. Si encara no rebeu notificacions, envieu un informe d\'error per ajudar-nos a investigar. + El diagnòstic bàsic és correcte. Si encara no reps notificacions, envia un informe d\'errors per ajudar-nos a investigar-ho. Ha fallat una o més proves, proveu les solucions proposades. Paràmetres del sistema. Les notificacions són habilitades als paràmetres del sistema. Les notificacions són inhabilitades als paràmetres del sistema. Comproveu els paràmetres del sistema. Obre els paràmetres - Paràmetres del compte. Les notificacions són habilitades per al vostre compte. Les notificacions són inhabilitades per al vostre compte. Comproveu els paràmetres del compte. Habilita - Paràmetres del dispositiu. Les notificacions són habilitades per a aquest dispositiu. Habilita - Repara els serveis de Google Play - Servei de notificacions El servei de notificacions s\'està executant. El servei de notificacions s\'està executant. Proveu de reiniciar l\'aplicació. Inicia el servei - Inicia\'l a l\'arrencada Inhabilita les restriccions - Optimització de bateria El Element no està afectat per l\'optimització de bateria. Mostra els esdeveniments d\'entrada i sortida @@ -1085,36 +920,28 @@ Proveu de reiniciar l\'aplicació. A la pantalla següent se us demanarà que permeteu al Element executar-se sempre al rerefons, si us plau, accepteu-ho. Contrasenya Informació addicional: %s - S\'ha habilitat el Markdown. S\'ha inhabilitat el Markdown. - Avatar de recepció Avatar de notificació Mostra l\'àrea d\'informació Sempre Per als missatges i errors Només per als errors - %1$s: %1$s: %2$s +%d %d+ No s\'ha trobat cap APK de Google Play Services vàlid. Les notificacions poden no funcionar correctament. - - Còpia de seguretat de la clau - Empra una còpia de seguretat de la clau - + Còpia de seguretat de les claus + Utilitza la còpia de seguretat de les claus Omet Fet - Paràmetres avançats de notificacions Importància de les notificacions per esdeveniment - Diagnostica les notificacions Diagnòstic de la resolució de problemes - Ha fallat una o més proves, envieu un informe d\'error per ajudar-nos a investigar-ho. - + Ha fallat una o més proves, envia un informe d\'errors per ajudar-nos a investigar-ho. Les notificacions no són permeses per a aquest dispositiu. Comproveu els paràmetres del Element. Paràmetres personalitzats. @@ -1122,7 +949,6 @@ Comproveu els paràmetres del Element. Algunes notificacions estan inhabilitades als vostres paràmetres personalitzats. No s\'ha pogut carregar les regles personalitzades, torneu-ho a provar. Comproveu els paràmetres - Comprovació dels serveis de Play L\'APK dels serveis de Google Play és disponible i al dia. El Element empra els serveis de Google Play per a lliurar les notificacions, però no sembla que estiguen configurats correctament. @@ -1135,54 +961,41 @@ Comproveu els paràmetres del Element. [%1$s] Aquest error és fora del control del Element i segons Google aquest error indica que aquest dispositiu té massa aplicacions registrades amb FCM. L\'error només ocorre en casos en què hi ha un nombre extrem d\'aplicacions, i no hauria d\'afectar un usuari normal. Afegeix un compte - Registre del testimoni S\'ha registrat correctament el testimoni FCM al servidor base. No s\'ha pogut registrar el testimoni FCM al servidor base. \n%1$s - Reinici automàtic del servei de notificacions El servei s\'ha parat i tornat a iniciar automàticament. No s\'ha pogut iniciar el servei - El servei s\'iniciarà quan s\'iniciï el dispositiu. El servei no s\'iniciarà quan el dispositiu s\'iniciï, per la qual cosa no rebreu notificacions fins que el Element s\'haja obert una vegada. Habilita l\'inici durant l\'arrencada - Comprova les restriccions del rerefons Ignora l\'optimització - - Configura les notificacions sorolloses - Configura les notificacions de les trucades + Configura les notificacions amb so + Configura les notificacions de trucada Configura les notificacions silencioses Seleccioneu el color de LED, la vibració, so… - - Gestió de claus criptogràfiques Mostra les confirmacions de lectura Feu clic en les confirmacions de lectura per obtenir una llista detallada. Concedeix el permís - S\'ha produït un error en verificar la vostra adreça de correu electrònic. - S\'ha produït un error en verificar el vostre número de telèfon. Gestiona les còpies de seguretat de la clau - Inicia la càmera del sistema en lloc de la pantalla personalitzada de la càmera. Aquesta opció requereix una aplicació de tercers per enregistrar els missatges. - L\'ordre «%s» necessita més paràmetres, o alguns paràmetres no són correctes. Silenciós Introduïu una frase de pas La frase de pas és massa feble - Suprimiu la frase de pas si voleu que el Element generi una clau de recuperació. No hi ha cap sessió de Matrix disponible - No perdeu mai els missatges xifrats - Els missatges en sales xifrades estan assegurats amb xifratge punt a punt. Només tu i els destinaris tenen les claus per a llegir aquests missatges. - -Feu una còpia de seguretat de manera segura per evitar perdre-les. + Els missatges en sales encriptades estan assegurats amb encriptació d\'extrem a extrem. Només tu i el/s destinatari/s tenen les claus per a llegir aquests missatges. +\n +\nFes una còpia de seguretat de les teves claus per evitar perdre\'ls. Estableix la frase de pas Fet Desa la clau de recuperació @@ -1190,7 +1003,6 @@ Feu una còpia de seguretat de manera segura per evitar perdre-les. S\'ha desat la clau de recuperació a «%s». Avís: és possible que calgui suprimir el fitxer si es desinstal·la l\'aplicació. - Feu una còpia Comparteix la clau de recuperació amb… Clau de recuperació @@ -1198,20 +1010,16 @@ Avís: és possible que calgui suprimir el fitxer si es desinstal·la l\'aplicac S\'ha iniciat la còpia de seguretat N\'esteu segur? És possible que perdeu l\'accés als vostres missatges si sortiu de la sessió o perdeu el dispositiu. - S\'està recuperant la versió de la còpia de seguretat… Empreu la vostra frase de pas de recuperació per desblocar el vostre historial de missatges xifrat empreu la clau de recuperació Si no coneixeu la vostra contrasenya de recuperació, podeu %s. - Empreu la vostra clau de recuperació per desblocar el vostre historial de missatges xifrat Introduïu la clau de recuperació - Recuperació de missatges - Heu perdut la vostra clau de recuperació? Podeu establir una nova a les preferències. - "[%1$s] -Aquest error és fora del control del Element. Pot ocórrer per diferents raons. És possible que funcioni si ho torneu a provar més endavant. També podeu comprovar que el servei de Google Play no està restringit a l\'ús de dades a les preferències del sistema, o que el rellotge del dispositiu marca l\'hora correcta. També pot passar amb ROM personalitzades." + [%1$s] +\nAquest error està fora del control d\'Element. Pot ser causat diferents motius. És possible que torni a funcionar més endavant. També pots comprovar, a la configuració del sistema, que els Serveis de Google Play no tinguin cap restricció de dades o que l\'hora del dispositiu sigui la correcta. També pot passar amb ROMs personalitzades. [%1$s] Aquest error és fora del control del Element. No hi ha cap compte de Google al telèfon. Obrir el gestor de comptes i afegiu un compte de Google. Les restriccions de rerefons són inhabilitades per al Element. Aquesta prova s\'hauria d\'executar emprant dades mòbils (sense wifi). @@ -1220,20 +1028,14 @@ Aquest error és fora del control del Element. No hi ha cap compte de Google al Les tasques que l\'aplicació intenta fer estaran restringides agressivament mentre estigui al rerefons, i això pot afectar les notificacions. %1$s Si un usuari deixa un dispositiu sense endollar i immòbil durant un període de temps, amb la pantalla apagada, el dispositiu entra en el mode d\'estalvi d\'energia. Això impedeix les aplicacions d\'accedir a la xarxa i ajorna les seves tasques, sincronitzacions i alarmes estàndard. - - S\'està generant la clau de recuperació emprant una frase de pas. Aquest procés pot trigar uns segons. Les vostres claus de xifratge s\'estan emmagatzemant al rerefons al vostre servidor base. La còpia inicial pot trigar alguns minuts. - - No s\'ha pogut desxifrar la còpia de seguretat amb aquesta frase de pas: verifiqueu que la frase de pas que heu introduït és la correcta. Error de xarxa: comproveu la vostra connectivitat i torneu-ho a provar. - S\'està restaurant la còpia de seguretat: Desbloca l\'historial Introduïu una clau de recuperació No s\'ha pogut desxifrar la còpia de seguretat amb aquesta clau de recuperació: verifiqueu que heu introduït la clau correcta. - S\'ha restaurat la còpia de seguretat %s! S\'ha restaurat una còpia amb %d clau. @@ -1243,24 +1045,16 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men S\'ha afegit %d clau nova a aquest dispositiu. S\'ha afegit %d claus noves a aquest dispositiu. - No s\'ha pogut obtenir la versió de les claus de recuperació més recents (%s). La criptografia de la sessió no és activa - - Restaura des de la còpia de seguretat Suprimeix la còpia de seguretat - S\'ha configurat la còpia de seguretat de la clau correctament per a aquest dispositiu. La còpia de seguretat de la clau no és activa en aquest dispositiu. No s\'està fent còpia de seguretat de les vostres claus en aquest dispositiu. - - S\'està suprimint la còpia de seguretat… No s\'ha pogut suprimir la còpia de seguretat (%s) - Suprimeix la còpia de seguretat - La còpia de seguretat té una signatura d\'un dispositiu desconegut amb ID %s. La còpia de seguretat té una signatura vàlida d\'aquest dispositiu. La còpia de seguretat té una signatura vàlida del dispositiu verificat %s. @@ -1268,28 +1062,23 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men La còpia de seguretat té una signatura no vàlida del dispositiu verificat %s La còpia de seguretat té una signatura no vàlida del dispositiu no verificat %s No s\'ha pogut obtenir la informació de confiança per a la còpia de seguretat (%s). - Voleu suprimir la còpia de les vostres claus de xifratge del servidor? Ja no podreu emprar la vostra clau de recuperació per llegir el vostre historial de missatges xifrats. - - La còpia de seguretat de les claus no ha finalitzat, espereu… - Perdreu els vostres missatges xifrats si sortiu ara - S\'està fent la còpia de seguretat de les claus. Si sortiu ara, perdreu accés als vostres missatges xifrats. + La còpia de seguretat de les claus no ha finalitzat, espera… + Si tanques sessió ara, perdràs els teus missatges xifrats + S\'està fent la còpia de seguretat de les claus. Si tanques sessió ara, perdràs els teus missatges xifrats. No vull els meus missatges xifrats - S\'està fent una còpia de les claus… - Empra la còpia de la clau - N\'esteu segur? - Fes una còpia - Roman + Fent còpia de seguretat de les claus… + Utilitza la còpia de seguretat de les claus + N\'estàs segur\? + Còpia de seguretat + Queda\'t Avorta - - Esteu segurs que voleu sortir de la sessió\? - + Estàs segur que vols tancar la sessió\? Recuperació de missatges xifrats Introduïu un nom d\'usuari. Comenceu a emprar la còpia de la clau (Avançat) Exporta les claus manualment - Assegureu la vostra còpia amb una frase de pas. S\'està creant una còpia de seguretat O, assegureu la vostra còpia amb una clau de recuperació, desant-la en un lloc segur. @@ -1300,35 +1089,27 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men Comparteix No perdeu mai els missatges xifrats Comenceu a emprar la còpia de seguretat de les claus - No perdeu mai els missatges xifrats Empra la còpia de seguretat de la clau - Claus de missatges xifrats noves Gestiona en la còpia de la clau - S\'està fent una còpia de seguretat de les claus… - S\'ha fet una còpia de seguretat de totes les claus S\'està fent una còpia de seguretat d\'%d clau… S\'està fent una còpia de seguretat de %d claus… - Versió Algoritme Signatura - He sigut jo Còpia de seguretat nova de la clau - Per evitar la pèrdua d\'accés als vostres missatges encriptats, hauríeu d\'activar la còpia de seguretat encriptada a tots els vostres dispositius. - Perdreu accés als vostres missatges encriptats si no feu una còpia de seguretat de les vostres claus abans de sortir de la sessió. - + Per evitar la pèrdua d\'accés als teus missatges xifrats, hauries d\'activar la còpia de seguretat segura a totes les teves sessions. + Perdràs l\'accés als teus missatges xifrats si no fas una còpia de seguretat de les teves claus abans de tancar la sessió. Es desarà una còpia encriptada de les vostres claus al vostre servidor base. Protegiu la vostra còpia de seguretat amb una contrasenya per tal de mantenir-la segura. \n \nPer màxima seguretat, aquesta contrasenya hauria de ser diferent de la contrasenya del vostre compte. El mode d\'estalvi de dades aplica un filtre específic que evita l\'enviament de notificacions de presència i d\'escriptura. - En cas que oblideu la vostra contrasenya, la vostra clau de recuperació és un últim recurs per recuperar l\'accés als vostres missatges xifrats. \nDeseu aquesta clau de recuperació en un lloc molt segur, com ara un gestor de contrasenyes o una caixa forta Deseu la vostra clau de recuperació en un lloc molt segur, com ara un gestor de contrasenyes o una caixa forta @@ -1339,19 +1120,16 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men S\'ha trobat una còpia de seguretat nova de la clau. \n \nSi no heu configurat el mètode de recuperació nou, un atacant podria estar intentant accedir al vostre compte. Canvieu la contrasenya del vostre compte i configureu-hi un mètode de recuperació nou immediatament a la configuració. - Inicialitzar servei - Ignorar - - Iniciar sessió amb Single Sign-on + Inicialitzant servei + Ignora + Inicia sessió amb la inscripció única (SSO) Aquesta URL no està disponible , si us plau verifiqueu-la El vostre dispositiu està usant una versió obsoleta del protocol de seguretat TLS, vulnerable a atacs. Per a la vostra seguretat no us podreu connectar Envieu un missatge amb Enter La tecla Enter del teclat virtual enviarà un missatge en comptes d\'afegir un salt de línia - Actualitzar la contrasenya La contrasenya no és vàlida Les contrasenyes no coincideixen - Medis Compressió estàndard Escollir @@ -1359,34 +1137,27 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men Escollir Resposta no vàlida en descobrir homeservers Usar Config - - Verificar dispositiu - - Marcar com a llegit + Verifica sessió + Marca-ho com a llegit Les app no necessita connectar-se al HomeServer en segon pla, hauria de reduir el consum de bateria Administrador d\'integracions - Reproduir el so de disparador - IP desconeguda - %1$s: 1 missatge + %1$s: %2$d missatge %1$s: %2$d missatges %d notificacion %d notificacions - Nou esdeveniment Sala Missatges nous Nova invitació Jo - ** Error en enviar - Si us plau obriu la sala - - Ho sentim, els dispositius amb SO Android inferior a 5.0 no suporten trucades multi-usuari amb Jitsi - + ** No s\'ha pogut enviar - si us plau, obre la sala + Ho sentim, les videoconferències amb Jitsi no són compatibles amb dispositius antics (dispositius amb Android inferior a 5.0) No heu configurat cap administrador d\'integracions. Un nou dispositiu està sol·licitant claus d\'encriptació. \nNom del dispositiu: %1$s @@ -1396,56 +1167,45 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men \nNom del dispositiu: %1$s \nVist per última vegada: %2$s \nSi no heu iniciat sessió en un altre dispositiu, ignoreu la sol·licitud. - Verificar Compartir Sol·licitud de compartició de clau Ignorar - Ja existeix una còpia de seguretat al vostre HomeServer Sembla que ja heu configurat una còpia de seguretat de claus des d\'un altre dispositiu. Voleu reemplaçar-la amb la que esteu creant\? Reemplaçar Aturar - Comprovant l\'estat de la còpia de seguretat Opcions d\'autocompleció del servidor Element ha detectat una configuració de servidor personalitzat pel domini del seu identificador d\'usuari \"%1$s\": \n%2$s Us heu desconnectat a causa de credencials incorrectes o caducades. - Verificar comparant una cadena de text curta. Per la màxima seguretat us recomanem fer això en persona o usar un altre medi de comunicació confiable. Començar la verificació Sol·licitud de verificació entrant Verificar aquest dispositiu per marcar-lo com a confiable. Confiar en dispositius d\'amistats us dona un alleujament addicional quan useu missatges encriptats end-to-end. Verificant aquest dispositiu el marcareu com a confiable, i també marcareu el vostre dispositiu com a confiable pel vostre company. - Verificar aquest dispositiu confirmant els següents emojis que apareguin a la pantalla del vostre company Verificar aquest dispositiu confirmant els següents números que sortiran a la pantalla del vostre company - Heu rebut una sol·licitud de verificació entrant. Veure sol·licitud Esperant que el vostre company confirmi… - Verificat! Heu verificat aquest dispositiu amb èxit. Els missatges segurs amb aquest usuari estan encriptats end-to-end i no serà possible llegir-los per tercers. Entesos - No surt res\? Encara no tots els clients suporten la verificació interactiva. Useu el mètode de verificació antic. Useu el mètode antic de verificació. - Verificació de clau Sol·licitud cancel·lada L\'altre part ha cancel·lat la verificació. \n%s S\'ha cancel·lat la verificació. \nMotiu: %s - Verificació de dispositiu interactiva Sol·licitud de verificació %s vol verificar el vostre dispositiu - L\'usuari ha cancel·lat la verificació El marge de temps pel procés de verificació ha expirat El dispositiu no coneix la transacció @@ -1457,71 +1217,161 @@ Les tasques que l\'aplicació intenta fer estaran restringides agressivament men La clau no coincideix L\'usuari no coincideix Error desconegut - - Editar Respondre - Tornar-ho a provar Unir-se a una sala per començar usant l\'app. Se t\'ha enviat una invitació Convidat per %s - Esteu al dia! - No teniu més missatges sense llegir + No tens més missatges sense llegir Benvingut a casa! Posar-se al dia dels missatges sense llegir Converses Els vostres missatges directes es mostraran aquí Sales Les vostres sales es mostraran aquí - Reaccions Confirmar M\'agrada Afegir reacció Veure reaccions Reaccions - Esdeveniment eliminat per l\'usuari Esdeveniment moderat per l\'administrador de la sala Última edició per %1$s el %2$s - - Esdeveniment mal format, no es pot mostrar - Crear sala nova + Crea sala nova No hi ha xarxa. Si us plau comproveu la vostra connexió a internet. Canviar Canviar de xarxa Espereu, si us plau… Totes les comunitats - Aquesta sala no es pot pre-visualitzar - Element encara no suporta la pre-visualització de sales llegibles per tothom - + Element encara no admet la pre-visualització de les sales llegibles per tothom Sales Missatges directes - Sala nova CREAR - Nom de la sala + Nom Públic Qualsevol podrà unir-se a aquesta sala Directori de sales Publicar aquesta sala al directori de sales - Hi ha hagut un error rebent informació de confança Hi ha hagut un error rebent dades de la còpia de seguretat de les claus - Importar claus e2e des del fitxer \"%1$s\". - Versió de l\'SDK de Matrix Ja esteu veient aquesta sala! - Reaccions ràpides - General Preferències Seguretat i privadesa Expert - + Motiu de l\'expulsió + Expulsa usuari + Aquesta operació encara no està disponible en comptes que utilitzen la inscripció única (SSO). + Continua amb SSO + Per a la teva pròpia privadesa, Element només admet l\'enviament del \"hash\" de correus electrònics i números de telèfon. + Només admès en sales xifrades + El xifrat que utilitza aquesta sala no és compatible + No pots fer això des del mòbil + L\'aplicació no ha pogut crear un compte en aquest servidor. +\n +\nVols registrar-te utilitzant un client web\? + L\'aplicació no ha pogut iniciar sessió en aquest servidor. El servidor és compatible amb el/s següent/s tipus d\'inici de sessió: %1$s. +\n +\nVols iniciar sessió utilitzant un client web\? + No pots fer això des d\'Element per a mòbils + La cerca en sales xifrades encara no està disponible. + La sala encara no s\'ha acabat de crear. Vols cancel·lar la seva creació\? + No s\'ha trobat aquesta sala. Assegura\'t que existeixi. + Mostra detalls com per exemple noms de sala i contingut dels missatges. + No hem pogut convidar els usuaris. Comprova els usuaris que vols convidar i torna-ho a provar. + Estàs segur que vols eliminar aquest esdeveniment\? Tingues en compte que, si suprimeixes un nom de sala o es canvia el tema, podria ser que es revertís. + Una vegada activada, l\'encriptació d\'una sala no es pot desactivar. El servidor no pot llegir els missatges enviats en una sala encriptada, només els poden llegir els participants. Habilitar l\'encriptació pot fer que molts bots i enllaços no funcionin correctament. + Una vegada activada, l\'encriptació no es pot desactivar. + Notificacions + Els missatges d\'aquí no estan encriptats d\'extrem a extrem. + Els missatges d\'aquesta sala no estan encriptats d\'extrem a extrem. + Una vegada activada, l\'encriptació no es pot desactivar. + Si canvies la contrasenya es restabliran les claus d\'encriptació d\'extrem a extrem de totes les teves sessions de manera que l\'historial del xat no es podrà llegir. Configura una còpia de seguretat de les claus o exporta les teves claus de sala d\'una altra sessió abans de fer el canvi de contrasenya. + Has fet la sala accessible per a qualsevol que tingui l\'enllaç. + %1$s ha fet la sala accessible per a qualsevol que tingui l\'enllaç. + Silencia + Només mencions + Tots els missatges + Tots els missatges (amb so) + En aquesta sala no hi ha mitjans + En aquesta sala no hi ha fitxers + No s\'ha trobat cap resultat, utilitza Afegeix amb ID de matrix per a cercar al servidor. + La sala ha estat creada però algunes invitacions no s\'han enviat pel motiu següent: +\n +\n%s + No hi ha ginys actius + %1$s s %2$s i %3$s + Si deixes d\'ignorar aquest usuari, tornaràs a veure tots els seus missatges. + Deixa d\'ignorar + Si ignores aquest usuari s\'eliminaran els seus missatges de les sales que compartiu. +\n +\nPots desfer aquest canvi en qualsevol moment a la configuració general. + Ignora usuari + No podràs desfer aquest canvi ja que t\'estàs baixant de rang, si ets l\'últim usuari de la sala amb privilegis, et serà impossible recuperar-los. + No s\'ha configurat cap servidor d\'identitat. + Sense més resultats + Notificacions + Èxit + Copia + Penja + Rebutja + Accepta + Rebutja + No s\'ha pogut eliminar el giny + No s\'ha pogut afegir el giny + No pots iniciar una trucada amb tu mateix, espera que els participants acceptin la invitació + No pots iniciar una trucada amb tu mateix + Les reunions utilitzen les polítiques de seguretat i permisos de Jitsi. Tots els participants que es trobin dins sala veuran una invitació per unir-se mentre la teva reunió estigui en curs. + Inicia una reunió d\'àudio + Inicia una reunió de vídeo + Ja hi ha una conferència en curs! + No tens permís per iniciar una trucada + No tens permís per iniciar una trucada en aquesta sala + No tens permís per iniciar una conferència + No tens permís per iniciar una videoconferència en aquesta sala + Reinicia + Omet + Atura + Desconnecta + Revoca + Cap + Latn + Trucada activa (%s) + Demana confirmació abans d\'iniciar una trucada + Evita trucada accidental + La descripció és massa curta + Envia l\'historial de sol·licituds de compartició de claus + Revisa + Reprodueix + Activa l\'HD + Desactiva l\'HD + Posterior + Frontal + Commuta la càmera + Auriculars sense fils + Auriculars + Altaveu + Mòbil + Selecciona el dispositiu d\'àudio + No m\'ho tornis a preguntar + Prova fent servir %s + La crida ha fallat per culpa d\'una mala configuració del servidor + Correu electrònic + Confirma la teva contrasenya + Motiu del veto + Veta usuari + Cancel·la invitació + Cancel·la invitació + Torna a la trucada + Error SSL. + La trucada d\'Element ha fallat + \ 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 702e380e17..75ee23cb12 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2244,4 +2244,8 @@ Raumeinstellungen Raum-Thema (optional) Raumname + Prüfung exportieren + Direktnachricht + Geschichte der Anfragen von Schlüsselfreigaben senden + Keine weiteren Ergebnisse \ No newline at end of file diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index c5ca0d5f1a..a26c4c91d6 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1,17 +1,15 @@ - + es ES - Mensajes Sala Ajustes Detalles de Miembro Histórico - Correcto Cancelar @@ -26,7 +24,7 @@ Reenviar Enlace Permanente Ver Fuente - Ver Fuente Descifrada + Ver Fuente Desencriptada Eliminar Renombrar Reportar contenido @@ -35,16 +33,15 @@ \nUnirse como %1$s o %2$s Voz Vídeo - No se puedo iniciar la llamada, por favor inténtelo de nuevo más tarde - Debido a que faltan permisos, pueden faltar algunas características… + No se puede iniciar la llamada, por favor inténtelo de nuevo más tarde + Debido a permisos insuficientes, pueden faltar algunas funciones… Necesitas permiso para invitar a iniciar una conferencia en esta sala No se puede iniciar la llamada Detalles de la sesión - No se admiten llamadas de conferencia en salas cifradas + No se admiten llamadas de conferencia en salas encriptadas Enviar de Todos Modos o Invitar - Cerrar sesión Llamada de Voz @@ -57,27 +54,22 @@ Cerrar Copiado al portapapeles Deshabilitar - Confirmación Advertencia - Inicio Favoritos Personas Salas - Filtrar salas Filtrar favoritos Filtrar personas Filtrar salas - Invitaciones Prioridad baja - Conversaciones Agenda de contactos local @@ -85,17 +77,15 @@ No hay conversaciones No permitiste que Element acceda a tus contactos locales No hay resultados - Salas Directorio de salas No hay salas No hay salas públicas disponibles - 1 usuario + %d usuario %d usuarios - Enviar registros Enviar registros de fallas Enviar captura de pantalla @@ -108,10 +98,8 @@ No se pudo enviar el informe de error (%s) Progreso (%s%%) La aplicación falló en la última sesión. ¿Te gustaría enviar un informe de error\? - Enviar en Leído - Unirse a la Sala Nombre de usuario Crear cuenta @@ -120,14 +108,11 @@ URL del Servidor Doméstico URL del Servidor de Identidad Buscar - Iniciar Nueva Conversación Iniciar Llamada de Voz Iniciar Llamada de Vídeo - Enviar archivos Tomar foto o vídeo - Iniciar sesión Crear cuenta @@ -161,8 +146,8 @@ Utilizar opciones personalizadas del servidor (avanzado) Por favor consulta tu correo electrónico para continuar con el registro Todavía no es posible registrarse con correo electrónico y número telefónico a la vez, hasta que exista la API. Solo se tendrá en cuenta el número telefónico. - -Puedes añadir tu correo electrónico a tu perfil en ajustes. +\n +\nPuedes añadir tu correo electrónico a tu perfil en ajustes. Este Servidor Doméstico quiere asegurarse de que no eres un robot Nombre de usuario en uso Servidor Doméstico: @@ -176,7 +161,6 @@ Puedes añadir tu correo electrónico a tu perfil en ajustes. Tu contraseña fue restablecida. \n \nSe ha cerrado sesión en todas tus sesiones y ya no recibirás notificaciones push. Para volver a habilitar las notificaciones, vuelve a iniciar sesión en cada dispositivo. - La URL debe comenzar con http[s]:// No es posible iniciar sesión: Error de red @@ -185,7 +169,6 @@ Puedes añadir tu correo electrónico a tu perfil en ajustes. No es posible registrarse No es posible registrarse : falló la propiedad del correo electrónico Por favor introduce una URL válida - Nombre de usuario/contraseña inválidos No se reconoció el código de acceso especificado JSON mal formado @@ -193,36 +176,27 @@ Puedes añadir tu correo electrónico a tu perfil en ajustes. Se enviaron demasiadas solicitudes Este nombre de usuario ya está en uso El enlace del correo electrónico que aún no se ha seguido - - Lista de Recibos de Lectura - - Enviar como Original Grande Mediano Pequeño - "¿Cancelar la descarga? ¿Cancelar la subida? %d s - %1$dm %2$ds - + %1$dmin %2$dseg Ayer Hoy - - Nombre de la sala Tema de la sala - Llamada conectada Conectando llamada… @@ -232,21 +206,18 @@ Puedes añadir tu correo electrónico a tu perfil en ajustes. Llamada de Vídeo Entrante Llamada de Voz Entrante Llamada En Curso… - El lado remoto no contestó. Falló la Conexión de Medios No se puede iniciar la cámara llamada contestada en otra parte - Tomar una foto o un vídeo No se puede grabar vídeo - Información Element necesita permiso para acceder a tu biblioteca de fotos y vídeos para enviar y guardar archivos adjuntos. - -Por favor permite el acceso en la próxima ventana emergente para poder enviar archivos desde tu teléfono. +\n +\nPor favor permite el acceso en la próxima ventana emergente para poder enviar archivos desde tu teléfono. Element necesita permiso para acceder a tu cámara para tomar fotos y realizar llamadas de vídeo. " \n @@ -256,61 +227,51 @@ Por favor permite el acceso en la próxima ventana emergente para poder enviar a \n \nPor favor permite el acceso en la próxima ventana emergente para poder realizar la llamada." Element necesita permiso para acceder a tu cámara y micrófono para realizar llamadas de vídeo. - -Por favor permite el acceso en las próximas ventanas emergentes para poder realizar la llamada. +\n +\nPor favor permite el acceso en las próximas ventanas emergentes para poder realizar la llamada. Element necesita permiso para acceder a tu agenda de contactos para encontrar otros usuarios de Matrix por sus correos electrónicos y números telefónicos. Por favor permite el acceso en la próxima ventana emergente para descubrir usuarios accesibles desde Element en tu agenda de contactos. - Element necesita permiso para acceder a tu agenda de contactos para encontrar otros usuarios de Matrix por sus correos electrónicos y números telefónicos. - -¿Permitir que Element acceda a tus contactos ? - + Element necesita permiso para acceder a tu agenda de contactos para encontrar otros usuarios de Matrix por sus correos electrónicos y números telefónicos. +\n +\n¿Permitir que Element acceda a tus contactos \? Lo sentimos. Acción no realizada, debido a que faltan permisos - Guardado ¿Guardar en descargas? NO Continuar - Eliminar Unirse Vista Previa Rechazar - Ir al primer mensaje no leído. - Has sido invitado por %s a unirte a esta sala Esta invitación fue enviada a %s, que no esta asociado a esta cuenta. -Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrónico a esta cuenta. +\nQuizás quieras iniciar sesión con otra cuenta, o añadir este correo electrónico a esta cuenta. Estás intentando acceder a %s. ¿Quieres unirte para participar en la discusión? una sala Esta es una vista previa de esta sala. Las interacciones dentro de la sala se han deshabilitado. - Nueva Conversación Añadir miembro 1 miembro - Salir de la sala ¿Seguro que quieres salir de la sala? ¿Seguro que quieres eliminar a %s de esta conversación? Crear - En línea Desconectado En reposo - HERRAMIENTAS DE ADMINISTRACIÓN LLAMAR CONVERSACIONES DIRECTAS SESIONES - Invitar Salir de esta sala Eliminar de esta sala @@ -324,26 +285,22 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón ID de Usuario, Nombre o correo electrónico Mencionar Mostrar Lista de Sesiones - No podrás deshacer este cambio porque estás promoviendo al usuario para tener el mismo nivel de autoridad que tú. -¿Estás seguro? - + No podrás deshacer este cambio porque estás ascendiendo al usuario al mismo nivel de autoridad que tú. +\n¿Estás seguro\? ¿Seguro que quieres invitar a %s a esta conversación? - Invitar por ID CONTACTOS LOCALES (%d) Solo usuarios de Matrix - Invitar usuario por ID Por favor, ingresa una o más direcciones de correo electrónico o ID de Matrix Correo electrónico o ID de Matrix - Buscar %s está escribiendo… %1$s y %2$s están escribiendo… %1$s y %2$s y otros están escribiendo… - Enviar un mensaje cifrado… + Enviar un mensaje encriptado… Enviar un mensaje (sin cifrar)… Se perdió la conexión con el servidor. Los mensajes no se enviaron. ¿%1$s o %2$s ahora? @@ -354,7 +311,6 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón Eliminar mensajes no enviados Archivo no encontrado No tienes permiso para publicar en esta sala - Confiar No confiar @@ -367,7 +323,6 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón El certificado cambió de uno que era confiable para tu teléfono. Esto es MUY INUSUAL. Se recomienda NO ACEPTAR este nuevo certificado. El certificado cambió de uno que era confiable a uno que no es confiable. El servidor puede haber renovado su certificado. Contacta al administrador del servidor para obtener la huella digital. Solo acepta el certificado si el administrador del servidor ha publicado una huella digital que coincide con la anterior. - Detalles de Sala Personas @@ -376,15 +331,13 @@ Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrón ID mal formada. Debería ser una dirección de correo electrónico o una ID de Matrix como \'@partelocal:dominio\' INVITADOS SE UNIERON - Motivo para reportar este contenido - ¿Quieres ocultar todos los mensajes de este usuario? - -Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de tiempo. + ¿Quieres ocultar todos los mensajes de este usuario\? +\n +\nTen en cuenta que esta acción reiniciará la aplicación y puede tardar algo de tiempo. Cancelar Subida Cancelar Descarga - Buscar Filtrar miembros de la sala @@ -393,7 +346,6 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de MENSAJES PERSONAS ARCHIVOS - UNIRSE DIRECTORIO @@ -406,18 +358,15 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Unirse a la sala Unirse a una sala Escribe una ID o alias de sala - Explorar directorio Buscando directorio… - Agregar a Favoritos Dejar de priorizar Conversación Directa Salir de la Conversación Olvidar - Mensajes Ajustes @@ -426,9 +375,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Avisos de terceros Derechos de autor Política de privacidad - - Imagen de Perfil Nombre Público Correo Electrónico @@ -437,22 +384,18 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Añadir número telefónico Mostrar la pantalla de información de la aplicación de los ajustes del sistema. Información de la aplicación - Habilitar notificaciones para esta cuenta Habilitar notificaciones para esta sesión Enciende la pantalla por 3 segundos - Mensajes en conversaciones uno a uno Mensajes en conversaciones en grupo Cuando soy invitado a una sala Invitaciones de llamada Mensajes enviados por bot - Sincronización en segundo plano Habilitar sincronización en segundo plano Venció el tiempo de espera para la solicitud de sincronización Retraso entre cada sincronización - Versión versión de olm Términos y condiciones @@ -461,8 +404,6 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Política de privacidad Borrar caché - - Ajustes de usuario Notificaciones Usuarios ignorados @@ -484,39 +425,31 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Visto por última vez %1$s @ %2$s Esta operación requiere autenticación adicional. -Para continuar, ingresa tu contraseña por favor. +\nPara continuar, introduce tu contraseña por favor. Autenticación Contraseña: Enviar - Sesión iniciada como Servidor Doméstico Servidor de Identidad - Verificación Pendiente Por favor, consulta tu correo electrónico y haz clic en el enlace que contiene. Una vez hecho esto, haz clic en continuar. No es posible verificar la dirección de correo electrónico. Por favor, consulta tu correo electrónico y haz clic en el enlace que contiene. Una vez hecho esto, haz clic en continuar. - Esta dirección de correo electrónico ya está en uso. No se encontró esta dirección de correo electrónico. Este número telefónico ya está en uso. - Cambiar contraseña Contraseña actual Contraseña nueva Confirmar contraseña No se pudo actualizar la contraseña Tu contraseña ha sido actualizada - ¿Mostrar todos los mensajes de %s? - -Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de tiempo. - + ¿Mostrar todos los mensajes de %s\? +\n +\nTen en cuenta que esta acción reiniciará la aplicación y puede tomar algo de tiempo. ¿Seguro que quieres eliminar este objetivo de notificaciones? - ¿Seguro que quieres eliminar los %1$s %2$s? - Elige un país - País Por favor, elige un país Número telefónico @@ -526,21 +459,17 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Ingresa un código de activación Error en la validación de tu número telefónico Código - - Imagen de Sala Nombre de Sala Tema Etiqueta de Sala Etiquetado como: - Agregar a Favoritos Prioridad baja Ninguno - Acceso y visibilidad Listar esta sala en el directorio de salas @@ -548,70 +477,57 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Legibilidad del Historial de la Sala ¿Quién puede leer el historial? ¿Quién puede acceder a esta sala? - Todos Solo miembros (desde el momento en que se selecciona esta opción) Solo miembros (desde que fueron invitados) Solo miembros (desde que se unieron) - Para crear un enlace a una sala, debe tener una dirección. Solo personas que han sido invitadas Cualquier persona que conozca el enlace a esta sala, excepto invitados Cualquier persona que conozca el enlace a esta sala, incluyendo invitados - Usuarios vetados - Avanzado La ID interna de esta sala Direcciones Laboratorios Estas son funcionalidades experimentales que pueden romperse de maneras inesperadas. Utilizar con precaución. - Cifrado de Extremo a Extremo - El Cifrado de Extremo a Extremo está activo - Necesitas cerrar sesión para poder habilitar el cifrado. + Encriptación de Extremo a Extremo + La encriptación de Extremo a Extremo está activa + Necesitas cerrar sesión para poder habilitar la encriptación. Cifrar solo a sesiones verificadas - Nunca enviar mensajes cifrados a sesiones sin verificar en esta sala desde esta sesión. - + Nunca enviar mensajes encriptados a sesiones sin verificar en esta sala desde esta sesión. Esta sala no tiene direcciones locales Dirección nueva (ej. #foo:matrix.org) - Formato de alias inválido \'%s\' no es un formato de alias válido No tendrás una dirección principal especificada para esta sala. Advertencias de la dirección principal - Establecer como Dirección Principal Dejar de Establecer como Dirección Principal Copiar ID de Sala Copiar Dirección de Sala - - El cifrado está habilitado en esta sala. - El cifrado está deshabilitado en esta sala. - Habilitar cifrado -(advertencia: ¡no se puede volver a deshabilitar!) - + La encriptación está habilitada en esta sala. + La encriptación está deshabilitada en esta sala. + Habilitar encriptado +\n(advertencia: ¡no se puede volver a deshabilitar!) Directorio - %s estaba intentando cargar un momento específico en la línea de tiempo de esta sala pero no pudo encontrarlo. - - Información de cifrado de extremo a extremo - + Información de encriptación Extremo-a-Extremo Información de eventos ID de Usuario Clave de identidad Curve25519 Clave de huella digital Ed25519 reclamada Algoritmo ID de Sesión - Error de descifrado - + Error de desencriptación Información de la sesión emisora Nombre público Nombre público @@ -619,45 +535,41 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Clave de sesión Verificación Huella digital Ed25519 - - Exportar claves de salas con Cifrado de Extremo a Extremo + Exportar claves de salas con encriptación Extremo-a-Extremo Exportar claves de sala Exportar las claves a un archivo local Exportar Ingresar frase de contraseña Confirmar frase de contraseña - Las claves de salas con Cifrado de Extremo a Extremo se guardaron en \'%s\'. - -Advertencia: este archivo puede ser eliminado si la aplicación se desinstala. - - Importar claves de salas con Cifrado de Extremo a Extremo + Las claves de salas con encriptado de Extremo-a-Extremo se guardaron en \'%s\'. +\n +\nAdvertencia: este archivo puede ser eliminado si la aplicación se desinstala. + Importar claves de salas con encriptación Extremo-a-Extremo Importar claves de sala Importar las claves desde un archivo local Importar Cifrar solo a sesiones verificadas - Nunca enviar mensajes cifrados a sesiones sin verificar desde esta sesión. - + Nunca enviar mensajes encriptados a sesiones sin verificar desde esta sesión. SIN Verificar Verificado Prohibido - sesión desconocida ninguno - Verificar Anular Verificación Prohibir Dejar de Prohibir - Verificar sesión Para verificar que esta sesión es confiable, por favor contacta a su dueño por algún otro medio (ej. cara a cara o por teléfono) y pregúntale si la clave que ve en sus Ajustes de Usuario para esta sesión coincide con la clave a continuación: Si coincide, presione el botón de verificar a continuación. Si no coincide, entonces alguien está interceptando esta sesión y probablemente debería prohibirlo. En el futuro, este proceso de verificación será más sofisticado. Verifico que las claves coinciden - La sala contiene sesiones desconocidas - Esta sala contiene sesiones desconocidas que no han sido verificados. Esto significa que no hay garantía de que las sesiones pertenezcan a los usuarios a los que dicen pertenecer. Recomendamos que pases por el proceso de verificación para cada sesión antes de continuar. Puedes reenviar el mensaje sin verificarlas si prefieres. Sesiones desconocidas: - + Esta sala contiene sesiones desconocidas que no han sido verificadas. +\nEsto significa que no hay garantía de que las sesiones pertenezcan a los usuarios a los que dicen pertenecer. +\nRecomendamos que hagas el proceso de verificación por cada sesión antes de continuar. Pero puedes reenviar el mensaje sin verificarlas si prefieres. +\n +\nSesiones desconocidas: Selecciona un directorio de salas El servidor puede estar no disponible o sobrecargado @@ -665,35 +577,27 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.URL del Servidor Doméstico Todas las salas en el servidor %s Todas las salas nativas de %s - Buscar en el historial Interfaz de usuario Idioma Elige idioma - Iniciar en el arranque Borrar caché de medios Guardar medios - Mostrar marcas temporales de todos los mensajes - 3 días 1 semana 1 mes Para siempre - Desconectado - Modo de ahorro de datos - Tamaño de letra Pequeño Normal Grande Mayor Tema - Diminuto Más Grande Enorme @@ -702,30 +606,23 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.Tema Claro Tema Oscuro Tema Negro - Sincronizando… - Detectar eventos + Captando eventos Notificaciones ruidosas Notificaciones silenciosas - Informe de error - Tomar foto Tomar vídeo - Llamar Sonido de notificación Mensajes que contienen mi nombre público Mensajes que contienen mi nombre de usuario Mostrar marcas temporales en formato de 12 horas - Análisis de Estadísticas - Necesitas permiso para gestionar los componentes en esta sala La creación del componente falló Crear llamadas de conferencia con jitsi ¿Seguro que quieres eliminar el widget de esta sala\? - No es posible crear el componente. El envío de la solicitud falló. @@ -737,60 +634,43 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.La sala %s no está visible. Añadir aplicaciones de Matrix Utilizar cámara nativa - - Añadiste una nueva sesión \'%s\', que está solicitando claves de cifrado. - Tu sesión sin verificar \'%s\' está solicitando claves de cifrado. + Has añadido una nueva sesión \'%s\', que está solicitando claves de encriptación. + Tu sesión sin verificar \'%s\' está solicitando claves de encriptación. Iniciar verificación Compartir sin verificar Ignorar solicitud - ¡Advertencia! Las llamadas de conferencia están en desarrollo y pueden no ser confiables. - Error de comando Comando no reconocido: %s - Desactivado Ruidoso - - Mensaje cifrado - + Mensaje encriptado Detalles de comunidad - Cargando… - Salir Comunidades - Filtrar comunidades - Invitar Comunidades No hay grupos - ¿Seguro que quieres iniciar una nueva conversación con %s? ¿Seguro que quieres iniciar una llamada de voz? ¿Seguro que quieres iniciar una llamada de vídeo? - Lista de Grupos - El usuario baneado lo echará de esta sala y evitará que se unan nuevamente. - Todos los mensajes (ruidoso) Todos los mensajes Solo menciones Silenciar - Añadir un Atajo a la Pantalla de Inicio - + Añadir a la Pantalla de Inicio Vistas previas de URL en línea Vibrar al mencionar un usuario - Insignia - Notificaciones Esta sala no está mostrando insignias para ninguna comunidad Nueva ID de comunidad (ej. +foo:matrix.org) @@ -798,48 +678,40 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.Olvidar sala Volver a unirse \'%s\' no es una ID de comunidad válida - - Crear Crear Comunidad Nombre de comunidad Ejemplo ID de Comunidad ejemplo - Inicio Personas Salas No hay usuarios - Salas Se unió Invitado Filtrar miembros del grupo Filtrar salas del grupo - Has sido expulsado de %1$s por %2$s Has sido vetado de %1$s por %2$s Motivo: %1$s El administrador de la comunidad no ha redactado una descripción larga para esta comunidad. - Agitar con rabia para reportar un error - Acciones Listar miembros Sincronizando… - 1 miembro + %d miembro %d miembros - 1 mensaje nuevo + %d mensaje nuevo %d mensajes nuevos - - 1 sala + %d sala %d salas @@ -847,58 +719,50 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.%1$s salas encontradas para %2$s - 1 sala + %d sala %d salas - 1 cambio de membresía + %d cambio de membresía %d cambios de membresía - Abrir título - 1 miembro activo + %d miembro activo %d miembros activos - 1 mensaje sin leer + %d mensaje sin leer %d mensajes sin leer - 1 mensaje notificado sin leer + %d mensaje notificado sin leer %d mensajes notificados sin leer %1$s en %2$s - - 1 componente activo + %d componente activo %d componentes activos - Enviar una pegatina - Actualmente no tienes ningún paquete de pegatinas habilitado. \n \n¿Añadir algunos ahora\? - Desactivar Cuenta Avatar - Avatar de recibo Avatar de aviso Para continuar utilizando el servidor doméstico %1$s, debes revisar y aceptar los términos y condiciones. Revisar ahora - Esto hará que tu cuenta quede permanentemente inutilizable. No podrás iniciar sesión, y nadie podrá volver a registrar la misma ID de usuario. Esto hará que tu cuenta salga de todas las salas en las cuales participa, y eliminará los datos de tu cuenta de tu servidor de identidad. Esta acción es irreversible. - -Desactivar tu cuenta no hace que por defecto olvidemos los mensajes que has enviado. Si quieres que olvidemos tus mensajes, por favor marca la casilla a continuación. - -La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Que olvidemos tus mensajes implica que los mensajes que hayas enviado no se compartirán con ningún usuario nuevo o no registrado, pero aquellos usuarios registrados que ya tengan acceso a estos mensajes seguirán teniendo acceso a su copia. +\n +\nDesactivar tu cuenta no hace que por defecto olvidemos los mensajes que has enviado. Si quieres que olvidemos tus mensajes, por favor marca la casilla a continuación. +\n +\nLa visibilidad de mensajes en Matrix es similar a la del correo electrónico. Que olvidemos tus mensajes implica que los mensajes que hayas enviado no se compartirán con ningún usuario nuevo o no registrado, pero aquellos usuarios registrados que ya tengan acceso a estos mensajes seguirán teniendo acceso a su copia. Por favor, olvida todos los mensajes enviados al desactivar mi cuenta (Advertencia: esto provocará que los usuarios futuros vean conversaciones incompletas) Para continuar, ingresa tu contraseña por favor: Desactivar Cuenta - Privacidad de notificaciones Normal Privacidad reducida @@ -908,51 +772,37 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu • El contenido del mensaje de la notificación está ubicado de forma segura directamente desde el servidor doméstico de Matrix • Las notificaciones contienen datos de mensajes y metadatos • Las notificaciones no mostrarán el contenido del mensaje - Desactivar cuenta Desactivar mi cuenta - Descargar Sí, ¡quiero ayudar! - Conceder permiso Enviar audio - Enviar pegatina Un parámetro no es válido. Ingresa tu contraseña por favor. - Enviar mensaje de voz - Elige otra opción - Falta un parámetro requerido. Solicitud enviada Conversar Por favor, inicia Element en otro dispositivo que pueda descifrar el mensaje para que pueda enviar las claves a esta sesión. - Licencias de terceros - Borrar continuar con… Lo sentimos, no se encontró ninguna aplicación externa para completar esta acción. - - Volver a solicitar las claves de cifrado de tus otras sesiones. - + Volver a solicitar las claves de encriptado de tus otras sesiones. Solicitud de clave enviada. - Privacidad de Notificaciones Element puede ejecutarse en segundo plano para gestionar tus notificaciones de forma segura y privada. Esto podría afectar la duración de la batería. Enviar datos de análisis de estadísticas Element recopila análisis de estadísticas anónimas para permitirnos mejorar la aplicación. Por favor, habilita los análisis de estadísticas para ayudarnos a mejorar Element. Escribe aquí… - Si es posible, por favor escribe la descripción en inglés. Enviar una respuesta cifrada… Enviar una respuesta (sin cifrar)… Actualmente no eres miembro de ninguna comunidad. - Utilizar la tecla Intro del teclado para enviar mensajes Muestra la acción Veta al usuario con la ID dada @@ -966,73 +816,58 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Cambia tu apodo público Activar/Desactivar markdown Para reparar la gestión de las Aplicaciones de Matrix - Vista previa de medios antes de enviar - Esta sala ha sido reemplazada y ya no está activa La conversación continúa aquí Esta sala es una continuación de otra conversación Haz clic aquí para ver mensajes más antiguos - Degrada al usuario con la ID dada Alertas de Sistema - - Debido a que faltan permisos, esta acción no es posible. + Debido a permisos insuficientes, esta acción no es posible. - 1s + $d %ds - 1m - %dm + %dmin + %dmins - 1h + %dh %dh - 1d + %dd %dd - %1$s ahora hace %1$s %2$s - "%1$s, " %1$s y %2$s %1$s %2$s - - 1 seleccionado + $d seleccionado %d seleccionados - 1 miembro + %d miembro %d miembros - - 1 sala + %d sala %d salas Límite de Recursos Excedido Contacta al Administrador - contacta al administrador de tu servicio - Este servidor doméstico ha excedido uno de sus límites de recursos, por lo que algunos usuarios no podrán iniciar sesión. Este servidor doméstico ha excedido uno de sus límites de recursos. - Este servidor doméstico ha alcanzado su límite Mensual de Usuarios Activos, por lo que algunos usuarios no podrán iniciar sesión. Este servidor doméstico ha alcanzado su límite Mensual de Usuarios Activos. - Por favor, %s para aumentar este límite. Por favor, %s para continuar utilizando este servicio. - Tema de Status.im - Error - Versión %s Por favor, crea una frase de contraseña para cifrar las claves exportadas. Necesitarás ingresar la misma frase de contraseña para poder importar las claves. Crear frase de contraseña @@ -1040,27 +875,19 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Utiliza carga diferida para los miembros de la sala Aumenta el rendimiento cargando los miembros de la sala solo en la primera vista. Tu servidor doméstico aún no admite la carga diferida de los miembros de la sala. Prueba más tarde. - Disculpas, ocurrió un error - expandir colapsar - - llamar de cada manera + Llamar de todos modos Aceptar - Por favor revisa y acepta las reglas de este servidor doméstico: - Llamadas Usar el tono de llamada normal de Element para llamadas entrantes Tono para llamadas entrantes Elegir sonido de llamadas: - Llamada de video en proceso… - Expulsar Razón - Diagnóstico de fallas Diagnóstico de errores Iniciar pruebas @@ -1068,51 +895,43 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Iniciando servicio Copia de seguridad de la clave Usar copia de seguridad de la clave - La copia de seguridad de la clave no ha finalizado, por favor espere… - No quiero mis mensajes cifrados + No quiero mis mensajes encriptados Creando copia de seguridad de las claves… Usar copia de seguridad de la clave ¿Estás seguro\? Copia de seguridad - Perderá el acceso a sus mensajes cifrados si cierra sesión sin hacer una copia de seguridad de sus claves. - + Perderá el acceso a sus mensajes encriptados si cierra sesión sin hacer una copia de seguridad de sus claves. Quedarse Saltar Hecho Cancelar Ignorar - Marcar como leído Iniciar sesión con single-sign-on Tu dispositivo usa una versión anticuada e insegura del protocolo de seguridad TLS. Por tu seguridad no puedes conectarte Ajustes avanzados de notificaciones Importancia de notificación por evento - Ajustes de sistema. Las notificaciones están activadas en los ajustes de sistema. Las notificaciones están desactivadas en los ajustes del sistema. \nPor favor comprueba los ajustes de sistema. Abrir ajustes - Ajustes de cuenta. Las notificaciones están activadas para tu cuenta. Las notificaciones están desactivadas para tu cuenta. \nPor favor comprueba los ajustes de cuenta. Activar - Ajustes de sesión. Las notificaciones están activadas para esta sesión. Las notificaciones no están habilitadas para esta sesión. \nPor favor comprueba los ajustes Element. Activar - Ajustes personalizados. Ten en cuenta que algunos mensajes son silenciosos (producen una notificación sin sonido). Algunas notificaciones están desactivadas en tus ajustes personalizados. Error al cargar reglas personalizadas, por favor prueba de nuevo. Comprueba ajustes - Prueba de servicios Google Play APK de servicios de Google Play esta disponible y actualizado. Al cerrar la sesión se perderán los mensajes encriptados @@ -1121,13 +940,11 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu El diagnóstico base se ha completado con éxito. Si aun no recibes notificaciones, por favor mándanos un informe de error. Una o más pruebas han fallado, por favor prueba las soluciones propuestas. Una o más pruebas han fallado, por favor mándanos un informe de error para que podamos investigar. - Copia de seguridad en progreso. Si cierras sesión ahora perderás el acceso a tus mensajes encriptados. - La copia de seguridad debería estar activa ahora en todas tus sesiones para evitar la pérdida del acceso a tus mensajes encriptados. + La copia de seguridad debería estar activa ahora en todas tus sesiones para evitar la pérdida de acceso a tus mensajes encriptados. Element usa los servicios de Google Play para entregar mensajes Push pero no parece estar configurado correctamente: \n%1$s solucionar error con los Servicios de Google Play - Token Base Token FCM recuperada correctamente:\n%1$s Error al recuperar token FCM:\n%1$s @@ -1135,45 +952,36 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu [%1$s]\nEste error esta fuera del control de Element. Puede ocurrir por numerosas razones. Probablemente funcione si vuelve a intentarlo mas tarde. También puede comprobar si los Servicios de Google Play están limitados por los ajustes del sistema o si la hora del dispositivo es correcta o si puede pasar en ROM personalizada. [%1$s] \nEste error esta fuera del control de Element. No hay cuenta de googled registrada en este dispositivo. Por favor abre el gestor dde cuentas y añade una cuenta de Google. - añadir cuenta - + Añadir cuenta Token de registro Token FCM registrado correctamente en el Servidor. Error al registrar el token FCM en el Servidor \n%1$s - Servicio de notificaciones El servicio de notificaciones esta funcionando. El servicio de notificaciones no esta funcionando. \nIntente reiniciar la aplicación. Borrando copia de seguridad… Error al borrar la copia de seguridad (%s) - Borrar copia de seguridad nueva copia de seguridad Ese era yo - nunca se pierden mensajes cifrados - Configurar copia de seguridad de las claves de cifrado - - Nunca pierdas mensajes cifrados - Nuevos mensajes clave cifrados + Nunca pierda mensajes encriptados + Configurar copia de seguridad de las claves de encriptado + Nunca pierdas mensajes encriptados + Nuevas claves de encriptación de mensajes Gestionar Copia de Seguridad - Guardando copia de seguridad… - Versión Algoritmo Reinicio automático del servicio de notificaciones Empezar servicio - El servicio se ha apagado y reiniciado automáticamente. Error al reiniciar el servicio - Inicio automático El servicio funcionará cuando reinicie el dispositivo. El servicio no se iniciará al reiniciar el dispositivo, no recibirá notificaciones hasta que Element haya sido abierto al menos 1 vez. - activar Inicio automático - + Activar Inicio automático Comprobar restricciones en segundo plano Las restricciones de segundo plano están desactivadas para Element. Este debería funcionar con datos móviles (sin WIFI). \n%1$s @@ -1181,19 +989,15 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu \nLa app estará completamente restringida mientras esté en segundo plano y esto podría afectar a las notificaciones. \n%1$s Desactivar restricciones - Optimización de la bateria A Element no le afecta la Optimización de la bateria. Si un usuario deja el dispositivo desenchufado e inmóvil durante cierto periodo de tiempo con la pantalla apagada, el dispositivo entrará en modo hibernación. Esto evita que las apps accedan a la red y postpone sus tareas, sincronizaciones y alarmas. ignorar optimización - Las apps no necesita conectarse al servidor doméstico en segundo plano, esto debería reducir el uso de la batería Configurar notificaciones de sonido Configurar notificaciones de llamada Configurar notificaciones silenciadas elegir Color de las luces LED, vibración. sonido… - - Administrar Claves de la criptografía Mostrar vistas previas de enlaces en el chat cuando el servidor doméstico soporte esta característica. Enviar notificaciones de escritura @@ -1208,82 +1012,65 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Incluye cambios en el avatar y en el nombre. Enviar mensaje con intro La tecla Intro enviará el mensaje en vez de añadir un salto de línea - Conexión en segundo plano Element necesita mantener una leve conexión en segundo plano para poder ofrecer notificaciones de confianza. \nEn la siguiente pantalla se le pedirá permisos para que Element siempre funcione en segundo plano, por favor acepte. Conceder permiso - El modo de guardado de datos aplica un filtro específico para que las actualizaciones de presencia y las notificaciones de escritura sean eliminadas. - Ha ocurrido un error mientras se verificaba tu dirección de correo electrónico. - Contraseña Actualizar contraseña La contraseña no es válida La contraseña no es correcta - Ha ocurrido un error tratando de verificar su número de teléfono. Información adicional: %s - Media Compresión predeterminada Seleccionar Seleccionar - Recuperación de mensajes cifrados + Recuperación de mensajes encriptados Gestionar copia de seguridad clave - - %1$s: 1 mensaje + %1$s: %2$d mensaje %1$s: %2$d mensajes %d notificación %d notificaciones - Nuevo evento Sala Nuevos mensajes Nueva invitación Yo ** Error al enviar - por favor abra la sala - - "Lo sentimos, las llamadas grupales con Jitsi no se pueden mantener en dispositivos antiguos (dispositivos con Android inferior a 5.0)" - + Lo sentimos, las llamadas de grupo con Jitsi no están soportadas en dispositivos antiguos (dispositivos con Android inferior a 5.0) Iniciar la cámara del sistema en lugar de la pantalla de cámara personalizada. Esta opción requiere una aplicación de terceros para grabar los mensajes. - El comando \"%s\" necesita mas parámetros o algunos parámetros son incorrectos. Markdown activado. Markdown desactivado. - Silencioso Por favor introduzca un nombre de usuario. Mostrar el área de información Siempre Para mensajes y errores Solo para errores - %1$s: %1$s: %2$s +%d %d+ No se ha encontrado ningún APK válido de Servicios de Google Play. Las notificaciones podrían no funcionar correctamente. - Por favor introduzca una contraseña La contraseña que has introducido es muy débil - Por favor borra la contraseña si quieres que Element genere una clave de recuperación. No hay ninguna sesión de Matrix disponible - - Nunca se pierden los mensajes cifrados - Los mensajes en salas cifradas están asegurados con cifrado de extremo a extremo. Solo los integrantes de la sala y tu podéis leer estos mensajes. + Nunca perder los mensajes encriptados + Los mensajes en salas encriptadas están asegurados con encriptación Extremo-a-Extremo. Solo los integrantes de la sala y tu podéis leer estos mensajes. \n \nAsegúrate de guardar bien tus claves para evitar perderlas. (Avanzado) Exportar claves manualmente - Asegura tu copia de seguridad con una contraseña. Almacenaremos una copia cifrada de tus claves en tu servidor. Protege tu copia de seguridad con una contraseña para mantenerla segura. \n @@ -1291,10 +1078,10 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Introduce una contraseña Creando copia de seguridad O, asegura tu copia de seguridad con una clave de recuperación, guardándola en algún lugar seguro. - "(Avanzado) preparar clave de recuperación" + (Avanzado) Establecer clave de recuperación Completado! Tus claves se están guardando. - Tu clave de recuperación es una red de seguridad - Puedes usarla para recuperar el acceso a tus mensajes cifrados si olvidas tu contraseña. + Tu clave de recuperación es una red de seguridad - puedes usarla para recuperar el acceso a tus mensajes encriptados si olvidas tu contraseña. \nMantén tu clave de recuperación en algún lugar muy seguro como un administrador de contraseñas (o en una caja fuerte) Mantén tu clave de recuperación en algún lugar muy seguro como un administrador de contraseñas (o en una caja fuerte) Hecho @@ -1305,7 +1092,6 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu La clave de recuperación ha sido guardada en \'%s\'. \n \nAtención: Este archivo podría borrarse si la aplicación es desinstalada. - Por favor, haga una copia Compartir clave de recuperación con… Generando clave de recuperación usando una contraseña, este proceso puede tardar varios segundos. @@ -1313,24 +1099,17 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Error inesperado Copia de seguridad iniciada Tus claves cifradas están siendo guardadas en segundo plano en tu servidor. La copia de seguridad inicial podría tardar varios minutos. - - Estás seguro\? Podrías perder el acceso a tus mensajes si te desconectas o pierdes este dispositivo. - - Utiliza tu clave de recuperación para desbloquear tu historial de mensajes cifrados + Utiliza tu clave de recuperación para desbloquear tu historial de mensajes encriptados Utiliza tu clave de recuperación No sabes tu clave de recuperación\? puedes %s. - - Utiliza tu clave de recuperación para desbloquear tu historial de mensajes cifrados + Utiliza tu clave de recuperación para desbloquear tu historial de mensajes encriptados Introduzca la clave de recuperación - Mensaje de recuperación - Has perdido tu clave de recuperación\? Puedes crear una nueva en ajustes. La copia de seguridad no se ha podido descifrar con esta contraseña: por favor verificar que has introducido la contraseña de recuperación correcta. Error de red: por favor comprueba tu conexión y vuelve a intentarlo. - Restaurando copia de seguridad: Creando clave de recuperación… Descargando claves… @@ -1338,7 +1117,6 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Desbloquear historial Por favor introduce una clave de recuperación La copia de seguridad no se ha podido descifrar con esta contraseña: por favor verificar que has introducido la contraseña de recuperación correcta. - Copia de seguridad restaurada %s ! Copia de seguridad restaurada con la clave %d. @@ -1348,111 +1126,87 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Se ha añadido %d como clave a esta sesión. Se han añadido %d como claves a esta sesión. - Error al recuperar la ultima versión de las claves (%s). La sesión crypto no esta activada - - Restaurada desde copia de seguridad Borrar copia de seguridad - La copia de seguridad ha sido correctamente activada para esta sesión. La copia de seguridad ha sido correctamente desactivada para esta sesión. Tus claves no están siendo guardadas en esta sesión. - La copia de seguridad tiene una firma de una sesión desconocida con el ID %s. La copia de seguridad tiene una firma valida de esta sesión. La copia de seguridad tiene una firma válida para la sesión verificada %s. Usar copia de seguridad de la clave - Todas las claves guardadas Cargando %d de la clave… Cargando %d de las claves… - Firma - autocompletar opciones del servidor Element ha detectado una configuración personalizada del servidor para el dominio de su ID de usuario \"%1$s\": \n%2$s Configuración de uso - Origen predeterminado de medios - Configurar copia de seguridad de las claves de cifrado + Configurar copia de seguridad de las claves de encriptación Obteniendo una versión de copia de seguridad… La copia de seguridad tiene una firma valida de la sesión no verificada %s La copia de seguridad tiene una firma inválida de la sesión verificada %s La copia de seguridad tiene una firma inválida de la sesión no verificada %s Error al conseguir información de confianza para la copia de seguridad (%s). - Para usar la copia de seguridad de la clave en esta sesión introduzca su contraseña o su clave de recuperación ahora. - Desea borrar sus claves cifradas guardadas del servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes cifrados. - + Deseas borrar tus claves de encriptación guardadas en el servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes encriptados. Una nueva copia de seguridad de mensajes ha sido detectada. \n \nSi no ha establecido un nuevo método de recuperación, alguien podría estar intentando acceder a su cuenta. Cambie su contraseña y establezca un nuevo método de recuperación inmediatamente en ajustes. Respuesta inválida del descubrimiento del servidor doméstico Reproducir sonido de cámara - Verificar sesión - ip desconocida - Una nueva sesión solicita claves de cifrado. -\nSesión: %1$s -\nVisto por última vez: %2$s + Una nueva sesión solicita claves de encriptación. +\nSesión: %1$s +\nVisto por última vez: %2$s \nSi no has iniciado sesión en otro dispositivo ignora esta solicitud. - Una sesión no verificada solicita claves de cifrado. -\nSesión: %1$s -\nVisto por última vez: %2$s + Una sesión no verificada solicita claves de encriptación. +\nSesión: %1$s +\nVisto por última vez: %2$s \nSi no has iniciado sesión en otro dispositivo ignora esta solicitud. - Verificar Compartir Petición de compartición de clave Ignorar - Ya existe una copia de respaldo en tu servidor Parece que ya habías configurado una copia de seguridad para las claves en otra sesión. ¿Quieres reemplazarla por la nueva que has creado\? Reemplazar Parar - Comprobando copias de respaldo Tu sesión ha terminado por credenciales caducadas o inválidas. - Verificar comparando un texto corto. Para más seguridad, te recomendamos que hagas esto en persona o por otros medios confiables. Empezar verificación Solicitud de verificación - Verifica esta sesión para marcarla como confiable. Confiar en sesiones de otros te da aún más tranquilidad cuando usas cifrado de mensajes de punto a punto. + Verifica esta sesión para marcarla de confianza. Marcar sesiones de otros como de confianza te da aún más tranquilidad cuando usas encriptacion de Extremo-a-Extremo. Verificar esta sesión la marcará como confiable, y también marcará como confiable tu sesión para la contraparte. - Verifica esta sesión confirmando los emojis que aparecen en la pantalla de la contraparte Verifica esta sesión confirmando que los siguietes números aparecen en la pantalla de la contraparte - Se ha recibido una solicitud de verificación. Ver solicitud Esperando confirmación de la contraparte… - ¡Verificado! Has verificado correctamente esta sesión. - Los mensajes con este usuario están cifrados punto a punto y no son legible por terceros. + Los mensajes con este usuario están encriptados de Extremo-a-Extremo y no son legibles por terceros. Ok - ¿No aparece nada\? No todas las aplicaciones cliente soportan verificación interactiva. Usa la verificación clásica. Usar verificación clásica. - Verificación de clave Solicitud cancelada La contraparte canceló la verificación. \n%s La verificación ha sido cancelada. \nRazón: %s - Verificación de sesión interactiva Solicitud de verificación %s quiere verificar tu sesión - El usuario canceló la verificación La verificación ha superado el límite de espera La sesión recibió un mensaje inesperado @@ -1460,35 +1214,27 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Error en clave Error en usuario Error desconocido - - Editar Responder - Reintentar Unirse a una sala y empezar a usar la app. Alguien te envió una invitación Invitado por %s - No tienes más mensajes sin leer ¡Bienvenido! Conversaciones Tus conversaciones directas (1 a 1) se mostrarán aquí Salas Tus salas se mostrarán aquí - Reacciones De acuerdo Me gusta Añadir reacción Ver reacciones Reacciones - Evento borrado por el usuario Evento moderado por el administrador de la sala Última edición por %1$s on %2$s - - Evento con error, no se puede mostrar Crear sala No hay red, por favor comprueba tu conexión a internet. @@ -1496,24 +1242,19 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Cambiar red Espere por favor… Todas la comunidades - Esta sala no se puede previsualizar La previsualización de salas públicas no es posible todavía con Element - Salas Mensajes directos - Nueva sala CREAR - Nombre de la sala + Nombre Público Cualquiera puede unirse a esta sala Directorio de salas Publicar esta sala en el directorio de salas - Error obteniendo información de confiabilidad Error obteniendo claves para copias de respaldo - !Estás al día! Preferencias Seguridad & Privacidad @@ -1522,31 +1263,23 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Por favor escriba su sugerencia a continuación. Describa su sugerencia aquí Latn - Ninguno Revocar Desconectar Revisar Declinar - No se ha configurado un servidor de identidad. - La llamada ha fallado por un servidor mal configurado Intente usar %s No volver a preguntar - Para hacer esto, vaya a las opciones y añada un servidor de identidad. Confirme su contraseña Eso no se puede hacer en Element para móvil Se necesita autenticación - - Optimizado para batería Optimizado para operar en tiempo real Sin sincronización en segundo plano No se han podido actualizar las opciones. - - Integraciones Descubrimiento Gestione sus preferencias de descubrimiento. @@ -1562,7 +1295,7 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu El SAS no coincidió Pon un número de teléfono para que las personas que conoces te puedan encontrar. Se usará %s como asistencia cuando el servidor doméstico no la ofrezca (su dirección IP se compartirá durante una llamada) - Modo sincronización en segundo plano (Experimental) + Modo Sincronización en segundo plano Element se sincronizará en segundo plano de manera que se preserven los recursos del dispositivo (batería). \nDependiendo del estado de los recursos del dispositivo, la sincronización puede ser aplazada por el sistema operativo. Element se sincronizará en segundo plano periódicamente en un momento preciso (configurable). @@ -1575,7 +1308,6 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu \nLos Gestores de Integración reciben los datos de configuración y pueden modificar los widgets, enviar invitaciones a salas y establecer niveles de poder en su nombre. Permitir integraciones Adiministrador de integraciones - Nombre público (visible por las personas con quien te comuniques) Un nombre de sesión público es visible por las personas con quién te comunicas Widget @@ -1588,110 +1320,77 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Recargar widget Abrir en el navegador Revocar acceso para mi - Tu nombre visible La URL de tu avatar Tu ID de usuaria Tu tema ID de Widget ID de Sala - - Este widget quiere usar los siguientes recursos: Permitir Bloquear todos Usar la cámara Usar el micrófono Leer medios protegidos por DRM - No se ha configurado ningún administrador de integraciones. Para continuar es necesario que aceptes los Términos de este servicio. - La sesión no sabe nada de esa transacción La sesión no puede acordar el acuerdo de llaves, hash, MAC o método SAS El compromiso hash no ha coincidido No estás usando ningún Servidor de Identidad No hay ningún Servidor de Identidad configurado, esto es requerido para restablecer tu contraseña. - Parece que estás intentando conectarte a otro servidor doméstico. ¿Quieres cerrar sesión\? - Importar llaves E2E des del fichero \"%1$s\". - Versión del SDK de Matrix Otros avisos de terceros ¡Ya estas viendo esta sala! - Reacciones rápidas - Cuenta Experto Reglas Push No hay reglas push definidas No hay salidas push registradas - app_id: push_key: app_display_name: Url: Formato: - Ayuda y Acerca de - - Registrar token - Gracias, la sugerencia ha sido enviada correctamente El envio de la sugerencia ha fallado (%s) - Mostrar eventos ocultos en la línea de tiempo - Mensajes Directos - Esperando… Cifrando la miniatura… Enviando miniatura (%1$s / %2$s) Cifrando el archivo… Enviando el archivo (%1$s / %2$s) - Descargando archivo %1$s… ¡El archivo %1$s ha sido descargado! - (editado) - - Modificación de mensajes No se han encontrado modificaciones - Filtrar conversaciones… ¿No encuentras lo que buscas\? Crear una nueva sala Enviar un nuevo mensaje directo Ver el directorio de la sala - Nombre o ID (#ejemplo:servidor.org) - Habilitar \"desplazar para contestar\" en la línea de tiempo - Enlace copiado al portapapeles - Agregar por ID de matrix Creando sala… No se ha encontrado ningún resultado, utiliza \"Agregar usando ID de matrix\" para buscar en el servidor. Empieza a escribir para ver resultados Filtrar por usuario o ID… - Entrando en la sala… - Ver historial de modificaciones - Términos de Servicio Revisar Términos Ser descubierta por otros Utiliza Bots, puentes, widgets y packs de stickers - Leer en - - Servidor de identidad Desconectar servidor de identidad Configurar servidor de identidad @@ -1705,27 +1404,19 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Teléfonos para ser descubierto Te hemos enviado un correo de confirmación a %s, comprueba tu correo y haz click en el enlace de confirmación Pendiente - Entra en un nuevo servidor de identidad No se ha podido conectar al servidor de identidad Porfavor entra la url del servidor de identidad El servidor de identidad no tiene términos de servicio El servidor de identidad que has escojido no tiene términos de servicio. Solo continúa si confias en el propietario del servicio Un mensaje de texto ha sido enviado a %s. Porfavor escribe el código de verificación que contiene. - Actualmente estás compartiendo direcciones de correo electrónico o números de teléfono en el servidor de identidad %1$s. Necesitarás reconectarte a %2$s para dejar de compartirlos. Acepte los términos de servicio del servidor de identidad (%s) para permitir que sea descubierto por correo electrónico o número de teléfono. - Habilitar registros extensos. Los logs extensivos ayudarán a los desarrolladores proporcionando más información cuando envíes un \"RageShake\" (Sacudir el dispositivo). Incluso cuando esto está habilitado, la aplicación no registra el contenido de los mensajes ni ningún otro dato privado. - - Por favor, vuelva a intentarlo una vez que haya aceptado los términos y condiciones de su servidor. - Parece que el servidor está tardando demasiado en responder, esto puede ser causado por una mala conectividad o un error con el servidor. Por favor, inténtelo de nuevo en un rato. - Enviar el archivo adjunto - Abrir el cajón de navegación Abrir el menú de creación de sala Cerrar el menú de creación de sala… @@ -1735,17 +1426,14 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Mostrar contraseña Esconder contraseña Saltar al final - Leído por %1$s, %2$s y %3$s Leído por %1$s y %2$s Leído por %s - Leído por 1 usuario - Leído por %d usuarias + Leído por %d usuario + Leído por %d usuarios - El archivo \'%1$s\' (%2$s) es demasiado grande para ser subido. El límite es %3$s. - Ha ocurrido un error recuperando el archivo adjunto. Archivo Contacto @@ -1754,7 +1442,6 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Galería Pegatina No se han podido compartir los datos - Es spam Es inapropiado Reporte personalizado… @@ -1762,28 +1449,23 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Razón por la que se ha reportado el contenido REPORTAR BLOQUEAR USUARIO - Contenido reportado - El contenido ha sido reportado + El contenido ha sido reportado. \n -\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes +\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes. Reportar como spam Este contenido ha sido reportado como spam. \n -\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes +\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes. Reportado como inapropiado Este contenido fue reportado como inapropiado. \n -\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes - +\nSi no quieres ver más contenido de este usuario, puedes bloquearlo para ocultar sus mensajes. Element necesita permiso para guardar tus claves E2E en la memória del dispositivo. \n \nPorfavor permite el acceso en el siguiente pop-up para poder exportar tus claves manualmente. - No hay conexión de red - Ignorar usuario - Todos los mensajes (sonido) Todos los mensajes Solo menciones @@ -1794,29 +1476,22 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Envía el mensaje como spoiler Revelación Escribe las palabras clave para encontrar una reacción. - No hay usuarios ignorados - Mantén pulsada una sala para ver más opciones - - %1$s ha hecho la sala pública para cualquier persona con el link. %1$s: Ahora la sala solo es accesible por invitación. Mensajes no leídos - - Es tu conversación. Me pertenece. + Es tu conversación. Sé su dueño. Envía mensajes a personas o grupos Mantén las conversaciones privadas con encriptación Extiende y personaliza tu experiencia Empieza - Selecciona un servidor Como el correo electrónico, las cuentas tienen un hogar, aunque se puede hablar con cualquiera Alojamiento de pago para organizaciones Saber más Otro Ajustes avanzados y de personalización - Continuar Conectarse a %1$s Conectarse a Element Matrix Services @@ -1828,34 +1503,26 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Alojamiento de pago para organizaciones Introduzca la dirección de Modular Element o servidor que quieres usar Introduzca la dirección del servidor Element al que quieres conectarte - Se produjo un error al cargar la página: %1$s (%2$d) - "La aplicación no es capaz de iniciar sesión en este servidor. Este solo soporta el acceso mediante: %1$s. + La aplicación no es capaz de iniciar sesión en este servidor. Éste solo soporta el acceso mediante: %1$s. \n -\n¿Quieres acceder usando un cliente web\?" +\n¿Quieres acceder usando un cliente web\? Lo sentimos, este servidor no acepta nuevas cuentas. La aplicación no fue capaz de crear una cuenta en este servidor. \n \n¿Quieres registrarte usando un cliente web\? - La dirección de coreo electrónico no está asociada a ninguna cuenta. - Reiniciar contraseña en %1$s ¡Las claves ya están al día! - Reproducir Pausar Descartar - - Copiar Correcto - Notificaciones Element Fallo la Llamada Fallo al intentar establecer conexion. \nTURN Server fallo. Por favor, contacte con el administrador de su Servidor y notifique el fallo. - Seleccionar Dispositivo de Sonido Telefono Altavoz @@ -1866,22 +1533,20 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Tracera Apagar HD Activar HD - Error SSl: la identidad del par no a sido verificada. Error SSL. Permitir servidor de asistencia de llamadas Llamada activa (%s) Regresar a la llamada - Cancelar invitación Ignorar Usuario Cancelar Invitacion Por favor, elija un nombre de usuario. Por favor, elija una contraseña. Verifica este enlace - Este link %1$s loredirecciona a otro sitio %2$s. . -\nEsta seguro de continuar\? - + Este link %1$s lo redirecciona a otro sitio %2$s. +\n +\n¿Está seguro de continuar\? Adicionar miembros INVITAR Invitando usuarios… @@ -1893,11 +1558,9 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Invitaciones enviadas a %$s y a %2$d más No se pudo invitar el usuario. Por favor, intente nuevamente. - Idioma actual Otros idiomas disponibles Cargando lenguajes disponibles… - Leer los terminos de %s Desconectarse del servidor de Identidad %s\? Servidor de identidad desactualizado. Element solo soporta API V2. @@ -1906,20 +1569,17 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Riot ahora es Element! Entendido Aprender Mas - Element - - Buscar en mis contactos Rechazar invitación Confirma PIN para desabilitarlo No posee permisos para iniciar una conferencia en esta sala Conferencia en progreso! - Iniciar Video Conferencia - Iniciar Audio Conferencia - No puedes hacer una llamada contigo mismo - No puedes hacer una llamada contigo mismo, espera a que los participantes acepten la invitación - Fallo al adicionar Widget + Iniciar Videoconferencia + Iniciar Audioconferencia + No puedes hacer llamarte a tí mismo + No puedes hacer llamarte a tí mismo, espera a que los participantes acepten la invitación + Fallo al añadir Widget Fallo al eliminar Widget Confirmar llamada Pedir confirmacion antes de iniciar una llamda @@ -1928,14 +1588,10 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Rason de baneo Desbanear usuario Desbanear usuario y permitir entrar a la sala nuevamente. - nombre_session: Adicionar pestaña dedicada para notificaciones no leidas en la pantalla principal. - Descripcion muy corta - Sincronización inicial… - Mostrar todas mis sessiones Opciones Avanzadas Modo Desarrollador @@ -1946,33 +1602,25 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Ajustes Sesión Actual Otras Sesiones - Mostrando solo el primer resultado, agregue mas letras… - Fallar rápido (Test) Element puede fallar con más frecuencia cuando ocurre un error inesperado - Antepone ¯\\_(ツ)_/¯ a un mensaje de texto sin formato - Habilitar encriptacion - Una vez habilitada, el cifrado no se puede deshabilitar. - + Una vez habilitada, la encriptación no se puede deshabilitar. Su dominio de correo electrónico no está autorizado para registrarse en este servidor - Inicio de sesión no confiable Coinciden No coinciden Verifique a este usuario confirmando que el siguiente emoji único aparece en su pantalla, en el mismo orden. Para mayor seguridad, use otro medio de comunicación confiable o hágalo en persona. Busque el escudo verde para asegurarse de que se confía en un usuario. Confíe en todos los usuarios de una sala para asegurarse de que la sala sea segura. - No seguro Video. Imagen. Audio Archivo - Sticker - + Pegatina Esperando… %s cancelada Cancelado por usted @@ -1982,25 +1630,19 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Solicitud de verificación Verifica esta Sesion Verificar manualmente - Usted - Escanee el código con el dispositivo del otro usuario para verificarse mutuamente de forma segura Escanear código Error al escanear Si no estás en persona, compara los emojis - Verificar comparando emojis - Verificar por emojis Si no puede escanear el código anterior, verifique comparando una selección breve y única de emoji. - Imagen de código QR - Verificar %s Verificado %s Esperando por %s… - Los mensages en esta sala no están encriptados punto a punto. + Los mensajes en esta sala no están encriptados de Extremo-a-Extremo. Seguridad Saber mas Mas @@ -2014,141 +1656,103 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Archivos, Medias y Documentos Abandonar Sala Saliendo de la sala… - Administradores Moderadores Nivel Personalizado Invitados Usuarios - Administrador en %1$s Moderador en %1$s Nivel Personalizado en %1$s Nivel Personalizado (%1$d) en %2$s - Saltar para leer el recibo - Element no maneja eventos de tipo \'%1$s\' Element no maneja el mensaje de tipo \'%1$s\' Element encontró un problema al representar el contenido del evento con el ID \'%1$s\' - Dejar de ignorar - Salas recientes Otras salas - Envía el mensaje dado en colores Línea de tiempo - Editor de mensage - Encriptar (end-to-end) - Una vez habilitado, el cifrado no se puede deshabilitar. - + Una vez habilitada, la encriptación no se puede deshabilitar. Encriptar \? - Habilitar el cifrado - + Habilitar la encriptación Firma cruzada Firma cruzada no habilitada - Sesiones Activas Mostrar todas las Sesiones Administrar Sesiones Cerrar Sesión - Verificar este inicio de sesión Otros usuarios pueden no confiar en la sesion Completar Seguridad - Verificar Verificada Precaucion - Error al obtener sesiones Sesiones Confirmado No es confiable - Inicializar Firmas Cruzadas Restablecer claves - Codigo QR - Correcto No - Sin conexión Modo Avión Activado - Herramientas de desarrollo Datos de cuenta Seleccionar Opcion Nuevo inicio de sesión - Advertencia: Remover… Razón Razón para redactar - Element Android - Refrescar - Nuevo inicio de sesión detectado . ¿Fue usted\? Toca para revisar y verificar Este no era yo Su cuenta puede estar comprometida - Verificación cancelada - Frase de contraseña de recuperación Clave de mensaje Contraseña de la cuenta - ¡Listo! - Cifrado habilitado + Encriptación habilitada Sala creada y configurada por usted. - Esperando por %s… - Ajuste de Notificaciones Mensaje… - Introduza su %s para continuar Usar archivo - No se pudo guardar el archivo multimedia Verificar Sesión Confirmar PIN Resetear PIN Nuevo PIN - Para resetear su PIN, debe iniciar sección y crear uno nuevo. + Para resetear su PIN, debe iniciar sesión y crear uno nuevo. Establecer PIN Si decea resetear su PIN, toque Olvidé PIN para cerrar sesión y restablecer. Numeros telefonicos Correos y numeros telefonicos Administre el correo y numero telefonico de su cuenta - Mostrar mensages eliminados Indicar marca de mensaje eliminado ARCHIVOS No se han subido archivos a la sala - Establecer notificaciones por eventos - Establecer una nueva contraseña… - Las reuniones utilizan políticas de seguridad y permisos de Jitsi. Todas las personas que se encuentren actualmente en la sala verán una invitación para unirse mientras se lleva a cabo la reunión. Aceptar Declinar Colgar - Este número de teléfono ya está definido. ¿Degradarte\? No podrá deshacer este cambio ya que se está degradando, si es el último usuario privilegiado en la sala, será imposible recuperar los privilegios. Degradar - - Si ignora a este usuario, se eliminarán sus mensajes de las salas que comparte. \n \n Puede revertir esta acción en cualquier momento en la configuración general. @@ -2164,56 +1768,42 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu No se ha agregado ningún correo electrónico a su cuenta ¿Elimina %s\? Asegúrese de haber hecho clic en el enlace del correo electrónico que le enviamos. - Copia de seguridad segura Gestionar Configurar copia de seguridad segura Restablecer copia de seguridad segura Configurar en este dispositivo - Protéjase contra la pérdida de acceso a los mensajes y datos cifrados haciendo una copia de seguridad de las claves de cifrado en su servidor. + Protéjase contra la pérdida de acceso a los mensajes y datos encriptados haciendo una copia de seguridad de las claves de encriptado en su servidor. Genere una nueva llave de seguridad o establezca una nueva frase de seguridad para su copia de seguridad existente. Esto reemplazará su clave o frase actual. - Las integraciones están deshabilitadas Habilite \'Permitir integraciones\' en Configuración para hacer esto. - %d usuario prohibido %d usuarios prohibidos - Claves exportadas correctamente - %1$d/%2$d clave importada con éxito. %1$d/%2$d claves importadas con éxito. - %1$s: %2$s %1$s: %2$s %3$s - VER Widgets activos - - Gestionar integraciones Sin widgets activos La clave de recuperación se ha guardada. - Copia de seguridad segura - Protéjase contra la pérdida de acceso a mensajes y datos cifrados - + Protéjase contra la pérdida de acceso a mensajes y datos encriptados Configurar copia de seguridad segura - Mensaje borrado Se ha creado la sala, pero algunas invitaciones no se han enviado por el siguiente motivo: \n \n%s - Le enviamos un correo electrónico de confirmación %s, primero revise su correo electrónico y haga clic en el enlace de confirmación Código El código de verificación no es correcto. - %1$s, %2$s y %3$d otra lectura %1$s, %2$s y %3$d otras lecturas @@ -2226,101 +1816,83 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu No hiciste cambios Hiciste que la sala fuera pública para quien conozca el enlace. Hiciste la sala solo por invitación. - Únase a millones gratis en el servidor público más grande + Únase gratis a millones en el servidor público más grande Continuar con SSO - Dirección de servicios de Element Matrix Ingrese la dirección del servidor que desea utilizar - Se enviará un correo electrónico de verificación a su bandeja de entrada para confirmar la configuración de su nueva contraseña. - Próximo + Siguiente Email Nueva contraseña - ¡Advertencia! - Cambiar su contraseña restablecerá cualquier clave de cifrado de extremo a extremo en todas sus sesiones, haciendo ilegible el historial de chat cifrado. Configure Key Backup o exporte las llaves de su sala desde otra sesión antes de restablecer su contraseña. + Cambiar su contraseña restablecerá cualquier clave de encriptado de Extremo-a-Extremo en todas sus sesiones, haciendo ilegible el historial de chat encriptado. Configure la Copia de seguridad de claves o exporte las claves de su sala desde otra sesión antes de restablecer su contraseña. Seguir - Este correo electrónico no está vinculado a ninguna cuenta - Revisa tu correo Se envió un correo electrónico de verificación a %1$s. Toque el enlace para confirmar su nueva contraseña. Una vez que haya seguido el enlace que contiene, haga clic a continuación. He verificado mi dirección de correo electrónico - ¡Éxito! Tu contraseña ha sido restablecida. Ha cerrado sesión en todas las sesiones y ya no recibirá notificaciones automáticas. Para volver a habilitar las notificaciones, inicie sesión nuevamente en cada dispositivo. Volver a Iniciar sesión - Advertencia u contraseña aún no ha cambiado. \n \n¿Detener el proceso de cambio de contraseña\? - Establecer dirección de correo electrónico Configure un correo electrónico para recuperar su cuenta. Más tarde, opcionalmente, puede permitir que las personas que conoce lo descubran mediante su correo electrónico. Correo electrónico Email (opcional) - Próximo - + Siguiente Establecer número de teléfono Configure un número de teléfono para permitir que las personas que conoce lo descubran opcionalmente. Utilice el formato internacional. Número de teléfono Numero de teléfono (opcional) - Próximo - + Siguiente Confirmar número de teléfono Acabamos de mandar un codigo a %1$s. Ingréselo a continuación para verificar que es usted. Introduzca el código Enviar de nuevo - Próximo - + Siguiente Utilice el formato internacional (el número de teléfono debe comenzar con \'+\') Los números de teléfono internacionales deben comenzar con \'+\' El número de teléfono parece no válido. Compruébelo por favor - Inscribirse a %1$s Nombre de usuario o correo electrónico Nombre de usuario Contraseña - Próximo + Siguiente Ese nombre de usuario está siendo usado Advertencia Tu cuenta aún no está creada. \n \n ¿Detener el proceso de registro\? - Seleccione matrix.org Seleccionar servicios de matriz de elementos Seleccione un servidor doméstico personalizado Realiza el desafío de captcha Acepta los términos para continuar - Por favor revise su correo electrónico Acabamos de enviar un correo electrónico a %1$s. \nHaga clic en el enlace que contiene para continuar con la creación de la cuenta. El código introducido no es correcto. Por favor, compruebe. Servidor doméstico obsoleto Este servidor doméstico está ejecutando una versión demasiado antigua para conectarse. Pídale al administrador de su servidor doméstico que actualice. - Se han enviado demasiadas solicitudes. Puedes volver a intentarlo en %1$d segundo… Se han enviado demasiadas solicitudes. Puedes volver a intentarlo en %1$d segundos… - Alternativamente, si ya tiene una cuenta y conoce su identificador Matrix y su contraseña, puede usar este método: Iniciar sesión con Matrix ID Iniciar sesión con Matrix ID Si configura una cuenta en un servidor doméstico, use su ID de Matrix (por ejemplo, @user: dominio.com) y contraseña a continuación. ID de Matrix Si no conoce su contraseña, vuelva a restablecerla. - "Este no es un identificador de usuario válido. Formato esperado: \'@user:homeserver.org\'" + Éste no es un identificador de usuario válido. Formato esperado: \'@user:homeserver.org\' No se pudo encontrar un servidor de inicio válido. Por favor verifique su identificador - Visto por - Estás desconectado Puede deberse a varias razones: \n @@ -2330,54 +1902,48 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu \n \n• El administrador de su servidor ha invalidado su acceso por motivos de seguridad. Iniciar sesión de nuevo - Estás desconectado Registrarse - "El administrador de su servidor doméstico (%1$s) ha cerrado la sesión de su cuenta %2$s (%3$s)." - Inicie sesión para recuperar las claves de cifrado almacenadas exclusivamente en este dispositivo. Los necesita para leer todos sus mensajes seguros en cualquier dispositivo. + El administrador de su servidor privado (%1$s) ha cerrado la sesión de su cuenta %2$s (%3$s). + Inicie sesión para recuperar las claves de encriptación almacenadas exclusivamente en este dispositivo. Los necesita para leer todos sus mensajes seguros en cualquier dispositivo. Registrarse Contraseña Borrar datos personales - Advertencia: sus datos personales (incluidas las claves de cifrado) todavía se almacenan en este dispositivo. + Advertencia: sus datos personales (incluidas las claves de encriptación) todavía están almacenadas en este dispositivo. \n \nBórrelo si terminó de usar este dispositivo o si desea iniciar sesión en otra cuenta. Borrar todos los datos - Borrar datos ¿Borrar todos los datos almacenados actualmente en este dispositivo\? \nVuelva a iniciar sesión para acceder a los datos y mensajes de su cuenta. - Perderás el acceso a los mensajes seguros a menos que inicies sesión para recuperar tus claves de cifrado. + Perderás el acceso a los mensajes seguros a menos que inicies sesión para recuperar tus claves de encriptación. Borrar datos - La sesión actual es para el usuario %1$s y usted proporciona las credenciales para el usuario %2$s. Esto no es compatible con Element. Primero borre los datos, luego inicie sesión nuevamente con otra cuenta. - + La sesión actual es para el usuario %1$s y usted proporciona las credenciales para el usuario %2$s. Esto no está suportado por Element. +\nPrimero borre los datos, luego inicie sesión nuevamente con otra cuenta. Su enlace matrix.to estaba mal formado El modo desarrollador activa funciones ocultas y también puede hacer que la aplicación sea menos estable. ¡Solo para desarrolladores! - Uno de los siguientes puede verse comprometido: -\n- Tu servidor doméstico -\n- El servidor doméstico al que está conectado el usuario que estás verificando -\n- La suya o la conexión a Internet de otros usuarios -\n- El suyo o el dispositivo de otros usuarios - + Uno de los siguientes puede verse comprometido: +\n +\n- Tu servidor privado +\n- El servidor privado al que está conectado el usuario que estás verificando +\n- Su conexión a internet o la de otros usuarios +\n- Su dispositivo o el de otros usuarios Para mayor seguridad, verifique %s verificando un código único en ambos dispositivos. \n \nPara máxima seguridad, hágalo en persona. - Los mensajes de esta sala están cifrados de extremo a extremo. + Los mensajes de esta sala están encriptados de Extremo-a-Extremo. \n \nSus mensajes están protegidos con candados y solo usted y el destinatario tienen las claves únicas para desbloquearlos. Esta sesión no puede compartir esta verificación con sus otras sesiones. \nLa verificación se guardará localmente y se compartirá en una versión futura de la aplicación. - Envía el emote dado coloreado como un arcoíris - - Una vez habilitado, el cifrado de una sala no se puede deshabilitar. Los mensajes enviados en una sala cifrada no pueden ser vistos por el servidor, solo por los participantes de la sala. Habilitar el cifrado puede evitar que muchos bots y puentes funcionen correctamente. + Una vez habilitado, la encriptación de una sala no se puede deshabilitar. Los mensajes enviados en una sala encriptada no pueden ser vistos por el servidor, solo por los participantes de la sala. Habilitar la encriptación puede impedir que muchos bots y puentes funcionen correctamente. Para estar seguro, verifique %s comprobando un código de un solo uso. Para estar seguro, hágalo en persona o use otra forma de comunicarse. - Compare los emoji únicos, asegurándose de que aparezcan en el mismo orden. Compare el código con el que se muestra en la pantalla del otro usuario. Los mensajes con este usuario están encriptados de extremo a extremo y no pueden ser leídos por terceros. - Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes cifrados y otros usuarios lo verán como de confianza. - + Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes encriptados y otros usuarios lo verán como de confianza. La firma cruzada está habilitada \n Claves privadas en el dispositivo. La firma cruzada está habilitada @@ -2385,25 +1951,18 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu \nNo se conocen las claves privadas La firma cruzada está habilitada. \nLas claves no son de confianza - El administrador de su servidor ha desactivado el cifrado de extremo a extremo de forma predeterminada en salas privadas y mensajes directos. + El administrador de su servidor ha desactivado la encriptación de Extremo-a-Extremo de forma predeterminada en salas privadas y mensajes directos. No hay información criptográfica disponible - Esta sesión es confiable para mensajería segura porque usted la verificó: Verifique esta sesión para marcarla como confiable y otorgarle acceso a mensajes encriptados. Si no inició sesión en esta sesión, su cuenta puede verse comprometida: - %d sesión activa %d sesiones activas - - Utilice una sesión existente para verificar esta, otorgándole acceso a los mensajes cifrados. - - - "Esta sesión es confiable para mensajería segura porque %1$s (%2$s) la verificó:" + Utilice una sesión existente para verificar ésta, otorgándole acceso a los mensajes encriptados. + Esta sesión es de confianza para mensajería segura porque %1$s (%2$s) la ha verificado: %1$s (%2$s) iniciado sesión con una nueva sesión: Hasta que este usuario confíe en esta sesión, los mensajes enviados hacia y desde ella se etiquetan con advertencias. Alternativamente, puede verificarlo manualmente. - - ¡Casi ahí! ¿Es %s muestra el mismo escudo\? %d voto @@ -2416,31 +1975,24 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Crea una encuesta simple Use una contraseña o clave de recuperación Si no puede acceder a una sesión existente - No puedo encontrar secretos almacenados Ingrese la contraseña de almacenamiento secreta Solo debe acceder al almacenamiento secreto desde un dispositivo confiable - ¿Quieres enviar este adjunto a %1$s\? Enviar imagen con el tamaño original Envía imágenes con el tamaño original - Confirmar eliminación ¿Está seguro de que desea eliminar (eliminar) este evento\? Tenga en cuenta que si elimina el nombre de una sala o el cambio de tema, podría deshacer el cambio. Evento eliminado por el usuario, motivo: %1$s Evento moderado por el administrador de la sala, motivo: %1$s - Solicitudes clave - - Desbloquear el historial de mensajes cifrados - + Desbloquear el historial de mensajes encriptados Utilice esta sesión para verificar su nuevo, otorgándole acceso a mensajes encriptados. Si cancela, no podrá leer mensajes encriptados en este dispositivo y otros usuarios no confiarán en él Si cancela, no podrá leer mensajes encriptados en su nuevo dispositivo y otros usuarios no confiarán en él No verificarás %1$s (%2$s) si cancelas ahora. Comience de nuevo en su perfil de usuario. - Uno de los siguientes puede verse comprometido: \n \n- Tu contraseña @@ -2449,29 +2001,21 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu \n- La conexión a Internet que está usando cualquiera de los dispositivos \n \nLe recomendamos que cambie su contraseña y clave de recuperación en Configuración de inmediato. - Verifique sus dispositivos desde Configuración. Establecer un %s Generar una clave de mensaje - Confirmar %s - - "Ingrese su %s para continuar." - - Proteja y desbloquee los mensajes cifrados y confíe en %s. + Ingrese su %s para continuar. + Proteja y desbloquee los mensajes encriptados y confíe en %s. Ingrese su %s nuevamente para confirmarlo. No use la contraseña de su cuenta. - Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor. - Esto puede tardar varios segundos, tenga paciencia. Configurando la recuperación. Tu clave de recuperación Manténlo seguro Terminar - Utilice este %1$s como red de seguridad en caso de que olvide su %2$s. - Publicar claves de identidad creadas Generando clave segura a partir de frase de contraseña Definición de la clave predeterminada de SSSS @@ -2479,54 +2023,42 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Sincronización de la clave de usuario Sincronización de la clave de autofirma Configuración de copia de seguridad de claves - - Tus %2$s y %1$s ahora están configurados. \n -\n¡Mantenlos a salvo! Los necesitará para desbloquear mensajes cifrados y proteger la información si pierde todas sus sesiones activas. - +\n¡Mantenlos a salvo! Los necesitará para desbloquear mensajes encriptados y proteger la información si pierde todas sus sesiones activas. Imprímelo y guárdalo en un lugar seguro Guárdelo en una llave USB o unidad de respaldo Cópielo en su almacenamiento personal en la nube - No puedes hacer eso desde el móvil - - Establecer una frase de contraseña de recuperación le permite proteger y desbloquear mensajes cifrados y de confianza. + Establecer una frase de contraseña de recuperación le permite proteger y desbloquear mensajes encriptados y de confianza. \n \nSi no desea establecer una contraseña de mensaje, genere una clave de mensaje. - Establecer una frase de contraseña de recuperación le permite proteger y desbloquear mensajes cifrados y de confianza. - Si cancela ahora, puede perder mensajes y datos cifrados si pierde el acceso a sus inicios de sesión. + Establecer una frase de contraseña de recuperación le permite proteger y desbloquear mensajes encriptados y de confianza. + Si cancela ahora, puede perder mensajes y datos encriptados si pierde el acceso a sus inicios de sesión. \n \nTambién puede configurar la Copia de seguridad segura y administrar sus claves en Configuración. - - Los mensajes de esta sala están cifrados de extremo a extremo. Obtenga más información y verifique a los usuarios en su perfil. - Cifrado no habilitado - El cifrado utilizado por esta sala no es compatible - + Los mensajes de esta sala están encriptados de Extremo-a-Extremo. Obtenga más información y verifique a los usuarios en su perfil. + Encriptación no habilitada + La encriptación usada por esta sala no es compatible %s creado y configurado la sala. ¡Casi ahí! ¿El otro dispositivo muestra el mismo escudo\? ¡Casi ahí! Esperando confirmación… No se pudieron importar las claves - Mensajes que contienen @room - Mensajes cifrados en chats uno a uno - Mensajes cifrados en chats grupales + Mensajes encriptados en chats 1:1 + Mensajes encriptados en chats de grupo Cuando las salas son actualizadas Solucionar problemas Envía un mensaje como texto estándar, sin interpretarlo como Markdown - Nombre de usuario y / o contraseña incorrectos. La contraseña ingresada comienza o termina con espacios, verifíquela. Esta cuenta ha sido desactivada. - Mejora de encriptación disponible Habilitar la firma cruzada Verifíquese a usted mismo y a los demás para mantener sus chats seguros - Entrar %s Frase de contraseña de recuperación No es una clave de recuperación válida Por favor introduce una clave de recuperación - Comprobando la clave de respaldo Comprobando la clave de respaldo (%s) Obteniendo clave de curva @@ -2535,15 +2067,12 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Generando clave SSSS a partir de clave de recuperación Almacenar el secreto de la copia de seguridad de claves en SSSS %1$s (%2$s) - Ingrese su Frase de contraseña de respaldo de clave para continuar. use su clave de recuperación de Key Backup No conoces tu frase de contraseña de copia de seguridad clave, puedes %s. Clave de recuperación de copia de seguridad - Evitar capturas de pantalla de la aplicación Al habilitar esta configuración, se agrega FLAG_SECURE a todas las actividades. Reinicie la aplicación para que el cambio surta efecto. - Archivo multimedia agregado a la Galería No se pudo agregar el archivo multimedia a la Galería Utilice la última versión de Element en sus otros dispositivos, Element Web, Element Desktop, Element iOS, Element para Android u otro cliente Matrix con capacidad de firma cruzada @@ -2560,28 +2089,22 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Seleccione su clave de recuperación o introdúzcala manualmente escribiéndola o pegándola desde su portapapeles La copia de seguridad no se pudo descifrar con esta clave de recuperación: verifique que ingresó la clave de recuperación correcta. No se pudo acceder al almacenamiento seguro - Sin encriptar - Cifrado por un dispositivo no verificado + Encriptado por un dispositivo no verificado Revise dónde inició sesión Verifique todas sus sesiones para asegurarse de que su cuenta y sus mensajes estén seguros Verifique el nuevo inicio de sesión accediendo a su cuenta: %1$s - Verificar manualmente por texto Verificación interactiva por emoji - Confirme su identidad verificando este inicio de sesión de una de sus otras sesiones, otorgándole acceso a los mensajes cifrados. - Confirme su identidad verificando este inicio de sesión, otorgándole acceso a los mensajes cifrados. + Confirme su identidad verificando este inicio de sesión de una de sus otras sesiones, otorgándole acceso a los mensajes encriptados. + Confirme su identidad verificando este inicio de sesión, otorgándole acceso a los mensajes encriptados. Marcar como de confianza - Lo sentimos, esta operación aún no es posible para las cuentas conectadas mediante el inicio de sesión único. - No pudimos crear tu DM. Marque los usuarios que desea invitar y vuelva a intentarlo. - Primero acepta los términos del servidor de identidad en la configuración. Para su privacidad, Element solo admite el envío de números de teléfono y correos electrónicos de usuario con hash. La asociación ha fallado. No hay asociación actual con este identificador. - Su servidor doméstico (%1$s) propone utilizar %2$s para su servidor de identidad Utilizar %1$s Alternativamente, puede ingresar cualquier otra URL del servidor de identidad @@ -2594,56 +2117,44 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Activar el sonido del micrófono Detén la cámara Enciende la cámara - Configurar copia de seguridad segura - Respaldo seguro - Protéjase contra la pérdida de acceso a los mensajes y datos cifrados haciendo una copia de seguridad de las claves de cifrado en su servidor. + Protéjase contra la pérdida de acceso a los mensajes y datos encriptados haciendo una copia de seguridad de las claves de encriptación en su servidor. Preparar Usa una llave de seguridad Genere una clave de seguridad para almacenar en un lugar seguro, como un administrador de contraseñas o una caja fuerte. Utilice una frase de seguridad Ingrese una frase secreta que solo usted conozca y genere una clave de respaldo. - Guarde su llave de seguridad Guarde su llave de seguridad en un lugar seguro, como un administrador de contraseñas o una caja fuerte. - Establecer una frase de seguridad Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor. Frase de seguridad Ingrese su Frase de seguridad nuevamente para confirmarla. - Guarde su llave de seguridad Guarde su llave de seguridad en un lugar seguro, como un administrador de contraseñas o una caja fuerte. - Nombre de la Sala Tema Cambiaste la configuración de la sala con éxito - No puedes acceder a este mensaje Esperando este mensaje, esto puede tardar un poco No se puede descifrar - Debido al cifrado de extremo a extremo, es posible que deba esperar a que llegue el mensaje de alguien porque las claves de cifrado no se le enviaron correctamente. + Debido a la encriptación de Extremo-a-Extremo, es posible que deba esperar a que llegue el mensaje de alguien porque las claves de encriptación no se le enviaron correctamente. No puede acceder a este mensaje porque ha sido bloqueado por el remitente No puede acceder a este mensaje porque el remitente no confía en su sesión No puede acceder a este mensaje porque el remitente no envió las claves a propósito - Esperando el historial de cifrado - + Esperando al historial de encriptación ¡Nos complace anunciar que hemos cambiado de nombre! Tu aplicación está actualizada y accediste a tu cuenta. Guardar la clave de recuperación en - Agregar desde mi directorio telefónico Tu directorio telefónico está vacío Directorio telefónico Recuperando tus contactos… Tu libro de contactos está vacío Libro de contactos - ¿Revocar la invitación a %1$s\? - Prohibido por %1$s No se pudo anular la prohibición del usuario - Las notificaciones push están deshabilitadas Revise su configuración para habilitar las notificaciones push @@ -2653,9 +2164,92 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu ¡Advertencia! ¡Último intento restante antes de cerrar sesión! Demasiados errores, se ha desconectado Elija un PIN por seguridad - No se pudo validar el PIN, toque uno nuevo. + No se pudo validar el PIN, por favor introduzca uno nuevo. Introduce tu PIN ¿Olvidó su PIN\? No se puede abrir una sala en la que está prohibido. No puedo encontrar esta sala. Asegúrate de que exista. - + Los mensajes en esta sala están encriptados punto-a-punto. + Mensaje directo + Salir + Preferencias + Los mensajes aquí están encriptados de Extremo-a-Extremo. +\n +\nTus mensajes están asegurados con un candado. Solo tú y tú destinatario tenéis las llaves especiales para desencriptarlos. + Los mensajes aquí no están encriptados de Extremo-a-Extremo. + Botones de Bot + Encuesta + Remover de Baja prioridad + Añadir a Baja prioridad + Rotar y recortar + + % segundo + %d segundos + + Por favor, haz click en la notificación. Si no la ves, por favor revisa las preferencias del sistema. + Mostrar notificación + ¡Estás viendo la notificación! ¡Haz click en mí! + Fallo al recibir Push. La solución puede ser el reinstalar la aplicación. + La aplicación está recibiendo PUSH + La aplicación está esperando al PUSH + Probar Push + Búsquedas en salas encriptadas todavía no están soportadas. + Filtrar usuarios excluidos + Enviar la historia de peticiones de claves compartidas + No hay más resultados + No posee permisos para iniciar una llamada + No posee permisos para iniciar una llamada en esta sala + No posee permisos para iniciar una conferencia + Resetear + Descartar cambios + Hay cambios sin salvar. ¿Descartar los cambios\? + La sala todavía no ha sido creada. ¿Cancelar la creación\? + El link está malformado + PIN es requerido cada vez que se abre Element. + PIN es necesario después de no usar Element por 2 minutos. + Requerir PIN después de 2 minutos + Sólo mostrar el número de mensajes no leídos en una notificación sencilla. + Mostrar detalles, como nombres de salas y contenido de mensajes. + Mostrar contenido de notificaciones + Element sólo puede ser desbloqueado via Código PIN. + Activar biometría de este dispositivo en particular, como huellas dactilares o reconocimiento facial. + Activar biometría + Configurar protecciones + Restringir acceso usando PIN y biométricos. + Restringir acceso + + Mostrar el dispositivo con el que puede verificar ahora + Mostrar %d dispositivos con los que puede verificar ahora + + Reiniciará sin historia, mensajes, dispositivos o usuarios verificados + Si resetea todo + Solo haga esto si no tiene otro dispositivo con el que verificar éste. + Resetear todo + ¿Ha perdido o olvidado todas las opciones de recuperación\? Resetear todo + Tú te has unido. + %s se ha unido. + Exportar inspección + ¿Borrar los datos de cuenta de typo %1$s\? +\n +\nPrecaución, puede causar funcionamiento inesperado. + Resultado de la verificación + Reaccionó con: %s + Este servidor particular usa una versión antigua. Pregunta a su administrador si puede actualizarlo. Puedes continuar usándolo, pero algunas características pueden no funcionar. + Tú has configurado como sólo con invitación. + %1$s ha configurado como sólo con invitación. + Añadir imagen de + Mostrar la historia completa en salas encriptadas + Ajustes de la sala + Tema + Tema de la sala (opcional) + Nombre de la sala + %1$s y %2$s + %1$s en %2$s y %3$s + + %d invitación + %d invitaciones + + Incluye invitar/unirse/expulsar/prohibir/mostrar cambios de nombre. + Mostrar eventos de los miembros de la sala + ¡La notificación ha sido cliqueada! + \ 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 73a5a0dff8..152f5f03e2 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -2187,4 +2187,8 @@ Jututoa seadistused Teema Jututoa teema (kui soovid) + Ekspordi rakenduse taustakontrolli andmed + Otsevestlus + Lisa kaasa võtmevahetusega seotud päringute ajalugu + Rohkem otsingutulemusi pole \ 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 e22d517789..ae3292bb2c 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -421,8 +421,8 @@ %d اتاق - %d اعلان - %d اعلان‌ها + %d آگاهی + %d آگاهی زمینه‌تان افزودن کاره‌های ماتریکس @@ -894,12 +894,12 @@ ممکن است کارساز در دسترس نبوده یا شلوغ باشد این‌جا بنویسید… - %d اعلان خوانده‌نشده - %d اعلان‌های خوانده‌نشده + %d پیام آگاهی نخوانده + %d پیام آگاهی نخوانده - %d اعلان خوانده‌نشده - %d اعلان‌های خوانده‌نشده + %d پیام آگاهی نخوانده + %d پیام آگاهی نخوانده %1$s: %2$d پیام @@ -1021,7 +1021,7 @@ ورود کلید بازیابی بازیابی پیام محاسبهٔ کلید بازیابی… - لطفاً‌یک کلید بازیابی وارد کنید + لطفاً یک کلید بازیابی وارد کنید من بودم هرگز پیام‌های رمزشده را از دست ندهید شروع با استفاده از پشتیبان کلید @@ -1334,8 +1334,8 @@ اطّلاعات رویداد اطّلاعات رمزنگاری سرتاسری شاخه - فعال‌کردن رمزنگاری -\n(هشدار: نمی‌تواند دوباره غیر فعال شود!) + به کار انداختن رمزنگاری. +\n(هشدار: نمی‌تواند دوباره از کار بیفتد!) رمزنگاری در این اتاق از کار افتاده است. رونوشت از نشانی اتاق رونوشت از شناسهٔ اتاق @@ -1500,9 +1500,9 @@ المنت در پس زمینه همگام‌سازی می‌کند به گونه ای که منابع محدود دستگاه (باتری) حفظ می‌شود. \nبسته به شارژ گوشی شما، ممکن است همگام‌سازی توسط سیستم‌عامل به تعویق بیوفتد. روشن کردن صفحه برای ۳ ثانیه - • محتوای پیام در اعلان‌ها نمایش داده نمی‌شوند - • اعلان ها حاوی فرا داده و محتوای پیام هستند - • محتوای پیام اعلان به طور ایمن و مستقیم از سرور هیوا دریافت می‌شود + • آگاهی‌ها محتوای پیام را نشان نخواهند داد + • آگاهی‌ها شاکل فراداده و محتوای پیام هستند + • محتوای پیام آگاهی به طور ایمن و مستقیم از کارساز خانگی ماتریکس دریافت می‌شود • اعلان ها فقط حاوی فرا داده هستند • اعلان ها از طریق سرور Firebase ارسال می شوند اگر دستگاه برای مدتی از شارژر جدا باشد و از دستگاه نیز استفاده نشود، گوشی وارد حالت غیر هوشیار می‌شود. در این حالت از دسترسی برنامه‌ها به اینترنت جلوگیری می‌شود و همگام سازی و هشدارهای استاندارد آن‌ها به تعویق می‌افتد. @@ -1543,11 +1543,11 @@ تست‌ها را اجرا کن در حال تشخیص مشکل مطمئن شوید لینک فعال‌سازی‌ای را که به ایمیل شما ارسال شده، باز کرده‌اید. - %s حذف شود؟ - شماره تلفن‌های شما - هیچ آدرس ایمیلی تا کنون به حساب کاربری شما افزوده نشده است - آدرس ایمیل‌های شما - هیچ شماره تلفنی تا کنون به اکانت شما افزوده نشده است + برداشتن %s؟ + شماره تلفن‌ها + هیچ رایانامه‌ای به حسابتان افزوده نشده + نشانی‌های رایانامه + هیچ شماره تلفنی به حسابتان افزوده نشده قابلیت جستجو در اتاق‌های رمزشده هنوز پیاده‌سازی نشده است. نتیجه‌ای در پی نداشت فیلترکردن کاربران مسدود شده @@ -1565,7 +1565,7 @@ %d مورد پرونده‌ها - اعضا + افراد اطلاعات اتاق گواهی را تنها در صورتی تایید کنید که اثر انگشت آن با اثر انگشتی که ادمین سرور ارائه کرده‌است برابر باشد. گواهی سرور تغییر کرده‌است. ممکن است این اتفاق به دلیل تمدید گواهی سرور رخ داده باشد. توصیه می‌شود از ادمین سرور سوال کنید. @@ -1585,8 +1585,8 @@ شما نمی‌توانید این تغییر را بازگردانید. زیرا در حال ارتقای سطح کاربر دیگر به سطح خودتان هستید. \nآیا مطمئن هستید؟ شما در حال دسترسی به %s هستید. آیا می خواهید به این اتاق بپیوندید؟ - این دعوت به %s ارسال شده است، که هیچ ارتباطی با اکانت شما ندارد. -\nممکن است بخواهید با اکانت دیگری وارد شوید یا این ایمیل را به اکانت خود اضافه کنید. + این دعوت به %s ارسال شده که ارتباطی با این حساب ندارد. +\nممکن است بخواهید با حسابی دیگر وارد شده یا این رایانامه را به حسابتان بیفزایید. متاسفانه به دلیل عدم دسترسی، درخواست شما امکان پذیر نمی باشد هیوا می‌تواند با دیدن دفترچه تلفن شما کاربرهای دیگر هیوا را بر اساس ایمبل و شماره تلفنشان پیدا کند. \n @@ -1648,7 +1648,7 @@ %2$s و %1$s شما تنظیم شد. \n \nآن‌ها را در جای مطمئن و امن نگهداری کنید! درصورتی که همه‌ی نشست‌های خود را از دست بدهید، به این دو جهت رمزگشایی پیام‌های رمزشده‌ی قبلی و اطلاعات امن نیاز دارید. - تنظیم پشتیبان‌گیری از کلید + برپایی پشتیبان‌گیری از کلید همگام‌سازی کلید Self Signing همگام‌سازی کلید کاربر همگام‌سازی کلید اصلی @@ -1724,9 +1724,9 @@ دسترسی به پیام‌های رمزشده را از دست خواهید داد مگر اینکه برای بازیابی کلیدهای رمزگذاری خود، به حساب خود وارد شوید. آیا تمامی اطلاعات ذخیره‌شده در این دستگاه پاک شود؟ \nبرای دسترسی به اطلاعات و پیام‌های حساب خود، دوباره وارد شوید. - هشدار: اطلاعات شخصی شما (شامل کلید‌های رمزنگاری) همچنان روی این دستگاه ذخیره شده‌اند. + هشدار: داده‌های شخصیتان (شامل کلید‌های رمزنگاری) همچنان روی این افزاره ذخیره شده‌اند. \n -\nاگر نمی‌خواهید از این دستگاه استفاده کنید، یا می‌خواهید با اکانت دیگری وارد شوید، این اطلاعات را حذف کنید. +\nاگر کارتان با این افزاره تمام شده یا می‌خواهید به حساب دیگری وارد شوید، پاکشان کنید. برای بازیابی کلیدهای رمزگذاری ذخیره شده در این دستگاه، وارد حساب خود شوید. شما برای خواندن همه پیام‌های رمزشده‌ی خود در هر دستگاهی به این کلید‌ها نیاز دارید. ادمین سرور (%1$s) شما را از حسابتان خارج کرده‌است %2$s (%3$s). این می تواند به دلایل مختلف باشد: @@ -1888,7 +1888,7 @@ توضیح در مورد اتاق (اختیاری) نام اتاق پیام پاک شد - به نظر می‌رسد که شما در حال تلاش برای اتصال به یک سرور دیگر هستید. آیا می خواهید از اکانت خود خارج شوید؟ + به نظر می‌رسد تلاش می‌کنید تا به کارساز خانگی دیگری وصل شوید. می‌خواهید خارج شوید؟ برای بازنشانی گذرواژه‌ی خود نیاز به پیکربندی سرور هویت‌سنجی دارید. شما از سرور هویت‌سنجی استفاده نمی‌کنید خطای نامشخص @@ -1899,30 +1899,30 @@ SAS تطابق نداشت Hash تطابق نداشت نشست نمی تواند در مورد روش‌های فرآیند تائید: hash ، MAC یا روش SAS به توافق برسد - نشست از آن تعامل اطلاعی ندارد - زمان فرآیند تائید به پایان رسید - کاربر فرآیند تائید را لغو کرد - %s می‌خواهد نشست شما را تائید کند - درخواست فرآیند تائید - تائید تعاملی نشست - فرآیند تائید لغو شد. + نشست، اطّلاعی از آن تعامل ندارد + زمان فرایند تأیید به پایان رسید + کاربر تأیید را لغو کرد + %s می‌خواهد نشستتان را تأیید کند + درخواست تأیید + تأیید تعاملی نشست + تأیید لغو شد. \nدلیل: %s - طرف مقابل فرآیند تائید را لغو کرد. + طرف مقابل تأیید را لغو کرد. \n%s - از روش قدیمی برای فرآیند تائید استفاده کنید. - چیزی نمایش داده نشده‌است؟ هنوز تمام کلاینت‌ها از امکان تائید به روش تعاملی پشتیبانی نمی‌کنند. از تائید به روش قدیمی استفاده کنید. - شما با موفقیت این نشست را تائید کردید. - منتظر تائید طرف مقابل… - شما یک درخواست فرآیند تأئید داخلی دریافت کرده اید. - این نشست را با تصدیق اعداد زیر که روی صفحه‌ی طرف مقابل نیز ظاهر شده‌است، تائید کنید - با تأئید یکسان بودن شکلک‌های زیر در صفحه طرف مقابل، این نشست را تأئید کنید - با تأئید این نشست، آن را به عنوان معتمد علامت‌گذاری می‌کنیم و همچنین نشست خود را به عنوان معتمد برای طرف مقابل علامت‌گذاری می کنیم. - فرآیند تائید کلید + استفاده از تأیید قدیمی. + چیزی ظاهر نمی‌شود؟ هنوز تمامی کارخواه‌ها از تأیید تعاملی پشتیبانی نمی‌کنند. از تأیید قدیمی استفاده کنید. + این نشست را با موفّقیت تأیید کردید. + منتظر تأیید طرف مقابل… + درخواست تأییدی دریافت کردید. + با تأیید ظاهر شدن عددهای زیر روی صفحهٔ طرف مقابل، این نشست را تأیید کنید + با تأیید ظاهر شدن شکلک‌های زیر روی صفحهٔ طرف مقابل، این نشست را تأیید کنید + تأیید این نشست، آن را برای خودتان و طرف مقابل، به عنوان مطمئن علامت خواهد زد. + تأیید کلید نمایش درخواست درخواست لغو شد فهمیدم - محتوای گفتگوی امن شما با این کاربر رمزنگاری سرتاسر شده و امکان دسترسی به آن‌ها توسط فرد سومی مقدور نیست. - نشست تائید شد! + پیام‌های امن با این کاربر به صورت سرتاسری رمزنگاری شده و قابل خوانده شدن به دست دیگران نیست. + تأییدشده! این نشست را تأیید کنید تا به عنوان معتمد علامت‌گذاری شود. اعتماد به نشست‌ها هنگام استفاده از پیام های رمزشده به صورت سرتاسر ، به شما اطمینان بیشتری از امنیت گفتگو‌ها می‌دهد. درخواست فرآیند تأیید داخلی شروع فرآیند تایید کردن @@ -1975,17 +1975,17 @@ پشتیبان با %d کلید بازیابی شد. پشتیبان با %d کلید بازیابی شد. - رمزگشایی با این کلید بازیابی امکان‌پذیر نیست: لطفاً بررسی کنید که کلید بازیابی را به درستی وارد کرده‌اید. - رمزگشایی پیام‌های قبلی - بارگذاری کلید‌ها… - در حال دریافت کلید‌های بازیابی… - بازیابی نسخه پشتیبان: + پشتیبان نتپانست با این کلید بازیابی رمزگشایی شود: لطفاً تأیید کنید که کلید بازیابی درستی را وارد کرده‌اید. + قفل‌گشایی تاریخچه + بارگذاری کردن کلید‌ها… + بارگیری کردن کلید‌ها… + بازیابی پشتیبان: خطای شبکه: لطفاً اتصال خود را بررسی کنید و دوباره امتحان کنید. رمزگشایی با این کلید امنیتی امکان‌پذیر نیست: لطفاً بررسی کنید که کلید امنیتی را به درستی وارد کرده‌اید. کلید بازیابی خود را گم کرده‌اید؟ می‌توانید کلید جدیدی را در تنظیمات تنظیم کنید. از کلید بازیابی برای رمزگشایی پیام‌های رمزشده‌ی قبلی خود استفاده کنید کلید امنیتی خود را نمی‌دانید؟ شما می‌توانید %s. - از کلید امنیتی برای رمزگشایی پیام‌های رمزشده‌ی قبلی خود استفاده کنید + برای قفل‌گشایی تاریخچهٔ پیام‌های رمزشده‌تان از عبارت عبور بازیابیتان استفاده کنید اگر از دستگاه خارج شوید یا دستگاه خود را از دست دهید، ممکن است امکان دسترسی به پیام های خود را نداشته باشید. کلیدهای رمزگذاری شما اکنون در پس زمینه در حال پشتیبان‌گیری بر روی سرور است. تهیه نسخه‌ی پشتیبان اولیه ممکن است چند دقیقه طول بکشد. در حال تولید کلید پشتیبان با استفاده از کلید امنیتی، این ممکن است چند ثانیه زمان ببرد. @@ -2003,17 +2003,17 @@ ما یک نسخه رمزگذاری شده از کلیدهای شما را در سرور ذخیره خواهیم کرد. با استفاده از کلید امنیتی قوی، از نسخه‌ی پشتیبان خود محافظت کنید. \n \nبرای حداکثر امنیت، کلید امنیتی باید با رمز ورود حساب شما متفاوت باشد. - پیام‌های موجود در اتاق های رمزگذاری شده به صورت سرتاسری رمز و ایمن می‌شوند. کلیدهای این پیام‌ها فقط در دسترس شما و گیرنده‌(ها) می‌باشد. + پیام‌‌ها در اتاق‌های رمزشده، با رمزنگاری سرتاسری امن شده‌اند. فقط شما و گیرنده(ها) کلیدهای خواندم این پیام‌ها را دارید. \n -\nبرای جلوگیری از از دست دادن کلیدهای خود به طور ایمن از آنها پشتیبان تهیه کنید. +\nبرای جلوگیری از گم کردن کلیدهایتان، از آن‌ها به صورت امن، پشتیبان بگیرید. هیچ نشست ماتریکسی موجود نیست - اگر می خواهید المنت یک کلید بازیابی ایجاد کند، لطفاً کلید امنیتی را حذف کنید. - کلید امنیتی بسیار ضعیف است - لطفا کلید امنیتی را وارد کنید - کلید امنیتی یکسان نبود - ورود کلید امنیتی - تائید کلید امنیتی - ایجاد کلید امنیتی + اگر می خواهید المنت یک کلید بازیابی ایجاد کند، لطفاً عبارت عبور را حذف کنید. + عبارت عبور بیش از حد ضعیف است + لطفاً عبارت عبوری وارد کنید + عبارت عبور، مطابق نبود + ورود عبارت عبور + تأیید عبارت عبور + ایجاد عبارت عبور APK معتبر Google Play Services پیدا نشد. اعلان‌ها ممکن است به درستی کار نکنند. %d+ +%d @@ -2048,12 +2048,12 @@ فعال و غیرفعال کردن markdown نام مستعار شما را تغییر می‌دهد اخراج کاربر با شناسه داده شده - بیشتر درباره‌ی اتاق توضیح دهید + تنظیم موضوع اتاق ترک اتاق با نام مستعار داده‌شده به اتاق بپیوندید کاربر با شناسه داده شده را به این اتاق دعوت می کند کاربر با شناسه داده شده را غیر‌فعال می‌کند - سطح اختیارات و دسترسی کاربر را مشخص می‌کند + سطح قدرت کاربر را تعریف می‌کند کاربر با شناسه داده شده را رفع مسدودیت می کند کاربر با شناسه داده شده را مسدود می کند نمایش اقدام @@ -2061,10 +2061,10 @@ دستور ناشناخته: %s خطا در اجرای دستور قابلیت همایش تصویری در حال توسعه بوده و ممکن است به درستی کار نکند. - یک نشست تایید نشده، درخواست کلید‌های رمزنگاری را دارد. + یک نشست تایید نشده، کلید‌های رمزنگاری را درخواست می‌کند. \nنام نشست: %1$s \nآخرین بازدید: %2$s -\nاگر در دستگاه دیگری وارد اکانت خود نشده‌اید این درخواست را نادیده بگیرید. +\nاگر به نشست دیگری وارد نشده‌اید، این درخواست را نادیده بگیرید. نشست تایید نشده‌ی شما \\\'%s\\\' درخواست کلید‌های رمزنگاری را دارد. نشست جدید درخواست دریافت کلید‌های رمزنگاری را دارد. \nنام نشست: %1$s @@ -2080,7 +2080,7 @@ %d دعوت %d دعوت - آدرس سرور + نشانی کارساز خانگی این اتاق شامل نشست‌های تایید نشده هستند. \nهیچ تضمینی وجود ندارد این نشست‌های تائیدنشده متعلق به کاربرانی باشد که فکر می‌کنید. \nتوضیه می‌شود افراد نشست‌های خود را تائید کنند. هر چند در صورتی که تمایلی به این کار ندارید، همچنان می‌توانید پیام ارسال کنید. @@ -2105,27 +2105,27 @@ این اتاق در هیچ اجتماع خاصی قرار نگرفته‌است هر کسی که لینک اتاق را دارد ( حتی اگر کاربر مهمان باشد) هر کسی که لینک اتاق را دارد (به جز کاربران مهمان‌) - تنها افرادی که دعوت شده‌اند - برای لینک دادن به یک اتاق، آن اتاق باید آدرس داشته باشد. + تنها کسانی که دعوت شده‌اند + برای پیوند به یک اتاق، باید نشانی داشته باشد. اجتماع - پخش صدای شاتر دوربین - انتخاب کنید - منبع پیش‌فرض رسانه - انتخاب کنید + پخش صدای شاتر + گزینش + منبع رسانهٔ پیش‌گزیده + گزینش فشرده سازی پیش‌فرض رسانه اطلاعات اضافه: %s خطایی هنگام تایید شماره تلفن رخ داده است. - کد + رمز خطایی هنگام تایید شماره تلفن رخ داده است - کد فعال‌سازی را وارد کنید + رمز فعّال‌سازی‌ای را وارد کنید ما یک پیام کوتاه با کد فعال‌سازی ارسال کرده‌ایم. لطفاً این کد را در زیر وارد کنید. تایید شماره تلفن شماره تلفن نامعتبر برای کشور مورد نظر شماره تلفن - لطفا یک کشور را انتخاب نمائید + لطفاً کشوری را برگزینید کشور - انتخاب کشور + گزینش یک کشور آیا از حذف %1$s %2$s مطمئن هستید؟ خطایی در هنگام تایید ایمیل شما رخ داده است. این شماره تلفن قبلا استفاده شده‌است. @@ -2154,4 +2154,9 @@ ارسال داده های تجزیه و تحلیل تجزیه و تحلیل مجوز دادن + تخصیص شکست خورد. + قطع اتّصال از کارساز هویت %s؟ + پیام مستقیم + ارسال تاریخچهٔ درخواست‌های هم‌رسانی کلید + نتایج بیش‌تری نیست \ No newline at end of file diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 50793add88..d6258d45bd 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -25,7 +25,7 @@ Appel en cours Téléconférence en cours. \nLa rejoindre en %1$s ou en %2$s - vocal + Audio vidéo Impossible d’initier l’appel, réessayez plus tard En raison de permissions manquantes, certaines fonctionnalités peuvent être absentes… @@ -2037,7 +2037,7 @@ Affichage de la notification Vous voyez la notification ! Cliquez-moi dessus ! La notification a été cliquée ! - Échec de l\'appel Element + Appel échoué Les messages de ce salon sont chiffrés de bout en bout. Vous avez créé et configuré ce salon. Confirmer le code pour le désactiver @@ -2089,7 +2089,7 @@ Quitter Actions d\'administrateur Les messages ici ne sont pas chiffrés de bout en bout. - Réagis avec : %s + A réagi avec : %s Sondage Si vous ne connaissez pas votre mot de passe, faites précédent et réinitialisez-le. Veuillez utiliser le format international (le numéro de téléphone doit commencer avec « + ») @@ -2195,4 +2195,8 @@ Sujet Sujet du salon (facultatif) Nom du salon + Message direct + Inclure l\'historique d\'échange de clés + Plus aucun résultat + Exporter le rapport d\'audit \ 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 6ef0b9437d..45a6091093 100644 --- a/vector/src/main/res/values-gl/strings.xml +++ b/vector/src/main/res/values-gl/strings.xml @@ -1,16 +1,13 @@ - + gl ES - Tema claro Tema escuro Tema negro - - Sincronizando + Sincronizando… Notificacións con son Notificacións silenciosas - Mensaxes Sala Configuración @@ -18,9 +15,7 @@ Histórico Informar de erros Detalles da comunidade - Cargando… - Aceptar Cancelar Gardar @@ -40,7 +35,8 @@ A escoita de eventos Adiante Informar sobre contido - Conferencia en curso.\nÚnase con %1$s ou %2$s. + Conferencia en curso. +\nÚnete con %1$s ou %2$s voz vídeo Non se pode iniciar a chamada, inténteo máis tarde @@ -53,7 +49,6 @@ ou Convidar Fóra de liña - Saír Accións Saír @@ -67,53 +62,43 @@ Pechar Copiado ao portaretallos Desactivar - Confirmación Aviso - Inicio Favoritas Xente Salas Comunidades - Buscar salas Buscar favoritas Buscar a xente Buscar salas Buscar comunidades - Convites Baixa prioridade - Conversas Axenda de enderezos local Directorio de usuario Só contactos Matrix Sen conversas - "Non lle permitiu acceder aos contactos locais a Element " + Non lle permitiches a Element acceder ós contactos locais Sen resultados - Salas Directorio de salas Sen salas Sen salas públicas accesibles Enviar unha icona - Licenzas de terceiras partes - Descargar Falar Limpar - 1 usuario - %d usuarios + 1 usuaria + %d usuarias - Convidar Comunidades Sen grupos - Enviar informes Enviar informes de fallos Enviar captura de pantalla @@ -126,10 +111,8 @@ Enviouse o informe de erros correctamente Houbo un problema enviando o informe de erros (%s) Progreso (%s%%) - Enviar a Ler - Unirse a sala Nome de usuario Rexistrar @@ -138,25 +121,20 @@ URL do servidor local URL do servidor de identidade Buscar - Iniciar conversa Iniciar chamada de voz Iniciar videochamada - Seguro que quere comezar a conversar con %s? Seguro que quere comezar unha chamada de audio? Seguro que quere comezar unha chamada de vídeo? - Enviar ficheiros Enviar iconas Sacar unha foto ou vídeo Sacar foto Sacar vídeo - - Non ten ningún paquete de iconas activado. - -Quere engadir algún? - + Non tes paquetes de pegatinas activados. +\n +\nQueres engadir algún\? Acceder Rexistrarse Enviar @@ -187,9 +165,9 @@ Quere engadir algún? Esqueceu o contrasinal? Usar configuracións de servidor personalizadas (avanzado) Comprobe o seu correo para continuar co rexistro - O rexistro tanto co correo como co número non se acepta dentro desta api. Só se vai a ter en conta o número de teléfono. - -Pode engadir a dirección de correo na sección de configuración de perfil. + O rexistro simultáneo co email e o número de teléfono non está soportado ata que o api exista. Só se terá en conta o número de teléfono. +\n +\nPoderás engadir o teu email ó perfil nos axustes. Este servidor local quere asegurarse de que non é un robot Nome de usuario empregado Servidor local: @@ -200,22 +178,16 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Debe introducir un novo contrasinal. Fallo na verificación do enderezo de correo: asegúrese de ter picado na ligazón do correo Petición de chave enviada. - Enviar como Onte Hoxe - Chamar Continuar - Eliminar Rexeitar - Ir á primeira mensaxe non lida. - Deixar a sala Crear - En liña Fóra de liña En pausa @@ -235,13 +207,11 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Non ten permiso para comentar nesta sala 1 mensaxe nova - %d mensaxe nova + %d mensaxes novas - Confiar Desconectar Ignorar - Xente Ficheiros Configuracións @@ -252,14 +222,12 @@ Pode engadir a dirección de correo na sección de configuración de perfil.MENSAXES XENTE FICHEIROS - Convites Iniciar conversa Crear sala Unirse á sala Unirse á sala Buscando cartafol… - Todas as mensaxes (alto) Todas as mensaxes Só mencións @@ -274,20 +242,17 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Versión Copyright Política de privacidade - Correo electrónico Engadir enderezo correo electrónico Teléfono Engada número de teléfono Activar notificacións para esta conta Mensaxes enviadas por bot - Versión versións anteriores Limpar o caché Limpar o caché de imaxes Manter as imaxes - Configuracións dos usuarios Notificacións Outro @@ -296,14 +261,12 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Dispositivos Activar por defecto as vistas previas en liña de URL Mostrar sempre marcas de tempo - "Mostrar marcas de tempo con formato 12 horas (ex. 2:30pm)" + Mostrar marcas de tempo con formato 12-horas Desactivar a miña conta - Analytics Enviar datos de análises Element recolle información analítica anónima para permitirnos mellorar o aplicativo. - Si, quero axuda - + Si, quero axudar! ID Nome Nome do dispositivo @@ -312,55 +275,42 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Autenticación Contrasinal: Enviar - Idioma da Interface Verificación pendente Comprobe o seu correo electrónico e pulse na ligazón que contén. Unha vez feito iso prema continuar. - Xa se está a usar este correo - Xa se está a usar este teléfono - + Xa se está a usar este correo. + Xa se está a usar este teléfono. novo contrasinal confirmar o contrasinal Escolla un país - País Número de teléfono Verificación do teléfono Código - - Aura - 3 días 1 semana 1 mes Para sempre - Foto da sala Nome da sala Asunto Etiqueta de sala Marcado como: - Favorita Prioridade baixa Ningún - Notificacións Quen pode ler o histórico? Quen pode acceder a esta sala? - Calquera Só membros (desde o momento en que se selecciona esta opción) Só membros (desde que foron convidados) Só membros (desde que se uniron) - Só persoas que foron convidadas Calquera que coñeza o enderezo da sala, aparte das convidadas Calquera que coñeza a ligazón a sala, incluíndo as convidadas - Usuarios excluídos - Avanzado Enderezos Labs @@ -369,12 +319,9 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Formato de alias non válido Copiar a ID da sala Copiar a dirección da sala - Directorio Tema - Información do cifrado extremo-a-extremo - Información do evento ID de usuario Chave de identidade Curve25519 @@ -382,15 +329,13 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Algoritmo ID de sesión Fallo ao descifrar - Información do dispositivo do remitente Nome do dispositivo Nome - ID de dispositivo + ID de sesión Chave do dispositivo Validación pegada Ed25519 - Exportar chaves E2E da sala Exportar chaves da sala Exportar @@ -399,49 +344,40 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Importar chaves E2E da sala Importar chaves de sala Importar - Nunca enviar mensaxes cifradas aos dispositivos que non estean verificados neste dispositivo - + Non enviar mensaxes cifradas desde esta sesión a sesións non verificadas. SEN Verificar Verificados Omitidos - dispositivo descoñecido ningún - Verificar Retirar verificación Por na lista negra Quitar da lista negra - Verificar dispositivo Certifico que coinciden as chaves - A sala contén dispositivos descoñecidos Escoller unha sala principal O servidor podería non estar dispoñible ou sobrecargado Escriba un servidor local para saber cales son todas súas as salas públicas URL do servidor local - "Todas as salas do servidor %s " + Todas as salas do servidor %s Todas as salas de %s nativas - Escriba aquí… - - 1 notificación sen ler + %d notificación sen ler %d notificacións sen ler - 1 notificación sen ler + %d notificación sen ler %d notificacións sen ler - 1 sala + %d sala %d salas %1$s en %2$s - Buscar no histórico - Tamaño da letra Anana Pequena @@ -450,13 +386,11 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Máis grande Aínda máis grande Enorme - Fallou a creación do trebello - 1 trebello activo - %d trebellos activos + %d widget activo + %d widgets activos - Non se puido crear o trebello. Fallo ao enviar a petición. O nivel de poder ten que ser un enteiro positivo. @@ -469,63 +403,47 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Hai un parámetro que non é válido. Engadir aplicacións de Matrix Usar a cámara nativa - Iniciar verificación Compartir sen verificar Ignorar petición - Aviso! As chamadas de reunión poderían non ser totalmente estables xa que están en desenvolvemento. - Erro na orde Comandos que non se recoñecen: %s - Off Ruidoso - Mensaxe cifrada - Crear Crear comunidade Nome da comunidade Exemplo ID da comunidade exemplo - Inicio Xente Salas Sen usuarios - Salas Uniuse Convidada Filtrar os membros do grupo Filtrar as salas dos grupos - O administrador da comunidade non fixo unha descrición longa desta comunidade. - Foi expulsado de %1$s por %2$s Foi bloqueado de %1$s por %2$s Motivo: %1$s Volver a unirse Esquecer sala - Avatar de recepción Avatar de aviso Avatar - Revisar agora - Desactivar conta Para continuar introduza o seu contrasinal: Desactivar conta - Enviar voz - continuar con… Non se atopou unha aplicación que poida completar iso. - Enviouse un correo a %s. Unha vez abra a ligazón que contén prema abaixo. A URL ten que comezar con http[s]:// Non se puido acceder: erro da rede @@ -534,51 +452,40 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Non se puido rexistrar Non se puido rexistrar: erro coa propiedade do correo electrónico Introduza unha URL válida - Usuario/contrasinal incorrecto JSON con defectos Non contiña unha JSON correcta Enviáronse demasiadas peticións Este nome de usuario xa se está a usar Unha ligazón de correo na que aínda non se premeu - Volver a pedir as chaves de cifrado do outro dispositivo seu. - Petición enviada Inicie Element noutro dispositivo que poida descifrar esta mensaxe e que despois desde alí lle poida enviar as chaves a este dispositivo. - - 1 cambio de membros - %d cambio de membros + 1 cambio de participantes + %d cambios de participantes - Autor Inicial Mediano Pequeno - Cancelar a descarga? Cancelar a subida? %d s %1$dm %2$ds - Nome da sala Tema da sala - Chamada establecida Chamada finalizada Chamando… Chamada entrante Videochamada entrante Chamada de audio entrante - Chamada en activo - + Chamada activa… Fallou a conexión desde o outro lado. Non se deu iniciado a cámara chamara respondida noutro lugar - Saque unha foto ou video Non foi posible gravar vídeo - Información Gardado Gardar nas descargas? @@ -586,11 +493,10 @@ Pode engadir a dirección de correo na sección de configuración de perfil.NON Unirse Vista previa - O convite envióuselle a %s, mais non é alguén que estea asociado con esta conta. -Seguramente queira conectarse cunha conta distinta, ou engadir este correo a súa conta. + Este convite enviouse a %s, que non está asociada a esta conta. +\nPodes conectarte cunha conta diferente, ou engadir este email á túa conta. unha sala Esta é unha vista previa desta sala. Desactiváronse as interaccións coa sala. - Nova conversa Engadir membro @@ -598,14 +504,12 @@ Seguramente queira conectarse cunha conta distinta, ou engadir este correo a sú %d participantes activos - 1 membro - %d membros + 1 participante + %d participantes 1 membro - Chamar DISPOSITIVOS - Deixar a sala Borrar desta sala Mensaxes non enviadas. %1$s ou %2$s agora? @@ -613,10 +517,8 @@ Seguramente queira conectarse cunha conta distinta, ou engadir este correo a sú Pegada (%s): CONVIDADO UNIUSE - Cancelar a subida Cancelar a descarga - UNIRSE DIRECTORIO FAVORITOS @@ -636,7 +538,6 @@ Seguramente queira conectarse cunha conta distinta, ou engadir este correo a sú • O contido da mensaxe das notificacións está provén dun xeito seguro e directo do servidor local de Matrix • As notificacións conteñen metadatos e os datos da mensaxe • As notificacións non van a mostrar o contido da mensaxe - Son das notificacións Activar notificacións para este dispositivo Petición de sincronización esgotada @@ -651,22 +552,17 @@ Seguramente queira conectarse cunha conta distinta, ou engadir este correo a sú Element pode estar agochado e seguir traballando na xestión das notificacións dun xeito seguro e privado (inda que iso podería afectar ao uso da batería). Outorgar permisos Escolla outra opción - Accedeuse como Servidor de identidade - Seguro que quere eliminar este tipo de notificacións? - O ID interno desta sala é Precisa saír da aplicación primeiro para poder activar o cifrado. Nunca enviar mensaxes cifradas aos dispositivos que non estean verificados nesta sala desde este dispositivo. - O cifrado está activado nesta sala. - " Para verificar que se pode confiar neste dispositivo, contacte co seu dono utilizando algún outro medio (ex. en persoa ou chamada de teléfono) e pregúntelle se a clave que ven nos axustes de usuario do se dispositivo coincide coa clave inferior:" + Comfirma comparando o seguinte cos Axustes de Usuaria na túa outra sesión: Engadiu un novo dispositivo «%s» que está a solicitar as chaves de cifrado. O seu dispositivo sen verificar «%s» está solicitando as chaves de cifrado. Para continuar usando o servidor %1$s ten que revisar primeiro os seus termos e condicións. Esquezan todas as mensaxes que eu enviara no momento en que elimine a miña conta. (Aviso: iso suporá que os seguintes participantes só verán unha versión incompleta das conversas.) Introduza o seu contrasinal. - - + \ No newline at end of file diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index d9e29c37bf..60171a30d1 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -188,7 +188,7 @@ A hívott fél nem vette fel. Médiacsatlakozás sikertelen A kamera nem készíthető elő - a hívást máshol vették fel + a hívás más eszközön lett felvéve Kép vagy videó készítése Videorögzítés sikertelen Információ @@ -206,8 +206,8 @@ A Elementnek engedélyre van szüksége a mikrofonod és kamerád eléréséhez, hogy videohívást tudj indítani. \n \nEngedélyezd a hozzáférést a következő felugró ablakon, hogy hívást tudj indítani. - A Element a névjegyekben lévő e-mail és telefonszám alapján képes felkutatni más Matrix felhasználókar. Ha egyetértesz a névjegyek ilyen célú megosztásával, kérlek engedélyezd a hozzáférést a következő felugró üzenetben. - A Element a névjegyekben lévő e-mail és telefonszám alapján képes felkutatni más Matrix felhasználókar. + A Element a névjegyekben lévő e-mail és telefonszám alapján képes felkutatni más Matrix felhasználókat. Ha egyetértesz a névjegyek ilyen célú megosztásával, kérlek engedélyezd a hozzáférést a következő felugró üzenetben. + A Element a névjegyekben lévő e-mail és telefonszám alapján képes felkutatni más Matrix felhasználókat. \n \nEgyetértesz a névjegyek ilyen célú megosztásával\? "Elnézést. A művelet nem lett végre hajtva hiányzó engedélyek miatt" @@ -424,7 +424,7 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< Alacsony prioritású Semmi Hozzáférés és láthatóság - Listázza ezt a szobát a szobák könyvtárba + Listázza ezt a szobát a szoba listában Szoba hozzáfárés Szoba előzmény olvashatósága Ki tud előzményt olvasni? @@ -505,7 +505,7 @@ Figyelmeztetés: ez a fájl törlésre kerülhet, ha az alkalmazást törli.Eltávolítás feketelistáról Munkamenet hitelesítése Hogy ellenőrizni lehessen, hogy ez a munkamenet megbízható, kérlek használj más kommunikáció módot a tulajdonossal (pl.: személyesen vagy telefonon keresztül) és kérdezd meg hogy a kulcs amit lát a Felhasználói Beállítások alatt megegyezik-e az alábbi kulccsal: - Ha egyezik, nyomja meg a hitelesítés gombot. Ha nem, akkor valaki más elfogta ezt a munkamenetet és érdemes lenne tiltólistára tenni. A jövőben ez a hitelesítési mód kényelmesebbé lesz téve. + Ha nem egyeznek, akkor a kommunikáció biztonsága kompromittálva lehet. A jövőben ez a hitelesítési mód kényelmesebbé lesz téve. Hitelesítem, hogy a kulcsok egyeznek Szoba ismeretlen munkameneteket tartalmaz Ez a szoba ismeretlen munkameneteket tartalmaz. @@ -515,7 +515,7 @@ Figyelmeztetés: ez a fájl törlésre kerülhet, ha az alkalmazást törli. Válassz egy szoba könyvtárat A szerver lehet nem elérhető vagy túltöltött - Írj be egy Matrix szervert hogy listázza belőle a nyilvános szobákat + Írj be egy Matrix szervert, az ott található nyilvános szobák listázásához Matrix szerver URL Összes szoba a %s szerveren Összes anyanyelvi %s szoba @@ -763,7 +763,7 @@ Matrixban az üzenetek láthatósága hasonlít az e-mailre. Az üzenet törlés Matrix-kisalkalmazás-token törlése Ez a szoba le lett cserélve és már nem aktív A beszélgetés itt folytatódik - Ez a szoba a folytatása egy másik beszélgetésnek + Ez a szoba egy másik beszélgetés folytatása Régebbi üzenetek megjelenítéséhez kattints ide A műveletet a hiányzó engedélyek miatt nem lehet végrehajtani. @@ -803,7 +803,7 @@ Matrixban az üzenetek láthatósága hasonlít az e-mailre. Az üzenet törlés Erőforrás korlát túllépve Kapcsolatfelvétel az adminisztrátorral vedd fel a kapcsolatot a szolgáltatás adminisztrátorával - Ez a Matrix szerver túllépte valamely erőforrás korlátot így néhány felhasználó nem tud bejelentkezni. + Ez a Matrix szerver túllépte valamely erőforrás korlátot így néhány felhasználó nem tud majd bejelentkezni. Ez a Matrix szerver túllépte egyik erőforrás korlátját. Ez a Matrix szerver elérte a havi aktív felhasználói korlátját így néhány felhasználó nem tud bejelentkezni. Ez a Matrix szerver elérte a havi aktív felhasználói korlátját. @@ -963,9 +963,9 @@ Helyezd biztonságba a kulcsokat, hogy ne vesszenek el. A Visszaállítási Kulcs ide lett mentve: \'%s\'. Figyelmeztetés: ez a fájl törlésre kerülhet, ha az alkalmazást törlik. - Kérlek készíts egy másolatot + Kérlek, készíts egy másolatot! Visszaállítási Kulcs megosztása… - Visszaállítási Kulcs készítése jelmondatból, ez néhány másodpercet is igénybe vehet. + Visszaállítási Kulcs készítése jelmondatból, ez néhány másodpercet igénybe vehet. Visszaállítási Kulcs Váratlan hiba Mentés elkezdődött @@ -1032,9 +1032,9 @@ Figyelmeztetés: ez a fájl törlésre kerülhet, ha az alkalmazást törlik.(Haladó) Kulcsok kimentése kézzel Védd a mentésedet jelmondattal. - "A kulcsaid másolatait titkosítva a Matrix szerverünkön fogjuk tárolni. Védd a mentést jelszóval a biztonság érdekében. - -A maximális biztonság eléréséhez, használja más jelmondatot mint amit a felhasználói fiókhoz használtál." + A kulcsaid másolatait titkosítva a Matrix szervereden fogjuk tárolni. Védd jelszóval a mentést, a biztonság érdekében. +\n +\nA maximális biztonság eléréséhez, használj más jelszót, mint amit a bejelentkezéshez használtál. Mentés készítése Vagy védd a mentésedet egy Visszaállítási Kulccsal amit tárolj biztonságos helyen. (Haladó) Beállítás Visszaállítási Kulccsal @@ -1043,7 +1043,7 @@ A maximális biztonság eléréséhez, használja más jelmondatot mint amit a f A Visszaállítási Kulcs egy biztosíték amit használhatsz a titkosított üzenet hozzáférések visszaállítására, ha a jelmondatot elfelejtetted. A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókezelő (vagy széf) A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókezelő (vagy széf) - Készítettem másolatot + Készítettem egy másolatot Megosztás Soha ne veszíts el titkosított üzenetet Kulcs Mentés használatának megkezdése @@ -1127,7 +1127,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Ellenőrzési kérés érkezett Munkamenet ellenőrzése és beállítás megbízhatónak. A partnerek munkameneteiben való megbízás megnyugtató lehet, ha végponttól végpontig titkosítást használsz. A munkamenet ellenőrzése megbízhatónak fogja jelezni az eszközt és a partnernél a te munkamenetedet szintén megbízhatónak fogja jelezni. - Munkamenet ellenőrzése az alábbi emodzsik a partner képernyőjén való megjelenésének megerősítésével történik + Munkamenet ellenőrzése azáltal, hogy összehasonlítjátok, hogy a másik felhasználó képernyőjén is ugyan azok az emojik jelennek-e meg. Munkamenet ellenőrzése az alábbi számok a partner képernyőjén való megjelenésének megerősítésével történik Bejövő ellenőrzési kérés érkezett. Kérés megjelenítése @@ -1307,7 +1307,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró A munkamenet nyilvános neve látható azoknál akikkel beszélgetsz Nem használsz Azonosítási Szervert Nincs beállítva azonosítási szerver amire a jelszó visszaállításához szükség van. - Úgy látszik másik matrix szerverhez szeretnél csatlakozni. Kijelentkezel\? + Úgy tűnik, másik Matrix szerverhez szeretnél csatlakozni. Ki szeretnél jelentkezni\? Azonosítási szerver Azonosítási szerverről lecsatlakozás Azonosítási szerver beállítása @@ -1430,18 +1430,18 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró %1$s hozzáférhetővé tette a szobát bárkinek, aki ismeri a linket. %1$s beállította, hogy a szobába csak meghívóval lehessen belépni. Olvasatlan üzenetek - Szabadítsd fel a kommunikációdat. + A te beszélgetésed. Vedd birtokba! Beszélgess másokkal közvetlenül vagy csoportosan Beszélgess bizalmasan, titkosítást használva Bővítsd és szabd testre a élményt Kezdj neki Válassz szervert Hasonlóan az e-mailhez, egy fiókod van, de bárkivel tudsz beszélgetni - Milliók csatlakoznak ingyen a legnagyobb nyilvános szerveren - Prémium üzemeltetés szervezetek részére + Csatlakozz a milliónyi felhasználóhoz a legnagyobb nyilvános szerveren + Prémium szerver üzemeltetés szervezetek részére Tudj meg többet - Más - Személyre szabott szerver cím beállítása + Egyéni + Másik szerver cím megadása Folytatás Csatlakozás ide: %1$s Csatlakozás Element Matrix Services hoz @@ -1452,7 +1452,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró SSO-val való folytatás Element Matrix Services Cím Cím - Prémium üzemeltetés szervezetek részére + Prémium szerverüzemeltetés szervezetek részére Add meg az általad használt Modular szerver, vagy a hozzá tartozó Element címét Add meg a szerver vagy Element címét amihez csatlakozni szeretnél Az oldal betöltésekor hiba történt: %1$s (%2$d) @@ -1529,13 +1529,13 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Látták: Kijelentkeztél - A következő okok miatt lehet: -\n -\n• Másik munkamenetedben megváltoztattad a jelszavadat. -\n -\n• Törölted ezt a munkamenetedet egy másik munkamenetből. -\n -\n• A matrix szerver adminisztrátora biztonsági okokból érvénytelenítette a hozzáférésed. + A következő okok miatt lehet: +\n +\n• Másik munkamenetedben megváltoztattad a jelszavadat. +\n +\n• Törölted ezt a munkamenetedet egy másik munkamenetből. +\n +\n• Az általad használt Matrix szerver adminisztrátora biztonsági okokból érvénytelenítette a hozzáférésed. Lépj be újra Kijelentkeztél Bejelentkezés @@ -1546,7 +1546,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Személyes adatok törlése Figyelmeztetés: A személyes adataid (beleértve a titkosító kulcsokat is) továbbra is az eszközön vannak tárolva. \n -\nHa az eszközt nem használod tovább vagy másik fiókba szeretnél bejelentkezni, töröld őket. +\nHa az eszközt nem használod tovább, vagy másik fiókba szeretnél bejelentkezni, töröld őket. Minden adat törlése Adat törlése Biztos vagy benne, hogy minden az eszközön tárolt adatot törölni szeretnél\? @@ -1554,7 +1554,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Elveszted a hozzáférésedet a titkosított üzeneteidhez ha nem jelentkezel be a titkosítási kulcsok visszaállításához. Adat törlése A jelenlegi munkamenet %1$s felhasználóhoz tartozik és %2$s azonosítási adatait adtad meg. Ez Element-ben nem támogatott. -\nElőször töröld az adatokat, majd a másik felhasználói fiókba lépj be. +\nElőször töröld az adatokat, utána lépj be a másik fiókba! A matrix.to linked hibás A leírás túl rövid Első szinkronizáció… @@ -1579,7 +1579,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Megbízhatatlan belépés Egyeznek Nem egyeznek - Hitelesítheted a felhasználót, ha megerősíted, hogy az alábbi egyedi emodzsik azok amik ugyanabban a sorrendben megjelentek a képernyőjén. + Hitelesítheted a felhasználót, ha megerősíted, hogy ugyan azok az emojik jelennek meg az ő képernyőjén is, ugyan abban a sorrendben. A legnagyobb biztonság érdekében használj megbízható kommunikációs csatornát vagy tedd meg személyesen. Keresd a zöld pajzsot, hogy biztos lehess abban, hogy a felhasználó megbízható. Bízz meg a szoba minden felhasználójában, hogy a szoba biztonságos lehessen. Nem biztonságos @@ -1607,9 +1607,9 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Kód beolvasása Nem lehet beolvasni Ha nem vagy ott személyesen akkor inkább hasonlítsd össze az emodzsikat - Ellenőrzés emodzsikkal + Ellenőrzés emojik összehasonlításával Ellenőrzés emodzsival - Ha az alábbi kódot nem tudod beolvasni, ellenőrizd a rövid egyedi emodzsik összehasonlításával. + Ha az alábbi kódot nem tudod beolvasni, ellenőrizd a felhasználót néhány egyedi emoji összehasonlításával. QR kód kép Ellenőrzés: %s Ellenőrizve: %s @@ -1797,7 +1797,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró \nHa nem akarsz Üzenet Jelszót beállítani, hozz létre inkább Üzenet Kulcsot. Az Visszaállítási Jelmondat beállításával biztonságba helyezheted és hozzáférhetsz a titkosított üzeneteidhez valamint a bizalomhoz. Titkosítás bekapcsolva - Ebben a szobában az üzenetek végpontok között titkosítottak. További információkért és ellenőrzéshez nyisd meg a felhasználók profilját. + Ebben a szobában az üzenetek végpontok között titkosítottak. További információkért és ellenőrzéshez nyisd meg a felhasználók profiljait! Titkosítás nincs engedélyezve A szobában használt titkosítás nem támogatott %s elkészítette és beállította a szobát. diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index ae1fed2d19..d7eb7a29d4 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -2250,4 +2250,8 @@ Argomento Argomento stanza (facoltativo) Nome stanza + Esporta revisione + Messaggio diretto + Invia cronologia di richieste condivisione chiave + Nessun altro risultato \ No newline at end of file diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 6a48529dbe..e764f5001b 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -806,7 +806,7 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ 畳む メッセージとエラーの場合 とにかく通話する - 受け取る + 了承 このホームサーバーの方針を閲覧し承認してください: 通話設定画面 着信にElementの既定の着信音を使う @@ -1045,4 +1045,57 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ 他の利用可能な言語 メッセージエディタ 環境設定 + このデバイスを設定 + セキュアバックアップをリセット + セキュアバックアップを設定 + 管理 + セキュアバックアップ + 部屋を作成中… + 招待されています + %s からの招待 + このセッションは正常に検証されました。 + 概ね完了しました。%s の画面にも同じシールドアイコンが表示されていますか? + 相手のユーザーのデバイスのコードをスキャンし、相互に安全性を検証します。 + 相手のコードをスキャン + スキャンできません + 断る + 検証リクエスト + 通話の開始前に確認する + 意図しない通話を阻止する + お使いの端末は脆弱性のある古いTLSセキュリティプロトコルを使用しています、このセキュリティでは接続できません + SSLエラー + SSLエラー:相手のアイデンティティが認証されていません。 + このURLからホームサーバーに接続できませんでした、ご確認ください + 有効なMatrixサーバーアドレスではありません + このURLは検索結果に表示できません、ご確認ください + この電話番号はすでに使用されています。 + 復旧用のメールアドレスを設定します。後からオプションでメールアドレスや電話番号を使用して知人に見つけてもらえるようにできます。 + シングルサインオンを使用してサインインする + HDを使用する + HDを使用しない + 背面 + 前面 + カメラの切り替え + ワイヤレスヘッドセット + ヘッドセット + スピーカー + 電話 + サウンドデバイスを選択 + リアルタイム接続を確立できませんでした。 +\nホームサーバーの管理者に、通話が正常に動作するためにTURNを設定するようご連絡ください。 + 再び表示しない + アイデンティティサーバーが設定されていません。 + 終了 + 拒否 + 承諾 + 中止 + ウィジェットを削除できませんでした + ウィジェットを追加できませんでした + ビデオ通話を開始 + 会議が進行中です! + 通話を開始する権限がありません + このルームで通話を開始する権限がありません + グループ通話を開始する権限がありません + リセット + なし \ No newline at end of file diff --git a/vector/src/main/res/values-land/styles.xml b/vector/src/main/res/values-land/styles.xml new file mode 100644 index 0000000000..df40b71183 --- /dev/null +++ b/vector/src/main/res/values-land/styles.xml @@ -0,0 +1,8 @@ + + + + + + \ 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 48f9e87737..c6275dd936 100644 --- a/vector/src/main/res/values-nb-rNO/strings.xml +++ b/vector/src/main/res/values-nb-rNO/strings.xml @@ -1,9 +1,8 @@ - + nb NO Latn - Lyst tema Mørkt tema Svart tema @@ -18,7 +17,6 @@ Send et klistremerke Er du sikker\? Laster… - OK Lukk Lagre @@ -51,7 +49,6 @@ Ignorer Gjennomgang Avslå - Avslutt Handlinger Logg ut @@ -59,15 +56,12 @@ Lukk Kopiert til utklippstavle Slå av - Advarsel Feil - Hjem Folk Rom Samfunn - Invitasjoner Lavprioritet Ingen treff @@ -76,12 +70,9 @@ Inviter Samfunn Ingen grupper - Meld fra om en bug Fremgang (%s%%) - Les - Bli med i rommet Brukernavn Opprett konto @@ -89,10 +80,8 @@ Logg ut Identitetstjener-URL Søk - Ta et bilde Spill inn en video - Logg inn Opprett konto Send @@ -106,26 +95,21 @@ Glemt passord\? Jeg har verifisert E-postadressen min Vennligst skriv inn en gyldig URL - Original Store Medium Små - I går I dag - Informasjon Lagret JA NEI Fortsett - Fjern Bli med Forhåndsvisning Avvis - Synkroniserer … 1sek @@ -143,9 +127,7 @@ 1d %dd - Lag - Tilkoblet Frakoblet Rolig @@ -154,7 +136,6 @@ Opphev utestengelse Spark ut %1$s %2$s - Søk Filen ble ikke funnet Stol på @@ -164,10 +145,8 @@ Filer Instillinger BLE MED - Avbryt opplastning Avbryt nedlasting - Ingen treff ROM ROM @@ -184,14 +163,10 @@ E-post Telefon Åpne Innstillinger - Slå på - Slå på - Vanlig Varslingslyd - Versjon Opphavsrettighet Retningslinjer for personvern @@ -206,14 +181,12 @@ Analyser Send analytiske data Ja, jeg vil hjelpe til! - ID Offentlig navn Sist sett Autentisering Passord: Send - Logget inn som Identitetstjener Brukergrensesnitt @@ -224,7 +197,6 @@ Nytt passord Bekreft nytt passord Passordene er ikke like - Land Telefonnummer Kode @@ -236,20 +208,16 @@ 1 uke 1 måned For alltid - Emne Lavprioritet Ingen - Varsler Alle Bannlyste brukere - Avansert Adresser Mappe Tema - Hendelsesinformasjon Bruker-ID Algoritme @@ -261,14 +229,11 @@ Eksporter Importer Svartelistet - ingen - Bekreft Svarteliste Hjemmetjener-URL Skriv her … - Rom Meg Skriftstørrelse @@ -278,40 +243,31 @@ Større Det største Enorm - Åpne i nettleser Din bruker-ID Ditt tema Modul-ID Rom-ID - - Tillat Bekreft Del Ignorer - Av Lag Eksempel eksempel - Hjem Folk Ble med Invitert Bli med igjen Glem rommet - Profilbilde - Deaktiver kontoen Deaktiver kontoen - Vennligst skriv inn et brukernavn. utvid skjul - Alltid %1$s: %1$s: %2$s @@ -321,7 +277,6 @@ Lagre som fil Erstatt Stopp - Uventet feil Er du sikker\? Aldri mist krypterte beskjeder @@ -329,23 +284,17 @@ Versjon Algoritme Signatur - Verifisert! Jeg forstår - Verifiseringsforespørsel Ukjent feil - Rediger Svar - Prøv igjen Invitert av %s - Reaksjoner Liker Reaksjoner - Endre Vennligst vent … Nytt rom @@ -355,20 +304,14 @@ Sikkerhet og personvern Ekspert Format: - Stemme og video Hjelp/Om - - Vis skjulte hendelser i tidslinjen - Venter … (redigert) - Vilkår for bruk Bytt ut identitetstjener Venter - Opprett et nytt rom Vis passord Skjul passord @@ -385,54 +328,41 @@ Registrer deg Logg inn Fortsett med SSO - Neste E-post Nytt passord - Fortsett - Jeg har verifisert E-postadressen min - Suksess! Passordet ditt har blitt tilbakestilt. Advarsel E-post E-post (valgfritt) Neste - Telefonnummer Neste - Skriv inn kode Neste - Brukernavn Passord Neste Advarsel Vennligst sjekk eposten din Sett av - Logg på Logg på Passord Instillinger Gjeldende økt Føyer til ¯\\_(ツ)_/¯ på en råtekstmelding - Video. Bilde. Lyd Fil - Du avbrøt Du aksepterte Verifiseringsforespørsel - - Du - Verifiser %s Verifiserte %s Sikkerhet @@ -444,24 +374,18 @@ Tilpasset Invitasjoner Brukere - Opphev ignorering - Tidslinje - Vil du skru på kryptering\? Bekreft Advarsel - Sesjoner Ja Advarsel: Bekreft fjerning Status.im-tema - Lytter etter hendelser Verifiser økten - Aktiv samtale Send loggbøker Send tilbakestillings-E-post @@ -474,7 +398,6 @@ %d medlemmer 1 medlem - %1$s nå %s skriver … Søk @@ -490,15 +413,12 @@ Element samler inn anonyme statistikker for å hjelpe oss med å forbedre programmet. %1$s @ %2$s Integreringsbehandler - Rommets navn Hvem kan lese historikken\? Hvem kan gå inn i dette rommet\? - Kun medlemmer (f.o.m. da denne innstillingen ble valgt) Kun medlemmer (f.o.m. da de ble invitert) Kun medlemmer (f.o.m. de ble med) - Kun folk som har blitt invitert Dette rommet viser ikke merkeskilter for noen samfunn Kopier rommets ID @@ -513,7 +433,6 @@ 1 rom %d rom - %1$s: 1 melding %1$s: %2$d meldinger @@ -522,7 +441,6 @@ %d varsel %d varsler - Ny hendelse Nye meldinger Ny invitasjon @@ -531,8 +449,6 @@ 1 aktiv modul %d aktive moduler - - Modul Last inn modul Ditt visningsnavn @@ -545,21 +461,17 @@ Velg rommets tema Stille Bråkete - Kryptert melding - Opprett et samfunn Samfunnsnavn Samfunns-ID Rom Ingen brukere - Rom 1 medlem %d medlemmer - 1 rom %d rom @@ -571,57 +483,45 @@ Begynn å bruke Nøkkelsikkerhetskopiering Det var meg Begynn å bruke Nøkkelsikkerhetskopiering - Sendte deg en invitasjon Rom Opprett et nytt rom Rom Direktemeldinger - Rommets navn URL: Direktemeldinger - Meldingsredigeringer Filtrer samtaler … Opprett et nytt rom Blir med i rommet … - Identitetstjener Skriv inn en ny identitetstjener Send vedlegg - Klistremerke Det er søppelpost Alle meldinger Kun nevninger Forlat rommet Uleste meldinger - Kom i gang - Velg en tjener Tilbakestill passordet på %1$s Advarsel! Velg en E-postadresse Velg et telefonnummer Godkjenn vilkårene for å fortsette - Avanserte innstillinger Utviklermodus Dersom dette først har blitt skrudd på, kan kryptering aldri bli skrudd av. - De samsvarer Ikke sikker Venter … Verifiser denne økten QR-kodebilde - Forlat rommet Forlater rommet … - Dersom dette først har blitt skrudd på, kan kryptering aldri bli skrudd av. - Aktive økter Vis alle økter Behandle økter @@ -629,18 +529,13 @@ Verifisert Betrodd Ikke betrodd - QR-kode - Nei - Utviklerverktøy Ny innlogging - Fjern … Bråkete notifikasjoner Stille notifikasjoner - Samfunnsdetaljer Sikkerhetskopiering av nøkler Bruk sikkerhetskopiering av nøkler @@ -653,9 +548,7 @@ Bruk sikkerhetskopiering av nøkler Sikkerhetskopi Du kommer til å miste tilgang til dine enkrypterte meldinger med mindre du sikkerhetskopierer nøklene dine før du logger av. - Tredjepartslinsenser - Bli Snakk Se dekryptert kilde @@ -686,9 +579,7 @@ Filtrer folk Filtrer romnavn Filtrer samfunnsnavn - Systemadvarsler - Samtaler Lokal adressebok Brukerkatalog @@ -696,14 +587,12 @@ Ingen samtaler Du ga ikke Element tilgang til dine lokale kontakter Ingen identitetsserver konfigurert. - Romkatalog Ingen offentlige rom tilgjengelig - 1 bruker + %d bruker %d brukere - Send kjæsjlogg Send skjermbilde Vennligst forklar feilen. Hva gjorde du\? Hva forventet du at skulle skje\? Hva skjedde i stedet\? @@ -713,7 +602,6 @@ Det ser ut som du rister på telefonen i frustrasjon. Har du list til å åpne feilrapporteringsskjermen\? Applikasjonen kræsjet sist gang. Har du lyst til å åpne kræsjskjermen\? Sinnarist for å rapportere feil - Feilrapport har blitt sendt feilrapporten feilet å sendes (%s) Send inn i @@ -721,17 +609,13 @@ Start ny chat Start lydsamtale Start videosamtale - Send lydopptak - Er du sikker på du vil starte en samtale med %s\? Er du sikker på du vil starte en lydsamtale\? Er du sikker på du vil starte en videosamtale\? Spill av Pause Avslå - - Du har ikke rettigheter til å starte en konferansesamtale i dette rommet Det pågår allerede en konferanse! Start videomøte @@ -743,12 +627,10 @@ Kunne ikke fjerne utvidelse Kopier Suksess - Varsler Oppringing feilet som følge av feilkonfigurert tjener Prøv med %s Ikke spør meg igjen - Element samtale feilet Velg lydenhet Telefon @@ -760,17 +642,14 @@ Bak Slå HD av Slå HD på - Send filer Send klistremerke Ta bilde eller video Du har ingen pakker med klistremerker slått på. \n \nLegge til noen nå\? - fortsett med… Dessverre, kunne ikke finne en passende ekstern applikasjon for å utføre denne handlingen. - Logg på med single sign-on E-post eller brukernavn Brukernavn @@ -797,7 +676,6 @@ Forlat rommet %1$s for %2$s siden - Direktemeldinger Forlat dette rommet Fjern fra dette rommet @@ -805,9 +683,7 @@ Utnevn til admin Ignorer bruker Ignorer - Opphev ignorering - Bannlys bruker "%1$s, " %1$s og %2$s @@ -818,7 +694,6 @@ 1 ny melding %d nye meldinger - Romdetaljer 1 valgt @@ -827,13 +702,11 @@ INVITERT MELDINGER FILER - Opprett et rom Bli med i rommet Bli med i et rom Betingelser og vilkår Personvernregler - Legg til E-postadresse Legg til telefonnummer Avanserte varselsinnstillinger @@ -841,7 +714,6 @@ Kontoinnstillinger. Start ved systemoppstart Skru av restriksjoner - Batterioptimalisering Redusert privatliv Skru på varsler for denne kontoen @@ -850,7 +722,6 @@ Start ved systemoppstart Betingelser og vilkår Tøm mediemellomlager - Brukerinnstillinger Ignorerte brukere Integreringer @@ -863,23 +734,19 @@ Behandle dine oppdagelsesinnstillinger. Gi tillatelse Velg et annet alternativ - Gi tillatelse - Datasparingsmodus Øktinformasjon Hjemmetjener Tillat integreringer Integreringer er skrudd av Velg språk - Verifisering avventer Denne E-postadressen ble ikke funnet. Oppdater passord Telefonverifisering Standardkomprimering Merket som: - Tilgang og synlighet Romtilgang Punkt-til-punkt-kryptering @@ -891,10 +758,7 @@ %1$s i %2$s %1$s: %2$s %1$s: %2$s %3$s - Aktive moduler - - Last inn modulen på nytt Bruk kameraet Bruk mikrofonen @@ -903,25 +767,20 @@ Vennligst skriv inn passordet ditt. Samtalen fortsetter her Kontakt administrator - Beklager, det oppstod en feil - Opprett en passordfrase Bekreft passordfrasen Skriv inn passordfrase Passordfrasen samsvarer ikke Vennligst skriv inn en passordfrase Passordfrasen er for svak - (Avansert) Velg passordfrase Suksess! Gjenopprettingsnøkkel Skriv inn gjenopprettingsnøkkelen - Gjenopprett fra sikkerhetskopi Slett sikkerhetskopien - Slett sikkerhetskopien Begynn verifisering Du bruker ikke noen identitetstjenere @@ -931,35 +790,24 @@ Vis fjernede meldinger Vis en stattholder for fjernede meldinger Alle samfunn - Rom-arkiv Matrix SDK-versjon Hurtigreaksjoner - Send inn et forslag Krypterer filen … Navn eller ID (#example:matrix.org) - Filtrer etter brukernavn eller ID … - Vis redigeringshistorikk - MEDIA FILER %1$s, kl. %2$s Det er upassende IGNORER BRUKER - Ignorer bruker - Spoiler Du ignorerer ikke noen brukere - Langklikk på et rom for å se flere innstillinger - - Tilpassede og avanserte innstillinger - Koble til %1$s Adresse Sjekk innboksen din @@ -972,19 +820,14 @@ Du er logget av Du er logget av Tøm alle data - Se alle mine økter Skru på kryptering De samsvarer ikke Klistremerke - Skru på kryptering - Oppdater - Din gjenopprettingsnøkkel Avslutt - Kryptering er skrudd på Kryptering er ikke skrudd på Medlinger som inneholder @room @@ -993,9 +836,7 @@ Når rom blir oppgradert Feilsøk %1$s (%2$s) - Velg et nytt kontopassord … - Ukryptert Legg til medlemmer Inviterer brukere … @@ -1009,14 +850,12 @@ Opphev demping av mikrofonen Stopp kameraet Start kameraet - Sett opp Rommets navn Emne Kan ikke dekryptere SKJØNNER LÆR MER - Telefonkatalogen din er tom Telefonkatalog Søk i mine kontakter @@ -1025,4 +864,53 @@ Skriv inn PIN-koden din Glemt PIN-koden\? Skru på PIN-kode - + Sjekk e-postadressen din for å fortsette registreringen + Dette telefonnummeret er allerede definert. + Denne e-postadressen er allerede definert. + Passordet er for kort (min 6) + Brukernavn kan bare inneholde bokstaver, tall, prikker, bindestrek og understreker + Feil brukernavn og/eller passord + Angi en e-post for kontogjenoppretting. Bruk senere e-post eller telefon for å være mulig å oppdage av folk som kjenner deg. + Angi en e-post for kontogjenoppretting. Bruk senere e-post eller telefon for å være mulig å oppdage av folk som kjenner deg. + Sett en telefon, og senere for å bli valgbar for personer som kjenner deg. + Angi en e-post for gjenoppretting av kontoen, og senere for å være mulig å oppdage for folk som kjenner deg. + Send historikk om forespørsel om nøkkelandel + Ingen flere resultater + Legg på + Avslå + Godta + Du har ikke tillatelse til å starte en samtale + Du har ikke tillatelse til å starte en samtale i dette rommet + Du har ikke tillatelse til å starte en konferansesamtale + Tilbakestill + Ugyldig brukernavn/passord + Denne enheten bruker en utdatert TLS sikkerhetsprotokoll som er sårbar for angrep, for din egen sikkerhet får du ikke kople til serveren + Klarte ikke registrere bruker + Klarte ikke registrere bruker: Nettverksfeil + Klarte ikke logge inn + Klarte ikke logge inn: Nettverksfeil + Vennligst les og aksepter reglene for denne hjemmetjeneren: + Passordet ditt har blitt tilbakestilt. +\n +\nAlle øktene dine har blitt logget ut og får ikke lenger push-notifikasjoner. For å skru på notifikasjoner igjen, logg inn på enhetene på nytt. + Klarte ikke verifisere e-postadressen: Pass på at du har klikket på lenken i e-posten + En e-post har blitt sendt til %s. Etter du har fulgt lenken i den kan du klikke nedenfor. + Du må skrive et nytt passord. + Du må skrive inn e-postadressen som er knyttet til din konto. + For å tilbakestille passordet, skriv inn e-postadressen som er knyttet til kontoen din: + Denne hjemmetjeneren vil vite om du er en robot + Registrering med e-post og telefonummer samtidig fungerer ikke enda. Bare telefonummeret kommer til å bli registrert. +\n +\nDu kan legge e-postadressen din til i instillinger. + Bruk egendefinerte tjenerinstillinger (avansert) + Klarte ikke å starte en sanntidskopling. +\nVennligst be hjemmetjeneradministratoren din om å sette opp en TURN server så samtaler blir mer stabile. + Vennligst be administratoren for hjemmetjeneren din (%1$s) til å sette opp en TURN tjener for at telefonsamtaler skal fungere ordentlig. +\n +\nAlternativt kan du prøve å bruke den offentlige tjeneren på %2$s, men dette vil ikke være like stabilt, og det vil dele IP-adressen din med den serveren. Du kan styre dette i instillinger. + e-postlenken har ikke blitt trykket på enda + Dette brukernavnet er allerede brukt + Inneholdt ikke gyldig JSON + Ugyldig JSON + Biletten din ble ikke gjenkjent + \ 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 55e904bc2e..c040882e80 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -361,7 +361,7 @@ Digite o ide ou o apelido de uma sala Pesquisar na lista pública - Buscando no diretório… + Buscando na lista… Favoritar Despriorizar @@ -516,7 +516,7 @@ Ativar criptografia· \n(atenção: não é possível desativar depois!) - Diretório + Lista %s tentou carregar um trecho específico da conversa desta sala, mas não conseguiu. @@ -573,15 +573,15 @@ Escolha uma lista pública de salas O servidor pode estar indisponível ou sobrecarregado - Entre com um servidor principal (homeserver) a partir do qual serão listadas as salas públicas + Digite um servidor local, a partir do qual serão listadas as salas públicas Endereço do servidor principal Todas as salas com o servidor %s Todas as salas nativas em %s Pesquisar no histórico - Desconectado + Offline Lista de usuários - LISTA DE USUÁRIAS(OS) (%s) + LISTA DE USUÁRIOS (%s) Iniciar com o sistema Esvaziar o cache de mídia Manter mídia @@ -1407,7 +1407,7 @@ Público Qualquer pessoa poderá entrar nesta sala Lista de Salas - Publicar esta sala na lista das salas + Publicar esta sala na lista de salas Ocorreu um erro ao receber informações de confiança Ocorreu um erro ao obter dados de backup de chaves Importar as chaves de arquivo \"%1$s\". @@ -1452,7 +1452,7 @@ Não consegue encontrar o que você está procurando\? Criar uma sala nova Enviar nova mensagem - Veja lista das salas + Veja lista de salas Nome ou ID (#example:matrix.org) Ativar o recurso de deslizar para responder nas conversas Adicione uma aba dedicada para notificações não lidas na tela principal. @@ -2250,4 +2250,8 @@ Descrição Descrição da sala (opcional) Nome da sala + Enviar histórico de solicitações do compartilhamento de chaves + Não há mais resultados + Exportar auditoria + Enviar mensagem \ No newline at end of file diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 81d64d9669..ce76f7c387 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -2177,4 +2177,8 @@ Subjekt Subjekt dhome (në daçi) Emër dhome + Eksporto Auditim + Mesazh i drejtpërdrejtë + Dërgo historik kërkesash për dhënie kyçesh + S’ka më përfundime \ 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 97075ac975..4f71783b20 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -2187,4 +2187,8 @@ Rumsinställningar Rumsämne (valfritt) Rumsnamn + Exportera granskning + Direktmeddelande + Skicka historik över nyckeldelningsförfrågningar + Inga fler resultat \ 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 22ebb6c885..fdc69678fd 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -1002,4 +1002,20 @@ Резервне копіювання ключів… Якщо вийти зараз, ви втратите свої зашифровані повідомлення Перевірка сеансу + Стікер + Галерея + Файл + Додати зображення з + Контакт + Камера + Аудіо + Створити нову кімнату + Кімнати + Пошук в зашифрованих кімнатах не підтримується на даний момент. + Запитувати підтвердження перед початком дзвінку + Запобігати випадковим дзвінкам + Мені не потрібні мої зашифровані повідомлення + Скасувати зміни + SSL помилка. + Ви не можете здійснити дзвінок із самим собою, дочекайтесь, доки інші учасники приймуть ваше запрошення \ No newline at end of file diff --git a/vector/src/main/res/values-v23/theme_status.xml b/vector/src/main/res/values-v23/theme_status.xml deleted file mode 100644 index 236864d4b8..0000000000 --- a/vector/src/main/res/values-v23/theme_status.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/vector/src/main/res/values/styles.xml b/vector/src/main/res/values/styles.xml index 09f17a77b4..8cc1fe70d8 100644 --- a/vector/src/main/res/values/styles.xml +++ b/vector/src/main/res/values/styles.xml @@ -3,4 +3,8 @@ + \ No newline at end of file diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 68fda72cc6..2dff0ab39c 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -280,14 +280,6 @@ @color/riotx_links - - - -