Merge branch 'release/1.0.1'

This commit is contained in:
Valere 2020-07-28 18:03:37 +02:00
commit cc4298209b
163 changed files with 1514 additions and 889 deletions

View File

@ -28,8 +28,8 @@ Even if we try to be able to work on all the functionalities, we have more knowl
# Other contributors # Other contributors
First of all, we thank all contributors who use RiotX and report problems on this GitHub project or via the integrated rageshake function. First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.
We do not forget all translators, for their work of translating RiotX into many languages. They are also the authors of RiotX. We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
Feel free to add your name below, when you contribute to the project! Feel free to add your name below, when you contribute to the project!

View File

@ -1,3 +1,29 @@
Changes in Element 1.0.1 (2020-07-28)
===================================================
Improvements 🙌:
- Sending events is now retried only 3 times, so we avoid blocking the sending queue too long.
- Display warning when fail to send events in room list
- Improve UI of edit role action in member profile
- Moderation | New screen to display list of banned users in room settings, with unban action
Bugfix 🐛:
- Fix theme issue on Room directory screen (#1613)
- Fix notification not dismissing when entering a room
- Fix uploads don't work with Room v6 (#1558)
- Fix Requesting avatar thumbnails in Element uses wrong http "user-agent" string (#1725)
- Fix 404 on EMS (#1761)
- Fix Infinite loop at startup when migrating account from Riot (#1699)
- Fix Element crashes in loop after initial sync (#1709)
- Remove inner mx-reply tags before replying
- Fix timeline items not loading when there are only filtered events
- Fix "Voice & Video" grayed out in Settings (#1733)
- Fix Allow VOIP call in all rooms with 2 participants (even if not DM)
- Migration from old client does not enable notifications (#1723)
Other changes:
- i18n deactivated account error
Changes in Element 1.0.0 (2020-07-15) Changes in Element 1.0.0 (2020-07-15)
=================================================== ===================================================

View File

@ -2,9 +2,7 @@
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md
Android support can be found in this [![Riot Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riot-android:matrix.org.svg?label=%23riot-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riot-android:matrix.org) room. Android support can be found in this [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) room.
Dedicated room for RiotX: [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
# Specific rules for Matrix Android projects # Specific rules for Matrix Android projects
@ -37,15 +35,13 @@ Note that if the templates are modified, the only things to do is to restart And
## Compilation ## Compilation
For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action. For now, the Matrix SDK and the Element application are in the same project. So there is no specific thing to do, this project should compile without any special action.
## I want to help translating RiotX ## I want to help translating Element
If you want to fix an issue with an English string, please submit a PR. If you want to fix an issue with an English string, please submit a PR.
If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.riot.im/projects/riot-android/). If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.riot.im/projects/riot-android/).
For the moment, Strings from Riot will be used, there is no dedicated project in Weblate for RiotX.
## I want to submit a PR to fix an issue ## I want to submit a PR to fix an issue
Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it. Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it.
@ -101,7 +97,7 @@ Make sure the following commands execute without any error:
### Tests ### Tests
RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices. Element is currently supported on Android Lollipop (API 21+): please test your change on an Android device (or Android emulator) running with API 21. Many issues can happen (including crashes) on older devices.
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient. Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment. You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
@ -120,7 +116,7 @@ Please consider accessibility as an important point. As a minimum requirement, i
When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language. When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language.
You can check this in the layout editor preview by selecting any RTL language (ex: Arabic). You can check this in the layout editor preview by selecting any RTL language (ex: Arabic).
Also please check that the colors are ok for all the current themes of RiotX. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.). Also please check that the colors are ok for all the current themes of Element. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.).
### Authors ### Authors

View File

@ -1,38 +1,32 @@
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
[![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget) [![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget)
[![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org) [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx) [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
# RiotX Android # Element Android
RiotX is an Android Matrix Client currently in beta but in active development. Element Android is an Android Matrix Client provided by [Element](https://element.io/).
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented. It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience.
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx) [<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx) [<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
# New Android SDK # New Android SDK
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough. Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough.
# Roadmap # Roadmap
The current target is to release an application out of beta with the same level of features (and even more) as Riot. The version 1.0.0 of Element still misses some features which was previously included in Riot-Android.
The roadmap has 3 phases: The team will work to add them on a regular basis.
- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta
## Contributing ## Contributing
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org). Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).

View File

@ -14,7 +14,7 @@ Difference though (list not exhaustive):
- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest) - Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest)
- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true) - Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true)
- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client. - The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client.
- The SDK supports incremental sendAttempt (this is not used by RiotX) - The SDK supports incremental sendAttempt (this is not used by Element)
- The "Continue" button is now under the information, and not as the same place that the checkbox - The "Continue" button is now under the information, and not as the same place that the checkbox
- The app can cancel a binding. Current data are erased from DB. - The app can cancel a binding. Current data are erased from DB.
- The API (IdentityService) is improved. - The API (IdentityService) is improved.
@ -22,7 +22,7 @@ Difference though (list not exhaustive):
Missing features (list not exhaustive): Missing features (list not exhaustive):
- Invite by 3Pid (will be in a dedicated PR) - Invite by 3Pid (will be in a dedicated PR)
- Add email or phone to account (not P1, can be done on Riot-Web) - Add email or phone to account (not P1, can be done on Element-Web)
- List email and phone of the account (could be done in a dedicated PR) - List email and phone of the account (could be done in a dedicated PR)
- Search contact (not P1) - Search contact (not P1)
- Logout from identity server when user sign out or deactivate his account. - Logout from identity server when user sign out or deactivate his account.
@ -55,7 +55,7 @@ The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improveme
- Default identity server URL, from Wellknown data is proposed to the user. - Default identity server URL, from Wellknown data is proposed to the user.
- Identity server can be set - Identity server can be set
- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) RiotX should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever) - Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) Element should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever)
- Registration to the identity server is managed with an openId token - Registration to the identity server is managed with an openId token
- Terms of service can be accepted when configuring the identity server. - Terms of service can be accepted when configuring the identity server.
- Terms of service can be accepted after, if they change. - Terms of service can be accepted after, if they change.

View File

@ -1,4 +1,4 @@
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app. This document aims to describe how Element android displays notifications to the end user. It also clarifies notifications and background settings in the app.
# Table of Contents # Table of Contents
1. [Prerequisites Knowledge](#prerequisites-knowledge) 1. [Prerequisites Knowledge](#prerequisites-knowledge)
@ -9,7 +9,7 @@ This document aims to describe how RiotX android displays notifications to the e
* [How does the Home Server knows when to notify a client?](#how-does-the-home-server-knows-when-to-notify-a-client) * [How does the Home Server knows when to notify a client?](#how-does-the-home-server-knows-when-to-notify-a-client)
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation) * [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
* [Background processing limitations](#background-processing-limitations) * [Background processing limitations](#background-processing-limitations)
2. [RiotX Notification implementations](#riotx-notification-implementations) 2. [Element Notification implementations](#element-notification-implementations)
* [Requirements](#requirements) * [Requirements](#requirements)
* [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid) * [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid)
* [Push (FCM) received in background](#push-fcm-received-in-background) * [Push (FCM) received in background](#push-fcm-received-in-background)
@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync. **delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0. When the Element Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
## How does a mobile app receives push notification ## How does a mobile app receives push notification
@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication! In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
This server is called a **Push Gateway** in the matrix world This server is called a **Push Gateway** in the matrix world
That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client. That means that Element Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app. If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
@ -132,7 +132,7 @@ A Home Server can be configured with default rules (for Direct messages, group m
There are different kind of push rules, it can be per room (each new message on this room should be notified), it can also define a pattern that a message should match (when you are mentioned, or key word based). There are different kind of push rules, it can be per room (each new message on this room should be notified), it can also define a pattern that a message should match (when you are mentioned, or key word based).
Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In RiotX these notifications level are reflected as Noisy/Silent. Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In Element these notifications level are reflected as Noisy/Silent.
**What about encrypted messages?** **What about encrypted messages?**
@ -158,7 +158,7 @@ In a nutshell, apps can't do much in background now.
If the devices is not plugged and stays IDLE for a certain amount of time, radio (mobile connectivity) and CPU can/will be turned off. If the devices is not plugged and stays IDLE for a certain amount of time, radio (mobile connectivity) and CPU can/will be turned off.
For an application like RiotX, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time). For an application like Element, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time).
Notice that this is still evolving, and in future versions application that has been 'background restricted' by users won't be able to wake up even when a high priority push is received. Also high priority notifications could be rate limited (not defined anywhere) Notice that this is still evolving, and in future versions application that has been 'background restricted' by users won't be able to wake up even when a high priority push is received. Also high priority notifications could be rate limited (not defined anywhere)
@ -167,41 +167,41 @@ The documentation on this subject is vague, and as per our experiments not alway
It is getting more and more complex to have reliable notifications when FCM is not used. It is getting more and more complex to have reliable notifications when FCM is not used.
# RiotX Notification implementations # Element Notification implementations
## Requirements ## Requirements
RiotX Android must work with and without FCM. Element Android must work with and without FCM.
* The RiotX android app published on F-Droid do not rely on FCM (all related dependencies are not present) * The Element android app published on F-Droid do not rely on FCM (all related dependencies are not present)
* The RiotX android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services) * The Element android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
## Foreground sync mode (Gplay & F-Droid) ## Foreground sync mode (Gplay & F-Droid)
When in foreground, RiotX performs sync continuously with a timeout value set to 10 seconds (see HttpPooling). When in foreground, Element performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
As this mode does not need to live beyond the scope of the application, and as per Google recommendation, RiotX uses the internal app resources (Thread and Timers) to perform the syncs. As this mode does not need to live beyond the scope of the application, and as per Google recommendation, Element uses the internal app resources (Thread and Timers) to perform the syncs.
This mode is turned on when the app enters foreground, and off when enters background. This mode is turned on when the app enters foreground, and off when enters background.
In background, and depending on wether push is available or not, RiotX will use different methods to perform the syncs (Workers / Alarms / Service) In background, and depending on wether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
## Push (FCM) received in background ## Push (FCM) received in background
In order to enable Push, RiotX must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer. In order to enable Push, Element must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer.
When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for RiotX, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org. When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for Element, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org.
This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running riotX. This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running Element.
``` ```
Homeserver ----> Sygnal (configured for RiotX) ----> FCM ----> RiotX Homeserver ----> Sygnal (configured for Element) ----> FCM ----> Element
``` ```
The push gateway is configured to only send `(eventId,roomId)` in the push payload (for better [privacy](#push-vs-privacy-and-mitigation)). The push gateway is configured to only send `(eventId,roomId)` in the push payload (for better [privacy](#push-vs-privacy-and-mitigation)).
RiotX needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification. Element needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification.
As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), RiotX will then use the WorkManager API in order to trigger a background sync. As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), Element will then use the WorkManager API in order to trigger a background sync.
**Google recommendations:** **Google recommendations:**
> We recommend using FCM messages in combination with the WorkManager 1 or JobScheduler API > We recommend using FCM messages in combination with the WorkManager 1 or JobScheduler API
@ -209,7 +209,7 @@ As per [Google recommendation](https://android-developers.googleblog.com/2018/09
> Avoid background services. One common pitfall is using a background service to fetch data in the FCM message handler, since background service will be stopped by the system per recent changes to Google Play Policy > Avoid background services. One common pitfall is using a background service to fetch data in the FCM message handler, since background service will be stopped by the system per recent changes to Google Play Policy
``` ```
Homeserver ----> Sygnal ----> FCM ----> RiotX Homeserver ----> Sygnal ----> FCM ----> Element
(Sync) ----> Homeserver (Sync) ----> Homeserver
<---- <----
Display notification Display notification
@ -217,24 +217,24 @@ Homeserver ----> Sygnal ----> FCM ----> RiotX
**Possible outcomes** **Possible outcomes**
Upon reception of the FCM push, RiotX will perform a sync call to the Home Server, during this process it is possible that: Upon reception of the FCM push, Element will perform a sync call to the Home Server, during this process it is possible that:
* Happy path, the sync is performed, the message resolved and displayed in the notification drawer * Happy path, the sync is performed, the message resolved and displayed in the notification drawer
* The notified message is not in the sync. Can happen if a lot of things did happen since the push (`gappy sync`) * The notified message is not in the sync. Can happen if a lot of things did happen since the push (`gappy sync`)
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally) * The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails. * The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
RiotX implements several strategies in these cases (TODO document) Element implements several strategies in these cases (TODO document)
## FCM Fallback mode ## FCM Fallback mode
It is possible that RiotX is not able to get a FCM push token. It is possible that Element is not able to get a FCM push token.
Common errors (amoung several others) that can cause that: Common errors (amoung several others) that can cause that:
* Google Play Services is outdated * Google Play Services is outdated
* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`) * Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`)
If RiotX is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen. If Element is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen.
Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, RiotX will launch periodic background sync in order to stays in sync with servers. Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, Element will launch periodic background sync in order to stays in sync with servers.
The fallback mode is impacted by all the battery life saving mechanism implemented by android. Meaning that if the app is not used for a certain amount of time (`App-Standby`), or the device stays still and unplugged (`Light Doze`) , the sync will become less frequent. The fallback mode is impacted by all the battery life saving mechanism implemented by android. Meaning that if the app is not used for a certain amount of time (`App-Standby`), or the device stays still and unplugged (`Light Doze`) , the sync will become less frequent.
@ -248,7 +248,7 @@ The fallback mode is supposed to be a temporary state waiting for the user to fi
## F-Droid background Mode ## F-Droid background Mode
The F-Droid RiotX flavor has no dependencies to FCM, therefore cannot relies on Push. The F-Droid Element flavor has no dependencies to FCM, therefore cannot relies on Push.
Also Google's recommended background processing method cannot be applied. This is because all of these methods are affected by IDLE modes, and will result on the user not being notified at all when the app is in a Doze mode (only in maintenance windows that could happens only after hours). Also Google's recommended background processing method cannot be applied. This is because all of these methods are affected by IDLE modes, and will result on the user not being notified at all when the app is in a Doze mode (only in maintenance windows that could happens only after hours).
@ -262,7 +262,7 @@ F-Droid version will schedule alarms that will then trigger a Broadcast Receiver
Depending on the system status (or device make), it is still possible that the app is not given enough time to launch the service, or that the radio is still turned off thus preventing the sync to success (that's why Alarms are not recommended for network related tasks). Depending on the system status (or device make), it is still possible that the app is not given enough time to launch the service, or that the radio is still turned off thus preventing the sync to success (that's why Alarms are not recommended for network related tasks).
That is why on RiotX F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync. That is why on Element F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync.
Note that foreground services require to put a notification informing the user that the app is doing something even if not launched). Note that foreground services require to put a notification informing the user that the app is doing something even if not launched).

View File

@ -132,7 +132,7 @@ It's worth noting that the response from the homeserver contains the userId of A
### Login with Msisdn ### Login with Msisdn
Not supported yet in RiotX Not supported yet in Element
### Login with SSO ### Login with SSO
@ -155,9 +155,9 @@ Not supported yet in RiotX
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx > https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=element%3A%2F%element
The parameter `redirectUrl` is set to `riotx://riotx`. The parameter `redirectUrl` is set to `element://element`.
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs) ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
@ -167,9 +167,9 @@ During the process, user may be asked to validate an email by clicking on a link
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken` Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy > element://element?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login' > curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'

View File

@ -26,7 +26,7 @@ Useful links:
│ │ │ │ │ mx event │ │ │ │ │ │ │ │ │ mx event │ │ │ │
│ │ │ └────────────────────┘ │ │ │ │ │ │ │ └────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
Riot.im │ │ │ │ │ Riot.im Element │ │ │ │ │ Element
┌──│ App │ │ │ │ │ App │ ┌──│ App │ │ │ │ │ App │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
@ -103,7 +103,7 @@ Useful links:
│ │ ┌────┐ │ │ └────────────────────┘ │ │ │ │ │ ┌────┐ │ │ └────────────────────┘ │ │ │
│ │ │ 3 │ │ ┌────────────────────┐ │ │ │ │ │ │ │ 3 │ │ ┌────────────────────┐ │ │ │ │
│ │──────┴────┴───────┼──────────────────┼─▶│ m.call.candidates │ │ │ │ │ │──────┴────┴───────┼──────────────────┼─▶│ m.call.candidates │ │ │ │
Riot.im │ │ │ mx event │ │ │ │ Riot.im Element │ │ │ mx event │ │ │ │ Element
│ App │ │ │ └────────────────────┘ │ │ App │ │ App │ │ │ └────────────────────┘ │ │ App │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
@ -195,9 +195,9 @@ Useful links:
│ │ │ m.call.invite │───┼────────────────────────────┬────┬───▶│ │ │ │ │ m.call.invite │───┼────────────────────────────┬────┬───▶│ │
┌─────────────────┐ │ │ mx event │ │ │ │ 1 │ │ │ ┌─────────────────┐ │ │ mx event │ │ │ │ 1 │ │ │
│ │ │ │ └────────────────────┘ │ └────┘ │ │ │ │ │ │ └────────────────────┘ │ └────┘ │ │
│ │ │ ┌────────────────────┐ │ │ │ Riot.im │ │ │ ┌────────────────────┐ │ │ │ Element
│ │ │ │ │ m.call.candidates │ │ │ App │ │ │ │ │ │ m.call.candidates │ │ │ App │
Riot.im │ │ │ mx event │ │ │ │ │ Element │ │ │ mx event │ │ │ │ │
│ App │ │ │ └────────────────────┘ │ │ │ │ App │ │ │ └────────────────────┘ │ │ │
│ │ │ ┌────────────────────┐◀──┼─────────────────┼───┬────┬───────────┤ │ │ │ │ ┌────────────────────┐◀──┼─────────────────┼───┬────┬───────────┤ │
│ │◀──────────────────┼──────────────────┼──│ m.call.answer │ │ │ 4 │ └──┬──────────────┘ │ │◀──────────────────┼──────────────────┼──│ m.call.answer │ │ │ 4 │ └──┬──────────────┘
@ -275,7 +275,7 @@ Useful links:
│ │ │ │ └────────────────────┘ │ │ │ │ │ │ │ └────────────────────┘ │ │ │
│ │ │ ┌────────────────────┐ │ │ │ │ │ │ │ ┌────────────────────┐ │ │ │ │
│ │ │ │ │ m.call.candidates │ │ │ │ │ │ │ │ │ m.call.candidates │ │ │ │
Riot.im │ │ │ mx event │ │ │ │ Riot.im Element │ │ │ mx event │ │ │ │ Element
│ App │ │ │ └────────────────────┘ │ ┌────┐ │ App │ │ App │ │ │ └────────────────────┘ │ ┌────┐ │ App │
│ │ │ ┌────────────────────┐ │ │ │ 3 │ │ │ │ │ │ ┌────────────────────┐ │ │ │ 3 │ │ │
│ │◀──────────────────┼┐ │ │ m.call.answer │ │ ┌───────┴────┴────────│ │ │ │◀──────────────────┼┐ │ │ m.call.answer │ │ ┌───────┴────┴────────│ │
@ -370,7 +370,7 @@ Useful links:
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
Riot.im │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Riot.im Element │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Element
│ App │ │ App │ │ App │ │ App │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │

View File

@ -42,8 +42,7 @@ import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.Function3 import io.reactivex.functions.Function3
@ -179,7 +178,7 @@ class RxSession(private val session: Session) {
} }
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> { fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
return Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>( return Observable.combineLatest<List<UserAccountDataEvent>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)), liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
liveCrossSigningInfo(session.myUserId), liveCrossSigningInfo(session.myUserId),
liveCrossSigningPrivateKeys(), liveCrossSigningPrivateKeys(),

View File

@ -116,6 +116,7 @@ dependencies {
def markwon_version = '3.1.0' def markwon_version = '3.1.0'
def daggerVersion = '2.25.4' def daggerVersion = '2.25.4'
def work_version = '2.3.3' def work_version = '2.3.3'
def retrofit_version = '2.6.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
@ -128,8 +129,9 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// Network // Network
implementation 'com.squareup.retrofit2:retrofit:2.6.2' implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"

View File

@ -35,7 +35,7 @@ import im.vector.matrix.android.common.TestMatrixCallback
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@ -21,7 +21,6 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
interface AccountDataService { interface AccountDataService {
/** /**

View File

@ -14,14 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.internal.session.sync.model.accountdata package im.vector.matrix.android.api.session.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidgetsContent import im.vector.matrix.android.api.session.events.model.Content
/**
* This is a simplified Event with just a type and a content.
* Currently used types are defined in [UserAccountDataTypes].
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class UserAccountDataAllowedWidgets( data class UserAccountDataEvent(
@Json(name = "type") override val type: String = TYPE_ALLOWED_WIDGETS, @Json(name = "type") val type: String,
@Json(name = "content") val content: AllowedWidgetsContent @Json(name = "content") val content: Content
) : UserAccountData() )

View File

@ -0,0 +1,31 @@
/*
* 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.matrix.android.api.session.accountdata
object UserAccountDataTypes {
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
const val TYPE_DIRECT_MESSAGES = "m.direct"
const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms"
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules"
const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning"
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
const val TYPE_IDENTITY_SERVER = "m.identity_server"
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
}

View File

@ -21,9 +21,11 @@ import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
@ -240,6 +242,18 @@ fun Event.isFileMessage(): Boolean {
return getClearType() == EventType.MESSAGE return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) { && when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_FILE -> true MessageType.MSGTYPE_FILE -> true
else -> false else -> false
} }
} }
fun Event.getRelationContent(): RelationDefaultContent? {
return if (isEncrypted()) {
content.toModel<EncryptedEventContent>()?.relatesTo
} else {
content.toModel<MessageContent>()?.relatesTo
}
}
fun Event.isReply(): Boolean {
return getRelationContent()?.inReplyTo?.eventId != null
}

View File

@ -53,7 +53,8 @@ data class RoomSummary constructor(
val typingUsers: List<SenderInfo>, val typingUsers: List<SenderInfo>,
val inviterId: String? = null, val inviterId: String? = null,
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
val hasFailedSending: Boolean = false
) { ) {
val isVersioned: Boolean val isVersioned: Boolean
@ -66,7 +67,7 @@ data class RoomSummary constructor(
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
val canStartCall: Boolean val canStartCall: Boolean
get() = isDirect && joinedMembersCount == 2 get() = joinedMembersCount == 2
companion object { companion object {
const val NOT_IN_BREADCRUMBS = -1 const val NOT_IN_BREADCRUMBS = -1

View File

@ -25,7 +25,3 @@ interface MessageContent {
val relatesTo: RelationDefaultContent? val relatesTo: RelationDefaultContent?
val newContent: Content? val newContent: Content?
} }
fun MessageContent?.isReply(): Boolean {
return this?.relatesTo?.inReplyTo?.eventId != null
}

View File

@ -20,15 +20,16 @@ import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.getRelationContent
import im.vector.matrix.android.api.session.events.model.isReply
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
/** /**
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
@ -88,11 +89,18 @@ data class TimelineEvent(
*/ */
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
/**
* Get the relation content if any
*/
fun TimelineEvent.getRelationContent(): RelationDefaultContent? {
return root.getRelationContent()
}
/** /**
* Get the eventId which was edited by this event if any * Get the eventId which was edited by this event if any
*/ */
fun TimelineEvent.getEditedEventId(): String? { fun TimelineEvent.getEditedEventId(): String? {
return root.getClearContent().toModel<MessageContent>()?.relatesTo?.takeIf { it.type == RelationType.REPLACE }?.eventId return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId
} }
/** /**
@ -121,11 +129,16 @@ fun TimelineEvent.getLastMessageBody(): String? {
return null return null
} }
/**
* Returns true if it's a reply
*/
fun TimelineEvent.isReply(): Boolean {
return root.isReply()
}
fun TimelineEvent.getTextEditableContent(): String? { fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
val lastContent = getLastMessageContent() val lastContent = getLastMessageContent()
return if (isReply) { return if (isReply()) {
return extractUsefulTextFromReply(lastContent?.body ?: "") return extractUsefulTextFromReply(lastContent?.body ?: "")
} else { } else {
lastContent?.body ?: "" lastContent?.body ?: ""

View File

@ -19,7 +19,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater import im.vector.matrix.android.internal.session.room.send.LocalEchoRepository
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import javax.inject.Inject import javax.inject.Inject
@ -35,7 +35,7 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
internal class DefaultEncryptEventTask @Inject constructor( internal class DefaultEncryptEventTask @Inject constructor(
// private val crypto: CryptoService // private val crypto: CryptoService
private val localEchoUpdater: LocalEchoUpdater private val localEchoRepository: LocalEchoRepository
) : EncryptEventTask { ) : EncryptEventTask {
override suspend fun execute(params: EncryptEventTask.Params): Event { override suspend fun execute(params: EncryptEventTask.Params): Event {
if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
@ -44,7 +44,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
throw IllegalArgumentException() throw IllegalArgumentException()
} }
localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING) localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
params.keepKeys?.forEach { params.keepKeys?.forEach {

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater import im.vector.matrix.android.internal.session.room.send.LocalEchoRepository
import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -34,7 +34,7 @@ internal interface SendEventTask : Task<SendEventTask.Params, String> {
} }
internal class DefaultSendEventTask @Inject constructor( internal class DefaultSendEventTask @Inject constructor(
private val localEchoUpdater: LocalEchoUpdater, private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask, private val encryptEventTask: DefaultEncryptEventTask,
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val eventBus: EventBus) : SendEventTask { private val eventBus: EventBus) : SendEventTask {
@ -44,7 +44,7 @@ internal class DefaultSendEventTask @Inject constructor(
val localId = event.eventId!! val localId = event.eventId!!
try { try {
localEchoUpdater.updateSendState(localId, SendState.SENDING) localEchoRepository.updateSendState(localId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) { val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localId, localId,
@ -53,10 +53,10 @@ internal class DefaultSendEventTask @Inject constructor(
eventType = event.type eventType = event.type
) )
} }
localEchoUpdater.updateSendState(localId, SendState.SENT) localEchoRepository.updateSendState(localId, SendState.SENT)
return executeRequest.eventId return executeRequest.eventId
} catch (e: Throwable) { } catch (e: Throwable) {
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED) localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
throw e throw e
} }
} }

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater import im.vector.matrix.android.internal.session.room.send.LocalEchoRepository
import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -34,7 +34,7 @@ internal interface SendVerificationMessageTask : Task<SendVerificationMessageTas
} }
internal class DefaultSendVerificationMessageTask @Inject constructor( internal class DefaultSendVerificationMessageTask @Inject constructor(
private val localEchoUpdater: LocalEchoUpdater, private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask, private val encryptEventTask: DefaultEncryptEventTask,
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val eventBus: EventBus) : SendVerificationMessageTask { private val eventBus: EventBus) : SendVerificationMessageTask {
@ -44,7 +44,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
val localId = event.eventId!! val localId = event.eventId!!
try { try {
localEchoUpdater.updateSendState(localId, SendState.SENDING) localEchoRepository.updateSendState(localId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) { val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localId, localId,
@ -53,10 +53,10 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
eventType = event.type eventType = event.type
) )
} }
localEchoUpdater.updateSendState(localId, SendState.SENT) localEchoRepository.updateSendState(localId, SendState.SENT)
return executeRequest.eventId return executeRequest.eventId
} catch (e: Throwable) { } catch (e: Throwable) {
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED) localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
throw e throw e
} }
} }

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResu
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventInsertEntity import im.vector.matrix.android.internal.database.model.EventInsertEntity
import im.vector.matrix.android.internal.database.model.EventInsertEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
@ -46,17 +47,25 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
if (!results.isLoaded || results.isEmpty()) { if (!results.isLoaded || results.isEmpty()) {
return return
} }
val idsToDeleteAfterProcess = ArrayList<String>()
val filteredEvents = ArrayList<EventInsertEntity>(results.size)
Timber.v("EventInsertEntity updated with ${results.size} results in db") Timber.v("EventInsertEntity updated with ${results.size} results in db")
val filteredEvents = results.mapNotNull { results.forEach {
if (shouldProcess(it)) { if (shouldProcess(it)) {
results.realm.copyFromRealm(it) // don't use copy from realm over there
} else { val copiedEvent = EventInsertEntity(
null eventId = it.eventId,
eventType = it.eventType
).apply {
insertType = it.insertType
}
filteredEvents.add(copiedEvent)
} }
idsToDeleteAfterProcess.add(it.eventId)
} }
Timber.v("There are ${filteredEvents.size} events to process")
observerScope.launch { observerScope.launch {
awaitTransaction(realmConfiguration) { realm -> awaitTransaction(realmConfiguration) { realm ->
Timber.v("##Transaction: There are ${filteredEvents.size} events to process ")
filteredEvents.forEach { eventInsert -> filteredEvents.forEach { eventInsert ->
val eventId = eventInsert.eventId val eventId = eventInsert.eventId
val event = EventEntity.where(realm, eventId).findFirst() val event = EventEntity.where(realm, eventId).findFirst()
@ -72,7 +81,10 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
it.process(realm, domainEvent) it.process(realm, domainEvent)
} }
} }
realm.delete(EventInsertEntity::class.java) realm.where(EventInsertEntity::class.java)
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
.findAll()
.deleteAllFromRealm()
} }
} }
} }

View File

@ -26,6 +26,7 @@ import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -39,7 +40,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
} }
protected val observerScope = CoroutineScope(SupervisorJob()) protected val observerScope = CoroutineScope(SupervisorJob() + BACKGROUND_HANDLER.asCoroutineDispatcher())
protected abstract val query: Monarchy.Query<T> protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>() private val backgroundRealm = AtomicReference<Realm>()

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.internal.database.model.UserAccountDataEntity import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import javax.inject.Inject import javax.inject.Inject
internal class AccountDataMapper @Inject constructor(moshi: Moshi) { internal class AccountDataMapper @Inject constructor(moshi: Moshi) {

View File

@ -62,7 +62,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
encryptionEventTs = roomSummaryEntity.encryptionEventTs, encryptionEventTs = roomSummaryEntity.encryptionEventTs,
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId inviterId = roomSummaryEntity.inviterId,
hasFailedSending = roomSummaryEntity.hasFailedSending
) )
} }
} }

View File

@ -51,7 +51,8 @@ internal open class RoomSummaryEntity(
var isEncrypted: Boolean = false, var isEncrypted: Boolean = false,
var encryptionEventTs: Long? = 0, var encryptionEventTs: Long? = 0,
var roomEncryptionTrustLevelStr: String? = null, var roomEncryptionTrustLevelStr: String? = null,
var inviterId: String? = null var inviterId: String? = null,
var hasFailedSending: Boolean = false
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View File

@ -93,9 +93,12 @@ internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Re
roomId: String, roomId: String,
sendStates: List<SendState>) sendStates: List<SendState>)
: RealmResults<TimelineEventEntity> { : RealmResults<TimelineEventEntity> {
val sendStatesStr = sendStates.map { it.name }.toTypedArray() return whereRoomId(realm, roomId)
return realm.where<TimelineEventEntity>() .filterSendStates(sendStates)
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.`in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR, sendStatesStr)
.findAll() .findAll()
} }
internal fun RealmQuery<TimelineEventEntity>.filterSendStates(sendStates: List<SendState>): RealmQuery<TimelineEventEntity> {
val sendStatesStr = sendStates.map { it.name }.toTypedArray()
return `in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR, sendStatesStr)
}

View File

@ -34,24 +34,12 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVideoConte
import im.vector.matrix.android.internal.network.parsing.ForceToBooleanJsonAdapter import im.vector.matrix.android.internal.network.parsing.ForceToBooleanJsonAdapter
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
object MoshiProvider { object MoshiProvider {
private val moshi: Moshi = Moshi.Builder() private val moshi: Moshi = Moshi.Builder()
.add(UriMoshiAdapter()) .add(UriMoshiAdapter())
.add(ForceToBooleanJsonAdapter()) .add(ForceToBooleanJsonAdapter())
.add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataEvent::class.java)
.registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
.registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
.registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES)
.registerSubtype(UserAccountDataBreadcrumbs::class.java, UserAccountData.TYPE_BREADCRUMBS)
)
.add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java)
.registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT)
.registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE)

View File

@ -24,6 +24,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import javax.inject.Inject import javax.inject.Inject
internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
@ -48,6 +49,7 @@ internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
return okHttpClient.get().newCall(request) return okHttpClient.get().newCall(request)
} }
}) })
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(UnitConverterFactory) .addConverterFactory(UnitConverterFactory)
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(MoshiConverterFactory.create(moshi))
.build() .build()

View File

@ -29,7 +29,7 @@ internal class UserAgentInterceptor @Inject constructor(private val userAgentHol
userAgentHolder.userAgent userAgentHolder.userAgent
.takeIf { it.isNotBlank() } .takeIf { it.isNotBlank() }
?.let { ?.let {
newRequestBuilder.addHeader(HttpHeaders.UserAgent, it) newRequestBuilder.header(HttpHeaders.UserAgent, it)
} }
request = newRequestBuilder.build() request = newRequestBuilder.build()
return chain.proceed(request) return chain.proceed(request)

View File

@ -46,7 +46,7 @@ import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.profile.BindThreePidsTask import im.vector.matrix.android.internal.session.profile.BindThreePidsTask
import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask
import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@ -95,7 +95,7 @@ internal class DefaultIdentityService @Inject constructor(
lifecycleRegistry.currentState = Lifecycle.State.STARTED lifecycleRegistry.currentState = Lifecycle.State.STARTED
// Observe the account data change // Observe the account data change
accountDataDataSource accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY_SERVER) .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_IDENTITY_SERVER)
.observeNotNull(lifecycleOwner) { .observeNotNull(lifecycleOwner) {
notifyIdentityServerUrlChange(it.getOrNull()?.content?.toModel<IdentityServerContent>()?.baseUrl) notifyIdentityServerUrlChange(it.getOrNull()?.content?.toModel<IdentityServerContent>()?.baseUrl)
} }

View File

@ -34,8 +34,8 @@ import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.extensions.observeNotNull import im.vector.matrix.android.internal.extensions.observeNotNull
import im.vector.matrix.android.internal.session.SessionLifecycleObserver import im.vector.matrix.android.internal.session.SessionLifecycleObserver
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory
@ -87,7 +87,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
lifecycleRegistry.currentState = Lifecycle.State.STARTED lifecycleRegistry.currentState = Lifecycle.State.STARTED
observeWellknownConfig() observeWellknownConfig()
accountDataDataSource accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS)
.observeNotNull(lifecycleOwner) { .observeNotNull(lifecycleOwner) {
val allowedWidgetsContent = it.getOrNull()?.content?.toModel<AllowedWidgetsContent>() val allowedWidgetsContent = it.getOrNull()?.content?.toModel<AllowedWidgetsContent>()
if (allowedWidgetsContent != null) { if (allowedWidgetsContent != null) {
@ -95,7 +95,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
} }
} }
accountDataDataSource accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING)
.observeNotNull(lifecycleOwner) { .observeNotNull(lifecycleOwner) {
val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>() val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>()
if (integrationProvisioningContent != null) { if (integrationProvisioningContent != null) {
@ -103,7 +103,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
} }
} }
accountDataDataSource accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS) .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS)
.observeNotNull(lifecycleOwner) { .observeNotNull(lifecycleOwner) {
val integrationManagerContent = it.getOrNull()?.asIntegrationManagerWidgetContent() val integrationManagerContent = it.getOrNull()?.asIntegrationManagerWidgetContent()
val config = integrationManagerContent?.extractIntegrationManagerConfig() val config = integrationManagerContent?.extractIntegrationManagerConfig()
@ -132,7 +132,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
* Returns false if the user as disabled integration manager feature * Returns false if the user as disabled integration manager feature
*/ */
fun isIntegrationEnabled(): Boolean { fun isIntegrationEnabled(): Boolean {
val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING)
val integrationProvisioningContent = integrationProvisioningData?.content?.toModel<IntegrationProvisioningContent>() val integrationProvisioningContent = integrationProvisioningData?.content?.toModel<IntegrationProvisioningContent>()
return integrationProvisioningContent?.enabled ?: false return integrationProvisioningContent?.enabled ?: false
} }
@ -153,7 +153,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
} }
fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
val newContent = if (currentContent == null) { val newContent = if (currentContent == null) {
val allowedWidget = mapOf(stateEventId to allowed) val allowedWidget = mapOf(stateEventId to allowed)
@ -173,13 +173,13 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
} }
fun isWidgetAllowed(stateEventId: String): Boolean { fun isWidgetAllowed(stateEventId: String): Boolean {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
return currentContent?.widgets?.get(stateEventId) ?: false return currentContent?.widgets?.get(stateEventId) ?: false
} }
fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
val newContent = if (currentContent == null) { val newContent = if (currentContent == null) {
val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed)) val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed))
@ -203,7 +203,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
} }
fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean { fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
return currentContent?.native?.get(widgetType)?.get(domain) ?: false return currentContent?.native?.get(widgetType)?.get(domain) ?: false
} }

View File

@ -129,6 +129,21 @@ internal interface RoomAPI {
@Body content: Content? @Body content: Content?
): Call<SendResponse> ): Call<SendResponse>
/**
* Send an event to a room.
*
* @param txId the transaction Id
* @param roomId the room id
* @param eventType the event type
* @param content the event content as string
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}")
fun send(@Path("txId") txId: String,
@Path("roomId") roomId: String,
@Path("eventType") eventType: String,
@Body content: String?
): Call<SendResponse>
/** /**
* Get the context surrounding an event. * Get the context surrounding an event.
* *

View File

@ -128,6 +128,7 @@ internal class DefaultSendService @AssistedInject constructor(
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return sendEvent(localEcho.root) return sendEvent(localEcho.root)
} }
return null return null
@ -282,12 +283,6 @@ internal class DefaultSendService @AssistedInject constructor(
} }
} }
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
return SendEventWorker.Params(sessionId, event)
.let { WorkerParamsFactory.toData(it) }
.let { timelineSendEventWorkCommon.createWork<SendEventWorker>(it, startChain) }
}
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }

View File

@ -52,7 +52,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var crypto: CryptoService @Inject lateinit var crypto: CryptoService
@Inject lateinit var localEchoUpdater: LocalEchoUpdater @Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.v("Start Encrypt work") Timber.v("Start Encrypt work")
@ -74,7 +74,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
if (localEvent.eventId == null) { if (localEvent.eventId == null) {
return Result.success() return Result.success()
} }
localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING) localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
params.keepKeys?.forEach { params.keepKeys?.forEach {
@ -116,7 +116,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
senderCurve25519Key = result.eventContent["sender_key"] as? String, senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = crypto.getMyDevice().fingerprint() claimedEd25519Key = crypto.getMyDevice().fingerprint()
) )
localEchoUpdater.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
} }
val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent)
@ -126,7 +126,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
else -> SendState.UNDELIVERED else -> SendState.UNDELIVERED
} }
localEchoUpdater.updateSendState(localEvent.eventId, sendState) localEchoRepository.updateSendState(localEvent.eventId, sendState)
// always return success, or the chain will be stuck for ever! // always return success, or the chain will be stuck for ever!
val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage
?: "Error") ?: "Error")

View File

@ -30,7 +30,6 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.UnsignedData import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.AudioInfo import im.vector.matrix.android.api.session.room.model.message.AudioInfo
import im.vector.matrix.android.api.session.room.model.message.FileInfo import im.vector.matrix.android.api.session.room.model.message.FileInfo
import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.ImageInfo
@ -50,13 +49,13 @@ import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_POLL
import im.vector.matrix.android.api.session.room.model.message.OptionItem import im.vector.matrix.android.api.session.room.model.message.OptionItem
import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo
import im.vector.matrix.android.api.session.room.model.message.VideoInfo import im.vector.matrix.android.api.session.room.model.message.VideoInfo
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.isReply
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
@ -173,12 +172,13 @@ internal class LocalEchoEventFactory @Inject constructor(
val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) } val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) }
?: "" ?: ""
val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel()) val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
permalink, permalink,
userLink, userLink,
originalEvent.senderInfo.disambiguatedDisplayName, originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(), // Remove inner mx_reply tags if any
body.takeFormatted().replace(MX_REPLY_REGEX, ""),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
) )
// //
@ -367,12 +367,13 @@ internal class LocalEchoEventFactory @Inject constructor(
val userId = eventReplied.root.senderId ?: return null val userId = eventReplied.root.senderId ?: return null
val userLink = PermalinkFactory.createPermalink(userId) ?: return null val userLink = PermalinkFactory.createPermalink(userId) ?: return null
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel()) val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
permalink, permalink,
userLink, userLink,
userId, userId,
body.takeFormatted(), // Remove inner mx_reply tags if any
body.takeFormatted().replace(MX_REPLY_REGEX, ""),
createTextContent(replyText, autoMarkdown).takeFormatted() createTextContent(replyText, autoMarkdown).takeFormatted()
) )
// //
@ -412,10 +413,10 @@ internal class LocalEchoEventFactory @Inject constructor(
/** /**
* Returns a TextContent used for the fallback event representation in a reply message. * Returns a TextContent used for the fallback event representation in a reply message.
* We also pass the original content, because in case of an edit of a reply the last content is not * In case of an edit of a reply the last content is not
* himself a reply, but it will contain the fallbacks, so we have to trim them. * himself a reply, but it will contain the fallbacks, so we have to trim them.
*/ */
private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent { private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent {
when (content?.msgType) { when (content?.msgType) {
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
@ -424,7 +425,6 @@ internal class LocalEchoEventFactory @Inject constructor(
if (content is MessageContentWithFormattedBody) { if (content is MessageContentWithFormattedBody) {
formattedText = content.matrixFormattedBody formattedText = content.matrixFormattedBody
} }
val isReply = content.isReply() || originalContent.isReply()
return if (isReply) { return if (isReply) {
TextContent(content.body, formattedText).removeInReplyFallbacks() TextContent(content.body, formattedText).removeInReplyFallbacks()
} else { } else {
@ -485,5 +485,8 @@ internal class LocalEchoEventFactory @Inject constructor(
// </mx-reply> // </mx-reply>
// No whitespace because currently breaks temporary formatted text to Span // No whitespace because currently breaks temporary formatted text to Span
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s""" const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
// This is used to replace inner mx-reply tags
val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()
} }
} }

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
@ -24,7 +25,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.database.helper.nextId import im.vector.matrix.android.internal.database.helper.nextId
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.mapper.toEntity
@ -36,8 +39,8 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.util.awaitTransaction import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm import io.realm.Realm
@ -79,7 +82,33 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
realm.insert(eventInsertEntity) realm.insert(eventInsertEntity)
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
roomSummaryUpdater.update(realm, roomId) roomSummaryUpdater.updateSendingInformation(realm, roomId)
}
}
fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("Update local state of $eventId to ${sendState.name}")
monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) {
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
// If already synced, do not put as sent
} else {
sendingEventEntity.sendState = sendState
}
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
}
}
}
fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) {
monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) {
sendingEventEntity.type = EventType.ENCRYPTED
sendingEventEntity.content = ContentMapper.map(encryptedContent)
sendingEventEntity.setDecryptionResult(mxEventDecryptionResult)
}
} }
} }
@ -87,16 +116,18 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
roomSummaryUpdater.updateSendingInformation(realm, roomId)
} }
} }
suspend fun clearSendingQueue(roomId: String) { suspend fun clearSendingQueue(roomId: String) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
RoomEntity.where(realm, roomId).findFirst()?.let { room -> TimelineEventEntity
room.sendingTimelineEvents.forEach { .findAllInRoomWithSendStates(realm, roomId, SendState.IS_SENDING_STATES)
it.root?.sendState = SendState.UNDELIVERED .forEach {
} it.root?.sendState = SendState.UNSENT
} }
roomSummaryUpdater.updateSendingInformation(realm, roomId)
} }
} }
@ -106,6 +137,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
timelineEvents.forEach { timelineEvents.forEach {
it.root?.sendState = sendState it.root?.sendState = sendState
} }
roomSummaryUpdater.updateSendingInformation(realm, roomId)
} }
} }

View File

@ -1,57 +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.matrix.android.internal.session.room.send
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import timber.log.Timber
import javax.inject.Inject
internal class LocalEchoUpdater @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("Update local state of $eventId to ${sendState.name}")
monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) {
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
// If already synced, do not put as sent
} else {
sendingEventEntity.sendState = sendState
}
}
}
}
fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) {
monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) {
sendingEventEntity.type = EventType.ENCRYPTED
sendingEventEntity.content = ContentMapper.map(encryptedContent)
sendingEventEntity.setDecryptionResult(mxEventDecryptionResult)
}
}
}
}

View File

@ -54,7 +54,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
@Inject lateinit var workManagerProvider: WorkManagerProvider @Inject lateinit var workManagerProvider: WorkManagerProvider
@Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon
@Inject lateinit var localEchoUpdater: LocalEchoUpdater @Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.v("Start dispatch sending multiple event work") Timber.v("Start dispatch sending multiple event work")
@ -67,7 +67,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
if (params.lastFailureMessage != null) { if (params.lastFailureMessage != null) {
params.events.forEach { event -> params.events.forEach { event ->
event.eventId?.let { localEchoUpdater.updateSendState(it, SendState.UNDELIVERED) } event.eventId?.let { localEchoRepository.updateSendState(it, SendState.UNDELIVERED) }
} }
// Transmit the error if needed? // Transmit the error if needed?
return Result.success(inputData) return Result.success(inputData)

View File

@ -23,6 +23,7 @@ import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.failure.shouldBeRetried import im.vector.matrix.android.api.failure.shouldBeRetried
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.SessionWorkerParams
@ -32,6 +33,8 @@ import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3
/** /**
* Possible previous worker: [EncryptEventWorker] or first worker * Possible previous worker: [EncryptEventWorker] or first worker
* Possible next worker : None * Possible next worker : None
@ -43,11 +46,26 @@ internal class SendEventWorker(context: Context,
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val event: Event, // TODO remove after some time, it's used for compat
val event: Event? = null,
val eventId: String? = null,
val roomId: String? = null,
val type: String? = null,
val contentStr: String? = null,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams {
@Inject lateinit var localEchoUpdater: LocalEchoUpdater constructor(sessionId: String, event: Event, lastFailureMessage: String? = null) : this(
sessionId = sessionId,
eventId = event.eventId,
roomId = event.roomId,
type = event.type,
contentStr = ContentMapper.map(event.content),
lastFailureMessage = lastFailureMessage
)
}
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@ -58,42 +76,39 @@ internal class SendEventWorker(context: Context,
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)
if (params.eventId == null || params.roomId == null || params.type == null) {
val event = params.event // compat with old params, make it fail if any
if (event.eventId == null) { if (params.event?.eventId != null) {
localEchoRepository.updateSendState(params.event.eventId, SendState.UNDELIVERED)
}
return Result.success() return Result.success()
} }
if (params.lastFailureMessage != null) { if (params.lastFailureMessage != null) {
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED) localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
// Transmit the error // Transmit the error
return Result.success(inputData) return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") } .also { Timber.e("Work cancelled due to input error from parent") }
} }
return try { return try {
sendEvent(event) sendEvent(params.eventId, params.roomId, params.type, params.contentStr)
Result.success() Result.success()
} catch (exception: Throwable) { } catch (exception: Throwable) {
if (exception.shouldBeRetried()) { // It does start from 0, we want it to stop if it fails the third time
Result.retry() val currentAttemptCount = runAttemptCount + 1
if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) {
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
return Result.success()
} else { } else {
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED) Result.retry()
// always return success, or the chain will be stuck for ever!
Result.success()
} }
} }
} }
private suspend fun sendEvent(event: Event) { private suspend fun sendEvent(eventId: String, roomId: String, type: String, contentStr: String?) {
localEchoUpdater.updateSendState(event.eventId!!, SendState.SENDING) localEchoRepository.updateSendState(eventId, SendState.SENDING)
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send( apiCall = roomAPI.send(eventId, roomId, type, contentStr)
event.eventId,
event.roomId!!,
event.type,
event.content
)
} }
localEchoUpdater.updateSendState(event.eventId, SendState.SENT) localEchoRepository.updateSendState(eventId, SendState.SENT)
} }
} }

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomNameContent import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.ContentMapper
@ -34,6 +35,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.isEventRead
@ -75,6 +77,7 @@ internal class RoomSummaryUpdater @Inject constructor(
EventType.STATE_ROOM_ENCRYPTION, EventType.STATE_ROOM_ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER,
EventType.REACTION,
EventType.STATE_ROOM_CREATE EventType.STATE_ROOM_CREATE
) )
} }
@ -144,6 +147,7 @@ internal class RoomSummaryUpdater @Inject constructor(
} else if (roomSummaryEntity.membership != Membership.INVITE) { } else if (roomSummaryEntity.membership != Membership.INVITE) {
roomSummaryEntity.inviterId = null roomSummaryEntity.inviterId = null
} }
roomSummaryEntity.updateHasFailedSending()
if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
@ -166,6 +170,17 @@ internal class RoomSummaryUpdater @Inject constructor(
} }
} }
private fun RoomSummaryEntity.updateHasFailedSending() {
hasFailedSending = TimelineEventEntity.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES).isNotEmpty()
}
fun updateSendingInformation(realm: Realm, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
}
fun updateShieldTrust(realm: Realm, fun updateShieldTrust(realm: Realm,
roomId: String, roomId: String,
trust: RoomEncryptionTrustLevel?) { trust: RoomEncryptionTrustLevel?) {

View File

@ -169,7 +169,7 @@ internal class DefaultTimeline(
filteredEvents = nonFilteredEvents.where() filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings() .filterEventsWithSettings()
.findAll() .findAll()
filteredEvents.addChangeListener(eventsChangeListener) nonFilteredEvents.addChangeListener(eventsChangeListener)
handleInitialLoad() handleInitialLoad()
if (settings.shouldHandleHiddenReadReceipts()) { if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
@ -342,21 +342,18 @@ internal class DefaultTimeline(
private fun updateLoadingStates(results: RealmResults<TimelineEventEntity>) { private fun updateLoadingStates(results: RealmResults<TimelineEventEntity>) {
val lastCacheEvent = results.lastOrNull() val lastCacheEvent = results.lastOrNull()
val lastBuiltEvent = builtEvents.lastOrNull()
val firstCacheEvent = results.firstOrNull() val firstCacheEvent = results.firstOrNull()
val firstBuiltEvent = builtEvents.firstOrNull()
val chunkEntity = getLiveChunk() val chunkEntity = getLiveChunk()
updateState(Timeline.Direction.FORWARDS) { updateState(Timeline.Direction.FORWARDS) {
it.copy( it.copy(
hasMoreInCache = firstBuiltEvent != null && firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE, hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId),
hasReachedEnd = chunkEntity?.isLastForward ?: false hasReachedEnd = chunkEntity?.isLastForward ?: false
) )
} }
updateState(Timeline.Direction.BACKWARDS) { updateState(Timeline.Direction.BACKWARDS) {
it.copy( it.copy(
hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.displayIndex ?: Int.MAX_VALUE, hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
) )
} }

View File

@ -116,11 +116,11 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
tokenStore.saveToken(realm, syncResponse.nextBatch) tokenStore.saveToken(realm, syncResponse.nextBatch)
} }
// Everything else we need to do outside the transaction // Everything else we need to do outside the transaction
syncResponse.rooms?.also { syncResponse.rooms?.let {
checkPushRules(it, isInitialSync) checkPushRules(it, isInitialSync)
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
} }
syncResponse.groups?.also { syncResponse.groups?.let {
scheduleGroupDataFetchingIfNeeded(it) scheduleGroupDataFetchingIfNeeded(it)
} }

View File

@ -16,11 +16,14 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import com.squareup.moshi.Moshi
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -37,16 +40,13 @@ import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFie
import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.getDirectRooms
import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs import im.vector.matrix.android.internal.session.sync.model.accountdata.DirectMessagesContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.accountdata.IgnoredUsersContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
@ -60,25 +60,18 @@ internal class UserAccountDataSyncHandler @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
@UserId private val userId: String, @UserId private val userId: String,
private val directChatsHelper: DirectChatsHelper, private val directChatsHelper: DirectChatsHelper,
private val moshi: Moshi,
private val updateUserAccountDataTask: UpdateUserAccountDataTask) { private val updateUserAccountDataTask: UpdateUserAccountDataTask) {
fun handle(realm: Realm, accountData: UserAccountDataSync?) { fun handle(realm: Realm, accountData: UserAccountDataSync?) {
accountData?.list?.forEach { accountData?.list?.forEach { event ->
// Generic handling, just save in base // Generic handling, just save in base
handleGenericAccountData(realm, it.type, it.content) handleGenericAccountData(realm, event.type, event.content)
when (event.type) {
// Didn't want to break too much thing, so i re-serialize to jsonString before reparsing UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event)
// TODO would be better to have a mapper? UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event)
val toJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(it) UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event)
val model = toJson?.let { json -> UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event)
MoshiProvider.providesMoshi().adapter(UserAccountData::class.java).fromJson(json)
}
// Specific parsing
when (model) {
is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, model)
is UserAccountDataPushRules -> handlePushRules(realm, model)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, model)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, model)
} }
} }
} }
@ -116,8 +109,8 @@ internal class UserAccountDataSyncHandler @Inject constructor(
} }
} }
private fun handlePushRules(realm: Realm, userAccountDataPushRules: UserAccountDataPushRules) { private fun handlePushRules(realm: Realm, event: UserAccountDataEvent) {
val pushRules = userAccountDataPushRules.content val pushRules = event.content.toModel<GetPushRulesResponse>() ?: return
realm.where(PushRulesEntity::class.java) realm.where(PushRulesEntity::class.java)
.findAll() .findAll()
.deleteAllFromRealm() .deleteAllFromRealm()
@ -158,13 +151,14 @@ internal class UserAccountDataSyncHandler @Inject constructor(
realm.insertOrUpdate(underrides) realm.insertOrUpdate(underrides)
} }
private fun handleDirectChatRooms(realm: Realm, directMessages: UserAccountDataDirectMessages) { private fun handleDirectChatRooms(realm: Realm, event: UserAccountDataEvent) {
val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
oldDirectRooms.forEach { oldDirectRooms.forEach {
it.isDirect = false it.isDirect = false
it.directUserId = null it.directUserId = null
} }
directMessages.content.forEach { val content = event.content.toModel<DirectMessagesContent>() ?: return
content.forEach {
val userId = it.key val userId = it.key
it.value.forEach { roomId -> it.value.forEach { roomId ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
@ -177,8 +171,8 @@ internal class UserAccountDataSyncHandler @Inject constructor(
} }
} }
private fun handleIgnoredUsers(realm: Realm, userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) { private fun handleIgnoredUsers(realm: Realm, event: UserAccountDataEvent) {
val userIds = userAccountDataIgnoredUsers.content.ignoredUsers.keys val userIds = event.content.toModel<IgnoredUsersContent>()?.ignoredUsers?.keys ?: return
realm.where(IgnoredUserEntity::class.java) realm.where(IgnoredUserEntity::class.java)
.findAll() .findAll()
.deleteAllFromRealm() .deleteAllFromRealm()
@ -187,8 +181,8 @@ internal class UserAccountDataSyncHandler @Inject constructor(
// TODO If not initial sync, we should execute a init sync // TODO If not initial sync, we should execute a init sync
} }
private fun handleBreadcrumbs(realm: Realm, userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) { private fun handleBreadcrumbs(realm: Realm, event: UserAccountDataEvent) {
val recentRoomIds = userAccountDataBreadcrumbs.content.recentRoomIds val recentRoomIds = event.content.toModel<BreadcrumbsContent>()?.recentRoomIds ?: return
val entity = BreadcrumbsEntity.getOrCreate(realm) val entity = BreadcrumbsEntity.getOrCreate(realm)
// And save the new received list // And save the new received list

View File

@ -19,12 +19,6 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataAcceptedTerms(
@Json(name = "type") override val type: String = TYPE_ACCEPTED_TERMS,
@Json(name = "content") val content: AcceptedTermsContent
) : UserAccountData()
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class AcceptedTermsContent( internal data class AcceptedTermsContent(
@Json(name = "accepted") val acceptedTerms: List<String> = emptyList() @Json(name = "accepted") val acceptedTerms: List<String> = emptyList()

View File

@ -19,12 +19,6 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataBreadcrumbs(
@Json(name = "type") override val type: String = TYPE_BREADCRUMBS,
@Json(name = "content") val content: BreadcrumbsContent
) : UserAccountData()
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class BreadcrumbsContent( internal data class BreadcrumbsContent(
@Json(name = "recent_rooms") val recentRoomIds: List<String> = emptyList() @Json(name = "recent_rooms") val recentRoomIds: List<String> = emptyList()

View File

@ -16,12 +16,4 @@
package im.vector.matrix.android.internal.session.sync.model.accountdata package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json typealias DirectMessagesContent = Map<String, List<String>>
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
@JsonClass(generateAdapter = true)
data class UserAccountDataEvent(
@Json(name = "type") override val type: String,
@Json(name = "content") val content: JsonDict
) : UserAccountData()

View File

@ -19,12 +19,6 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataIdentityServer(
@Json(name = "type") override val type: String = TYPE_IDENTITY_SERVER,
@Json(name = "content") val content: IdentityServerContent? = null
) : UserAccountData()
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class IdentityServerContent( internal data class IdentityServerContent(
@Json(name = "base_url") val baseUrl: String? = null @Json(name = "base_url") val baseUrl: String? = null

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.emptyJsonDict import im.vector.matrix.android.api.util.emptyJsonDict
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -26,7 +25,7 @@ internal data class IgnoredUsersContent(
/** /**
* Required. The map of users to ignore. UserId -> empty object for future enhancement * Required. The map of users to ignore. UserId -> empty object for future enhancement
*/ */
@Json(name = "ignored_users") val ignoredUsers: Map<String, JsonDict> @Json(name = "ignored_users") val ignoredUsers: Map<String, Any>
) { ) {
companion object { companion object {

View File

@ -1,38 +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.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataContent
abstract class UserAccountData : AccountDataContent {
@Json(name = "type") abstract val type: String
companion object {
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
const val TYPE_DIRECT_MESSAGES = "m.direct"
const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms"
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules"
const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning"
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
const val TYPE_IDENTITY_SERVER = "m.identity_server"
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
}
}

View File

@ -1,26 +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.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataDirectMessages(
@Json(name = "type") override val type: String = TYPE_DIRECT_MESSAGES,
@Json(name = "content") val content: Map<String, List<String>>
) : UserAccountData()

View File

@ -1,26 +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.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataIgnoredUsers(
@Json(name = "type") override val type: String = TYPE_IGNORED_USER_LIST,
@Json(name = "content") val content: IgnoredUsersContent
) : UserAccountData()

View File

@ -1,27 +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.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse
@JsonClass(generateAdapter = true)
internal data class UserAccountDataPushRules(
@Json(name = "type") override val type: String = TYPE_PUSH_RULES,
@Json(name = "content") val content: GetPushRulesResponse
) : UserAccountData()

View File

@ -18,9 +18,9 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class UserAccountDataSync( internal data class UserAccountDataSync(
@Json(name = "events") val list: List<Event> = emptyList() @Json(name = "events") val list: List<UserAccountDataEvent> = emptyList()
) )

View File

@ -1,50 +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.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event
/*
"m.widgets":{
"stickerpicker_@rxl881:matrix.org_1514573757015":{
"content":{
"creatorUserId":"@rxl881:matrix.org",
"data":{
"..."
},
"id":"stickerpicker_@rxl881:matrix.org_1514573757015",
"name":"Stickerpicker",
"type":"m.stickerpicker",
"url":"https://...",
"waitForIframeLoad":true
},
"sender":"@rxl881:matrix.org"
"state_key":"stickerpicker_@rxl881:matrix.org_1514573757015",
"type":"m.widget"
},
{
"..."
}
}
*/
@JsonClass(generateAdapter = true)
internal data class UserAccountDataWidgets(
@Json(name = "type") override val type: String = TYPE_WIDGETS,
@Json(name = "content") val content: Map<String, Event>
) : UserAccountData()

View File

@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI
import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@ -109,7 +109,7 @@ internal class DefaultTermsService @Inject constructor(
} }
private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set<String> { private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set<String> {
return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS) return accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ACCEPTED_TERMS)
?.content ?.content
?.toModel<AcceptedTermsContent>() ?.toModel<AcceptedTermsContent>()
?.acceptedTerms ?.acceptedTerms

View File

@ -25,7 +25,7 @@ import im.vector.matrix.android.internal.database.mapper.AccountDataMapper
import im.vector.matrix.android.internal.database.model.UserAccountDataEntity import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject

View File

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.sync.UserAccountDataSyncHandler import im.vector.matrix.android.internal.session.sync.UserAccountDataSyncHandler
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject import javax.inject.Inject

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.sync.model.accountdata.IgnoredUsersContent import im.vector.matrix.android.internal.session.sync.model.accountdata.IgnoredUsersContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
@ -64,7 +64,7 @@ internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor(
val body = IgnoredUsersContent.createWithUserIds(list) val body = IgnoredUsersContent.createWithUserIds(list)
executeRequest<Unit>(eventBus) { executeRequest<Unit>(eventBus) {
apiCall = accountDataApi.setAccountData(userId, UserAccountData.TYPE_IGNORED_USER_LIST, body) apiCall = accountDataApi.setAccountData(userId, UserAccountDataTypes.TYPE_IGNORED_USER_LIST, body)
} }
// Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update) // Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update)

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.session.integrationmanager.IntegrationP
import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
@ -35,7 +35,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
fun getData(): Any fun getData(): Any
} }
data class IdentityParams(override val type: String = UserAccountData.TYPE_IDENTITY_SERVER, data class IdentityParams(override val type: String = UserAccountDataTypes.TYPE_IDENTITY_SERVER,
private val identityContent: IdentityServerContent private val identityContent: IdentityServerContent
) : Params { ) : Params {
@ -44,7 +44,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
} }
} }
data class AcceptedTermsParams(override val type: String = UserAccountData.TYPE_ACCEPTED_TERMS, data class AcceptedTermsParams(override val type: String = UserAccountDataTypes.TYPE_ACCEPTED_TERMS,
private val acceptedTermsContent: AcceptedTermsContent private val acceptedTermsContent: AcceptedTermsContent
) : Params { ) : Params {
@ -54,7 +54,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
} }
// TODO Use [UserAccountDataDirectMessages] class? // TODO Use [UserAccountDataDirectMessages] class?
data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES, data class DirectChatParams(override val type: String = UserAccountDataTypes.TYPE_DIRECT_MESSAGES,
private val directMessages: Map<String, List<String>> private val directMessages: Map<String, List<String>>
) : Params { ) : Params {
@ -63,7 +63,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
} }
} }
data class BreadcrumbsParams(override val type: String = UserAccountData.TYPE_BREADCRUMBS, data class BreadcrumbsParams(override val type: String = UserAccountDataTypes.TYPE_BREADCRUMBS,
private val breadcrumbsContent: BreadcrumbsContent private val breadcrumbsContent: BreadcrumbsContent
) : Params { ) : Params {
@ -72,7 +72,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
} }
} }
data class AllowedWidgets(override val type: String = UserAccountData.TYPE_ALLOWED_WIDGETS, data class AllowedWidgets(override val type: String = UserAccountDataTypes.TYPE_ALLOWED_WIDGETS,
private val allowedWidgetsContent: AllowedWidgetsContent) : Params { private val allowedWidgetsContent: AllowedWidgetsContent) : Params {
override fun getData(): Any { override fun getData(): Any {
@ -80,7 +80,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
} }
} }
data class IntegrationProvisioning(override val type: String = UserAccountData.TYPE_INTEGRATION_PROVISIONING, data class IntegrationProvisioning(override val type: String = UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING,
private val integrationProvisioningContent: IntegrationProvisioningContent) : Params { private val integrationProvisioningContent: IntegrationProvisioningContent) : Params {
override fun getData(): Any { override fun getData(): Any {

View File

@ -23,6 +23,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import im.vector.matrix.android.api.session.accountdata.UserAccountDataTypes
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -38,8 +40,6 @@ import im.vector.matrix.android.internal.session.SessionLifecycleObserver
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
import im.vector.matrix.android.internal.session.room.state.StateEventDataSource import im.vector.matrix.android.internal.session.room.state.StateEventDataSource
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory
import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence
@ -136,7 +136,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
widgetTypes: Set<String>? = null, widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null excludedTypes: Set<String>? = null
): LiveData<List<Widget>> { ): LiveData<List<Widget>> {
val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS) val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS)
return Transformations.map(widgetsAccountData) { return Transformations.map(widgetsAccountData) {
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList() it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList()
} }
@ -146,7 +146,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
widgetTypes: Set<String>? = null, widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null excludedTypes: Set<String>? = null
): List<Widget> { ): List<Widget> {
val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_WIDGETS) ?: return emptyList() val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) ?: return emptyList()
return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes) return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes)
} }

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.session.widgets.helper
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence<Widget> { internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence<Widget> {

View File

@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 0 ext.versionMinor = 0
ext.versionPatch = 0 ext.versionPatch = 1
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'

View File

@ -80,8 +80,8 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="riotx" /> <data android:scheme="element" />
<data android:host="riotx" /> <data android:host="element" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".features.media.ImageMediaViewerActivity" /> <activity android:name=".features.media.ImageMediaViewerActivity" />

View File

@ -28,7 +28,7 @@
</head> </head>
<body> <body>
<div> <div>
<p>RiotX Android</p> <p>Element Android</p>
<p>Third Party Licenses</p> <p>Third Party Licenses</p>
</div> </div>

View File

@ -77,6 +77,7 @@ import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
import im.vector.riotx.features.roommemberprofile.devices.DeviceListFragment import im.vector.riotx.features.roommemberprofile.devices.DeviceListFragment
import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionFragment import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionFragment
import im.vector.riotx.features.roomprofile.RoomProfileFragment import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
@ -534,4 +535,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(ContactsBookFragment::class) @FragmentKey(ContactsBookFragment::class)
fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomBannedMemberListFragment::class)
fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment
} }

View File

@ -16,7 +16,9 @@
package im.vector.riotx.core.epoxy package im.vector.riotx.core.epoxy
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
@ -26,14 +28,16 @@ import im.vector.riotx.core.extensions.setTextOrHide
abstract class LoadingItem : VectorEpoxyModel<LoadingItem.Holder>() { abstract class LoadingItem : VectorEpoxyModel<LoadingItem.Holder>() {
@EpoxyAttribute var loadingText: String? = null @EpoxyAttribute var loadingText: String? = null
@EpoxyAttribute var showLoader: Boolean = true
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.progressBar.isVisible = showLoader
holder.textView.setTextOrHide(loadingText) holder.textView.setTextOrHide(loadingText)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.loadingText) val textView by bind<TextView>(R.id.loadingText)
val progressBar by bind<ProgressBar>(R.id.loadingProgress)
} }
} }

View File

@ -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.riotx.core.epoxy.profiles
import android.view.View
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.crypto.util.toImageRes
import im.vector.riotx.features.home.AvatarRenderer
abstract class BaseProfileMatrixItem<T : ProfileMatrixItem.Holder> : VectorEpoxyModel<T>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var editable: Boolean = true
@EpoxyAttribute
var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: T) {
super.bind(holder)
val bestName = matrixItem.getBestName()
val matrixId = matrixItem.id
.takeIf { it != bestName }
// Special case for ThreePid fake matrix item
.takeIf { it != "@" }
holder.view.setOnClickListener(clickListener?.takeIf { editable })
holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId)
holder.editableView.isVisible = editable
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
}
}

View File

@ -20,43 +20,14 @@ package im.vector.riotx.core.epoxy.profiles
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.crypto.util.toImageRes
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) @EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>() { abstract class ProfileMatrixItem : BaseProfileMatrixItem<ProfileMatrixItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer open class Holder : VectorEpoxyHolder() {
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var editable: Boolean = true
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
val bestName = matrixItem.getBestName()
val matrixId = matrixItem.id
.takeIf { it != bestName }
// Special case for ThreePid fake matrix item
.takeIf { it != "@" }
holder.view.setOnClickListener(clickListener?.takeIf { editable })
holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId)
holder.editableView.isVisible = editable
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.matrixItemTitle) val titleView by bind<TextView>(R.id.matrixItemTitle)
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle) val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar) val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)

View File

@ -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.riotx.core.epoxy.profiles
import android.widget.ProgressBar
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item_progress)
abstract class ProfileMatrixItemWithProgress : BaseProfileMatrixItem<ProfileMatrixItemWithProgress.Holder>() {
@EpoxyAttribute var inProgress: Boolean = true
override fun bind(holder: Holder) {
super.bind(holder)
holder.progress.isVisible = inProgress
}
class Holder : ProfileMatrixItem.Holder() {
val progress by bind<ProgressBar>(R.id.matrixItemProgress)
}
}

View File

@ -51,7 +51,7 @@ class DefaultErrorFormatter @Inject constructor(
stringProvider.getString(R.string.login_error_unknown_host) stringProvider.getString(R.string.login_error_unknown_host)
is SSLPeerUnverifiedException -> is SSLPeerUnverifiedException ->
stringProvider.getString(R.string.login_error_ssl_peer_unverified) stringProvider.getString(R.string.login_error_ssl_peer_unverified)
is SSLException -> is SSLException ->
stringProvider.getString(R.string.login_error_ssl_other) stringProvider.getString(R.string.login_error_ssl_other)
else -> else ->
stringProvider.getString(R.string.error_no_network) stringProvider.getString(R.string.error_no_network)
@ -84,6 +84,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found) stringProvider.getString(R.string.login_reset_password_error_not_found)
} }
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
stringProvider.getString(R.string.auth_invalid_login_deactivated_account)
}
else -> { else -> {
throwable.error.message.takeIf { it.isNotEmpty() } throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() }

View File

@ -16,11 +16,18 @@
package im.vector.riotx.core.preference package im.vector.riotx.core.preference
import android.animation.Animator
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.TextView import android.widget.TextView
import androidx.core.animation.doOnEnd
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
/** /**
* Switch preference with title on multiline (only used in XML) * Switch preference with title on multiline (only used in XML)
@ -41,10 +48,49 @@ class VectorSwitchPreference : SwitchPreference {
isIconSpaceReserved = true isIconSpaceReserved = true
} }
var isHighlighted = false
set(value) {
field = value
notifyChanged()
}
var currentHighlightAnimator: Animator? = null
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {
// display the title in multi-line to avoid ellipsis. // display the title in multi-line to avoid ellipsis.
holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false
// cancel existing animation (find a way to resume if happens during anim?)
currentHighlightAnimator?.cancel()
val itemView = holder.itemView
if (isHighlighted) {
val colorFrom = Color.TRANSPARENT
val colorTo = ThemeUtils.getColor(itemView.context, R.attr.colorControlHighlight)
currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply {
duration = 250 // milliseconds
addUpdateListener { animator ->
itemView.setBackgroundColor(animator.animatedValue as Int)
}
doOnEnd {
currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorTo, colorFrom).apply {
duration = 250 // milliseconds
addUpdateListener { animator ->
itemView.setBackgroundColor(animator.animatedValue as Int)
}
doOnEnd {
isHighlighted = false
}
start()
}
}
startDelay = 200
start()
}
} else {
itemView.setBackgroundColor(Color.TRANSPARENT)
}
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
} }
} }

View File

@ -36,7 +36,7 @@ class AppNameProvider @Inject constructor(private val context: Context) {
return appName return appName
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## AppNameProvider() : failed") Timber.e(e, "## AppNameProvider() : failed")
return "RiotXAndroid" return "ElementAndroid"
} }
} }
} }

View File

@ -66,7 +66,7 @@ import im.vector.riotx.core.services.CallService
connection.connectionCapabilities = Connection.CAPABILITY_MUTE connection.connectionCapabilities = Connection.CAPABILITY_MUTE
connection.audioModeIsVoip = true connection.audioModeIsVoip = true
connection.setAddress(Uri.fromParts("tel", "+905000000000", null), TelecomManager.PRESENTATION_ALLOWED) connection.setAddress(Uri.fromParts("tel", "+905000000000", null), TelecomManager.PRESENTATION_ALLOWED)
connection.setCallerDisplayName("RiotX Caller", TelecomManager.PRESENTATION_ALLOWED) connection.setCallerDisplayName("Element Caller", TelecomManager.PRESENTATION_ALLOWED)
connection.statusHints = StatusHints("Testing Hint...", null, null) connection.statusHints = StatusHints("Testing Hint...", null, null)
bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0) bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0)

View File

@ -42,10 +42,13 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState
import im.vector.riotx.push.fcm.FcmHelper import im.vector.riotx.push.fcm.FcmHelper
@ -69,7 +72,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
@Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@ -134,6 +137,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
when (it) { when (it) {
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
}.exhaustive }.exhaustive
} }
homeActivityViewModel.subscribe(this) { renderState(it) } homeActivityViewModel.subscribe(this) { renderState(it) }
@ -193,6 +197,42 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
} }
} }
private fun handlePromptToEnablePush() {
popupAlertManager.postVectorAlert(
DefaultVectorAlert(
uid = "enablePush",
title = getString(R.string.alert_push_are_disabled_title),
description = getString(R.string.alert_push_are_disabled_description),
iconId = R.drawable.ic_room_actions_notifications_mutes,
shouldBeDisplayedIn = {
it is HomeActivity
}
).apply {
colorInt = ThemeUtils.getColor(this@HomeActivity, R.attr.vctr_notice_secondary)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
// action(it)
homeActivityViewModel.handle(HomeActivityViewActions.PushPromptHasBeenReviewed)
it.navigator.openSettings(it, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_NOTIFICATIONS)
}
}
dismissedAction = Runnable {
homeActivityViewModel.handle(HomeActivityViewActions.PushPromptHasBeenReviewed)
}
addButton(getString(R.string.dismiss), Runnable {
homeActivityViewModel.handle(HomeActivityViewActions.PushPromptHasBeenReviewed)
}, true)
addButton(getString(R.string.settings), Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
// action(it)
homeActivityViewModel.handle(HomeActivityViewActions.PushPromptHasBeenReviewed)
it.navigator.openSettings(it, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_NOTIFICATIONS)
}
}, true)
}
)
}
private fun promptSecurityEvent(userItem: MatrixItem.UserItem?, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) { private fun promptSecurityEvent(userItem: MatrixItem.UserItem?, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
popupAlertManager.postVectorAlert( popupAlertManager.postVectorAlert(
VerificationVectorAlert( VerificationVectorAlert(

View File

@ -14,14 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.internal.session.sync.model.accountdata package im.vector.riotx.features.home
import com.squareup.moshi.Json import im.vector.riotx.core.platform.VectorViewModelAction
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent
@JsonClass(generateAdapter = true) sealed class HomeActivityViewActions : VectorViewModelAction {
internal data class UserAccountDataIntegrationProvisioning( object PushPromptHasBeenReviewed : HomeActivityViewActions()
@Json(name = "type") override val type: String = TYPE_INTEGRATION_PROVISIONING, }
@Json(name = "content") val content: IntegrationProvisioningContent
) : UserAccountData()

View File

@ -22,4 +22,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class HomeActivityViewEvents : VectorViewEvents { sealed class HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents() data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents() data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents()
object PromptToEnableSessionPush : HomeActivityViewEvents()
} }

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.home package im.vector.riotx.features.home
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
@ -23,24 +24,32 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.pushrules.RuleIds
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.rx.asObservable import im.vector.matrix.rx.asObservable
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.login.ReAuthHelper import im.vector.riotx.features.login.ReAuthHelper
import im.vector.riotx.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
class HomeActivityViewModel @AssistedInject constructor( class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState, @Assisted initialState: HomeActivityViewState,
@Assisted private val args: HomeActivityArgs, @Assisted private val args: HomeActivityArgs,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val reAuthHelper: ReAuthHelper private val reAuthHelper: ReAuthHelper,
) : VectorViewModel<HomeActivityViewState, EmptyAction, HomeActivityViewEvents>(initialState) { private val vectorPreferences: VectorPreferences
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -62,6 +71,7 @@ class HomeActivityViewModel @AssistedInject constructor(
init { init {
observeInitialSync() observeInitialSync()
mayBeInitializeCrossSigning() mayBeInitializeCrossSigning()
checkSessionPushIsOn()
} }
private fun observeInitialSync() { private fun observeInitialSync() {
@ -115,6 +125,41 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
/**
* After migration from riot to element some users reported that their
* push setting for the session was set to off
* In order to mitigate this, we want to display a popup once to the user
* giving him the option to review this setting
*/
private fun checkSessionPushIsOn() {
viewModelScope.launch(Dispatchers.IO) {
// Don't do that if it's a login or a register (pass in memory)
if (reAuthHelper.data != null) return@launch
// Check if disabled for this device
if (!vectorPreferences.areNotificationEnabledForDevice()) {
// Check if set at account level
val mRuleMaster = activeSessionHolder.getSafeActiveSession()
?.getPushRules()
?.getAllRules()
?.find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL }
if (mRuleMaster?.enabled == false) {
// So push are enabled at account level but not for this session
// Let's check that there are some rooms?
val knownRooms = activeSessionHolder.getSafeActiveSession()?.getRoomSummaries(roomSummaryQueryParams {
memberships = Membership.activeMemberships()
})?.size ?: 0
// Prompt once to the user
if (knownRooms > 1 && !vectorPreferences.didAskUserToEnableSessionPush()) {
// delay a bit
delay(1500)
_viewEvents.post(HomeActivityViewEvents.PromptToEnableSessionPush)
}
}
}
}
}
private fun maybeBootstrapCrossSigning() { private fun maybeBootstrapCrossSigning() {
// In case of account creation, it is already done before // In case of account creation, it is already done before
if (args.accountCreation) return if (args.accountCreation) return
@ -167,7 +212,11 @@ class HomeActivityViewModel @AssistedInject constructor(
}) })
} }
override fun handle(action: EmptyAction) { override fun handle(action: HomeActivityViewActions) {
// NA when (action) {
HomeActivityViewActions.PushPromptHasBeenReviewed -> {
vectorPreferences.setDidAskUserToEnableSessionPush()
}
}.exhaustive
} }
} }

View File

@ -415,11 +415,11 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.clear_message_queue -> R.id.clear_message_queue ->
// For now always disable when not in developer mode, worker cancellation is not working properly // For now always disable when not in developer mode, worker cancellation is not working properly
timeline.pendingEventCount() > 0 && vectorPreferences.developerMode() timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0 R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0 R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call,
R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null R.id.video_call -> state.asyncRoomSummary()?.canStartCall == true && webRtcPeerConnectionManager.currentCall == null
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
else -> false else -> false
} }

View File

@ -74,7 +74,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
// fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
// fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
// fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) // fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEditedDecorationClicked(informationData: MessageInformationData) fun onEditedDecorationClicked(informationData: MessageInformationData)
@ -107,7 +108,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onUrlLongClicked(url: String): Boolean fun onUrlLongClicked(url: String): Boolean
} }
private var showingForwardLoader = false
// Map eventId to adapter position // Map eventId to adapter position
private val adapterPositionMapping = HashMap<String, Int>() private val adapterPositionMapping = HashMap<String, Int>()
private val modelCache = arrayListOf<CacheItemData?>() private val modelCache = arrayListOf<CacheItemData?>()
@ -233,7 +233,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun buildModels() { override fun buildModels() {
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
showingForwardLoader = LoadingItem_()
val showingForwardLoader = LoadingItem_()
.id("forward_loading_item_$timestamp") .id("forward_loading_item_$timestamp")
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
.addWhenLoading(Timeline.Direction.FORWARDS) .addWhenLoading(Timeline.Direction.FORWARDS)
@ -242,12 +243,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
add(timelineModels) add(timelineModels)
// Avoid displaying two loaders if there is no elements between them // Avoid displaying two loaders if there is no elements between them
if (!showingForwardLoader || timelineModels.isNotEmpty()) { val showBackwardsLoader = !showingForwardLoader || timelineModels.isNotEmpty()
LoadingItem_() // We can hide the loader but still add the item to controller so it can trigger backwards pagination
.id("backward_loading_item_$timestamp") LoadingItem_()
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS) .id("backward_loading_item_$timestamp")
.addWhenLoading(Timeline.Direction.BACKWARDS) .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
} .showLoader(showBackwardsLoader)
.addWhenLoading(Timeline.Direction.BACKWARDS)
} }
// Timeline.LISTENER *************************************************************************** // Timeline.LISTENER ***************************************************************************

View File

@ -69,7 +69,7 @@ class MessageActionsEpoxyController @Inject constructor(
id("send_state") id("send_state")
showProgress(false) showProgress(false)
text(stringProvider.getString(R.string.unable_to_send_message)) text(stringProvider.getString(R.string.unable_to_send_message))
drawableStart(R.drawable.ic_warning_small) drawableStart(R.drawable.ic_warning_badge)
} }
} }

View File

@ -30,9 +30,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.isReply
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyAction
@ -113,7 +111,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
} }
if (event.eventId == it.eventId) { if (event.eventId == it.eventId) {
originalIsReply = it.getClearContent().toModel<MessageContent>().isReply() originalIsReply = it.isReply()
} }
} }
} }

View File

@ -354,16 +354,22 @@ class MessageItemFactory @Inject constructor(
when (codeVisitor.codeKind) { when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> { CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes) if (codeFormattedBlock == null) {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
} else {
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
}
} }
CodeVisitor.Kind.INLINE -> { CodeVisitor.Kind.INLINE -> {
val codeFormatted = htmlRenderer.get().render(localFormattedBody) val codeFormatted = htmlRenderer.get().render(localFormattedBody)
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) if (codeFormatted == null) {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
} else {
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
} }
CodeVisitor.Kind.NONE -> { CodeVisitor.Kind.NONE -> {
val compressed = htmlCompressor.compress(messageContent.formattedBody!!) buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
val formattedBody = htmlRenderer.get().render(compressed)
buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
} }
} }
} else { } else {
@ -371,6 +377,16 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun buildFormattedTextItem(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
val formattedBody = htmlRenderer.get().render(compressed)
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
private fun buildMessageTextItem(body: CharSequence, private fun buildMessageTextItem(body: CharSequence,
isFormatted: Boolean, isFormatted: Boolean,
informationData: MessageInformationData, informationData: MessageInformationData,

View File

@ -17,11 +17,14 @@
package im.vector.riotx.features.home.room.detail.timeline.format package im.vector.riotx.features.home.room.detail.timeline.format
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.session.room.timeline.isReply
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
@ -31,6 +34,7 @@ import javax.inject.Inject
class DisplayableEventFormatter @Inject constructor( class DisplayableEventFormatter @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val emojiCompatWrapper: EmojiCompatWrapper,
private val noticeEventFormatter: NoticeEventFormatter private val noticeEventFormatter: NoticeEventFormatter
) { ) {
@ -47,10 +51,16 @@ class DisplayableEventFormatter @Inject constructor(
val senderName = timelineEvent.senderInfo.disambiguatedDisplayName val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
when (timelineEvent.root.getClearType()) { when (timelineEvent.root.getClearType()) {
EventType.STICKER -> { EventType.STICKER -> {
return simpleFormat(senderName, stringProvider.getString(R.string.send_a_sticker), appendAuthor) return simpleFormat(senderName, stringProvider.getString(R.string.send_a_sticker), appendAuthor)
} }
EventType.MESSAGE -> { EventType.REACTION -> {
timelineEvent.root.getClearContent().toModel<ReactionContent>()?.relatesTo?.let {
val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(it.key)
return simpleFormat(senderName, emojiSpanned, appendAuthor)
}
}
EventType.MESSAGE -> {
timelineEvent.getLastMessageContent()?.let { messageContent -> timelineEvent.getLastMessageContent()?.let { messageContent ->
when (messageContent.msgType) { when (messageContent.msgType) {
MessageType.MSGTYPE_VERIFICATION_REQUEST -> { MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
@ -69,13 +79,13 @@ class DisplayableEventFormatter @Inject constructor(
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor) return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
} }
MessageType.MSGTYPE_TEXT -> { MessageType.MSGTYPE_TEXT -> {
if (messageContent.isReply()) { return if (timelineEvent.isReply()) {
// Skip reply prefix, and show important // Skip reply prefix, and show important
// TODO add a reply image span ? // TODO add a reply image span ?
return simpleFormat(senderName, timelineEvent.getTextEditableContent() simpleFormat(senderName, timelineEvent.getTextEditableContent()
?: messageContent.body, appendAuthor) ?: messageContent.body, appendAuthor)
} else { } else {
return simpleFormat(senderName, messageContent.body, appendAuthor) simpleFormat(senderName, messageContent.body, appendAuthor)
} }
} }
else -> { else -> {
@ -84,7 +94,7 @@ class DisplayableEventFormatter @Inject constructor(
} }
} }
} }
else -> { else -> {
return span { return span {
text = noticeEventFormatter.format(timelineEvent) ?: "" text = noticeEventFormatter.format(timelineEvent) ?: ""
textStyle = "italic" textStyle = "italic"

View File

@ -52,6 +52,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false
@EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false
@EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasFailedSending: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null
@EpoxyAttribute var showSelected: Boolean = false @EpoxyAttribute var showSelected: Boolean = false
@ -72,6 +73,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
avatarRenderer.render(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null
holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes()) holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes())
holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
renderSelection(holder, showSelected) renderSelection(holder, showSelected)
holder.typingView.setTextOrHide(typingMessage) holder.typingView.setTextOrHide(typingMessage)
holder.lastEventView.isInvisible = holder.typingView.isVisible holder.lastEventView.isInvisible = holder.typingView.isVisible
@ -106,6 +108,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
val avatarCheckedImageView by bind<ImageView>(R.id.roomAvatarCheckedImageView) val avatarCheckedImageView by bind<ImageView>(R.id.roomAvatarCheckedImageView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView) val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val roomAvatarDecorationImageView by bind<ImageView>(R.id.roomAvatarDecorationImageView) val roomAvatarDecorationImageView by bind<ImageView>(R.id.roomAvatarDecorationImageView)
val roomAvatarFailSendingImageView by bind<ImageView>(R.id.roomAvatarFailSendingImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout) val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
} }
} }

View File

@ -109,6 +109,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.lastFormattedEvent(latestFormattedEvent) .lastFormattedEvent(latestFormattedEvent)
.showHighlighted(showHighlighted) .showHighlighted(showHighlighted)
.showSelected(showSelected) .showSelected(showSelected)
.hasFailedSending(roomSummary.hasFailedSending)
.unreadNotificationCount(unreadCount) .unreadNotificationCount(unreadCount)
.hasUnreadMessage(roomSummary.hasUnreadMessages) .hasUnreadMessage(roomSummary.hasUnreadMessages)
.hasDraft(roomSummary.userDrafts.isNotEmpty()) .hasDraft(roomSummary.userDrafts.isNotEmpty())

View File

@ -25,6 +25,7 @@ import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp import io.noties.markwon.html.TagHandlerNoOp
import org.commonmark.node.Node import org.commonmark.node.Node
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -41,11 +42,21 @@ class EventHtmlRenderer @Inject constructor(context: Context,
} }
fun render(text: String): CharSequence { fun render(text: String): CharSequence {
return markwon.toMarkdown(text) return try {
markwon.toMarkdown(text)
} catch (failure: Throwable) {
Timber.v("Fail to render $text to html")
text
}
} }
fun render(node: Node): CharSequence { fun render(node: Node): CharSequence? {
return markwon.render(node) return try {
markwon.render(node)
} catch (failure: Throwable) {
Timber.v("Fail to render $node to html")
return null
}
} }
} }

View File

@ -16,7 +16,7 @@
package im.vector.riotx.features.login package im.vector.riotx.features.login
const val MODULAR_LINK = "https://modular.im/services/matrix-hosting-riot" + const val EMS_LINK = "https://element.io/matrix-services" +
"?utm_source=riot-x-android" + "?utm_source=element-android" +
"&utm_medium=native" + "&utm_medium=native" +
"&utm_campaign=riot-x-android-authentication" "&utm_campaign=element-android-authentication"

View File

@ -19,12 +19,10 @@ package im.vector.riotx.features.login
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@ -73,14 +71,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer)
private val commonOption: (FragmentTransaction) -> Unit = { ft -> private val commonOption: (FragmentTransaction) -> Unit = { ft ->
// Find the loginLogo on the current Fragment, this should not return null
(topFragment?.view as? ViewGroup)
// Find findViewById does not work, I do not know why
// findViewById<View?>(R.id.loginLogo)
?.children
?.firstOrNull { it.id == R.id.loginLogo }
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
} }
@ -145,7 +135,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
addFragmentToBackstack(R.id.loginFragmentContainer, addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment::class.java, LoginServerSelectionFragment::class.java,
option = { ft -> option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering // TODO Disabled because it provokes a flickering
@ -231,7 +220,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) { private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) {
when (loginViewEvents.serverType) { when (loginViewEvents.serverType) {
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
ServerType.Modular, ServerType.EMS,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerUrlFormFragment::class.java, LoginServerUrlFormFragment::class.java,
option = commonOption) option = commonOption)

View File

@ -155,7 +155,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
loginNotice.text = getString(R.string.login_server_matrix_org_text) loginNotice.text = getString(R.string.login_server_matrix_org_text)
} }
ServerType.Modular -> { ServerType.EMS -> {
loginServerIcon.isVisible = true loginServerIcon.isVisible = true
loginServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services) loginServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services)
loginTitle.text = getString(resId, "Element Matrix Services") loginTitle.text = getString(resId, "Element Matrix Services")

View File

@ -43,15 +43,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
} }
private fun initTextViews() { private fun initTextViews() {
loginServerChoiceModularLearnMore.text = span { loginServerChoiceEMSLearnMore.text = span {
text = getString(R.string.login_server_modular_learn_more) text = getString(R.string.login_server_modular_learn_more)
textDecorationLine = "underline" textDecorationLine = "underline"
} }
} }
@OnClick(R.id.loginServerChoiceModularLearnMore) @OnClick(R.id.loginServerChoiceEMSLearnMore)
fun learnMore() { fun learnMore() {
openUrlInChromeCustomTab(requireActivity(), null, MODULAR_LINK) openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
} }
@OnClick(R.id.loginServerChoiceMatrixOrg) @OnClick(R.id.loginServerChoiceMatrixOrg)
@ -59,9 +59,9 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
} }
@OnClick(R.id.loginServerChoiceModular) @OnClick(R.id.loginServerChoiceEMS)
fun selectModular() { fun selectEMS() {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) loginViewModel.handle(LoginAction.UpdateServerType(ServerType.EMS))
} }
@OnClick(R.id.loginServerChoiceOther) @OnClick(R.id.loginServerChoiceOther)

View File

@ -62,7 +62,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
private fun setupUi(state: LoginViewState) { private fun setupUi(state: LoginViewState) {
when (state.serverType) { when (state.serverType) {
ServerType.Modular -> { ServerType.EMS -> {
loginServerUrlFormIcon.isVisible = true loginServerUrlFormIcon.isVisible = true
loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular) loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular)
loginServerUrlFormText.text = getString(R.string.login_server_url_form_modular_text) loginServerUrlFormText.text = getString(R.string.login_server_url_form_modular_text)
@ -70,7 +70,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint) loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint)
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice) loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice)
} }
else -> { else -> {
loginServerUrlFormIcon.isVisible = false loginServerUrlFormIcon.isVisible = false
loginServerUrlFormTitle.text = getString(R.string.login_server_other_title) loginServerUrlFormTitle.text = getString(R.string.login_server_other_title)
loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server) loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server)
@ -83,7 +83,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
@OnClick(R.id.loginServerUrlFormLearnMore) @OnClick(R.id.loginServerUrlFormLearnMore)
fun learnMore() { fun learnMore() {
openUrlInChromeCustomTab(requireActivity(), null, MODULAR_LINK) openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
} }
override fun resetViewModel() { override fun resetViewModel() {

View File

@ -38,7 +38,7 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo
loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl()) loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text)
} }
ServerType.Modular -> { ServerType.EMS -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services) loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services)
loginSignupSigninServerIcon.isVisible = true loginSignupSigninServerIcon.isVisible = true
loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular) loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular)

View File

@ -410,7 +410,7 @@ class LoginViewModel @AssistedInject constructor(
ServerType.MatrixOrg -> ServerType.MatrixOrg ->
// Request login flow here // Request login flow here
handle(LoginAction.UpdateHomeServer(matrixOrgUrl)) handle(LoginAction.UpdateHomeServer(matrixOrgUrl))
ServerType.Modular, ServerType.EMS,
ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType)) ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType))
}.exhaustive }.exhaustive
} }

View File

@ -73,7 +73,7 @@ data class LoginViewState(
append(homeServerUrl?.trim { it == '/' }) append(homeServerUrl?.trim { it == '/' })
append(SSO_REDIRECT_PATH) append(SSO_REDIRECT_PATH)
// Set a redirect url we will intercept later // Set a redirect url we will intercept later
appendParamToUrl(SSO_REDIRECT_URL_PARAM, RIOTX_REDIRECT_URL) appendParamToUrl(SSO_REDIRECT_URL_PARAM, VECTOR_REDIRECT_URL)
deviceId?.takeIf { it.isNotBlank() }?.let { deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755 // But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it) appendParamToUrl("device_id", it)
@ -83,6 +83,6 @@ data class LoginViewState(
companion object { companion object {
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
private const val RIOTX_REDIRECT_URL = "riotx://riotx" private const val VECTOR_REDIRECT_URL = "element://element"
} }
} }

View File

@ -19,6 +19,6 @@ package im.vector.riotx.features.login
enum class ServerType { enum class ServerType {
Unknown, Unknown,
MatrixOrg, MatrixOrg,
Modular, EMS,
Other Other
} }

View File

@ -155,14 +155,17 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
/** Clear all known message events for this room */ /** Clear all known message events for this room */
fun clearMessageEventOfRoom(roomId: String?) { fun clearMessageEventOfRoom(roomId: String?) {
Timber.v("clearMessageEventOfRoom $roomId") Timber.v("clearMessageEventOfRoom $roomId")
if (roomId != null) { if (roomId != null) {
var shouldUpdate = false
synchronized(eventList) { synchronized(eventList) {
eventList.removeAll { e -> shouldUpdate = eventList.removeAll { e ->
e is NotifiableMessageEvent && e.roomId == roomId e is NotifiableMessageEvent && e.roomId == roomId
} }
} }
refreshNotificationDrawer() if (shouldUpdate) {
notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
refreshNotificationDrawer()
}
} }
} }

View File

@ -65,7 +65,7 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter
putBoolean(PREFS_CRASH_KEY, true) putBoolean(PREFS_CRASH_KEY, true)
} }
val b = StringBuilder() val b = StringBuilder()
val appName = "RiotX" // TODO Matrix.getApplicationName() val appName = "Element" // TODO Matrix.getApplicationName()
b.append(appName + " Build : " + versionCodeProvider.getVersionCode() + "\n") b.append(appName + " Build : " + versionCodeProvider.getVersionCode() + "\n")
b.append("$appName Version : ${versionProvider.getVersion(longFormat = true, useBuildNumber = true)}\n") b.append("$appName Version : ${versionProvider.getVersion(longFormat = true, useBuildNumber = true)}\n")

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.roomdirectory.picker
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isInvisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
@ -58,6 +59,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
} }
} }
.into(holder.avatarView) .into(holder.avatarView)
holder.avatarView.isInvisible = directoryAvatarUrl.isNullOrBlank() && includeAllNetworks
holder.nameView.text = directoryName holder.nameView.text = directoryName
holder.descritionView.setTextOrHide(directoryDescription) holder.descritionView.setTextOrHide(directoryDescription)

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryD
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomdirectory.RoomDirectoryAction import im.vector.riotx.features.roomdirectory.RoomDirectoryAction
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction
@ -35,7 +36,6 @@ import kotlinx.android.synthetic.main.fragment_room_directory_picker.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// TODO Set title to R.string.select_room_directory
// TODO Menu to add custom room directory (not done in RiotWeb so far...) // TODO Menu to add custom room directory (not done in RiotWeb so far...)
class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory, class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory,
private val roomDirectoryPickerController: RoomDirectoryPickerController private val roomDirectoryPickerController: RoomDirectoryPickerController
@ -91,6 +91,11 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
sharedActionViewModel.post(RoomDirectorySharedAction.Back) sharedActionViewModel.post(RoomDirectorySharedAction.Back)
} }
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.select_room_directory)
}
override fun retry() { override fun retry() {
Timber.v("Retry") Timber.v("Retry")
pickerViewModel.handle(RoomDirectoryPickerAction.Retry) pickerViewModel.handle(RoomDirectoryPickerAction.Retry)

Some files were not shown because too many files have changed in this diff Show More