Merge remote-tracking branch 'upstream/develop' into rust

This commit is contained in:
Damir Jelić 2021-04-20 14:44:02 +02:00
commit 324cdc4db1
63 changed files with 947 additions and 397 deletions

View file

@ -1,9 +1,47 @@
Changes in Element 1.1.4 (2021-XX-XX)
Changes in Element 1.1.7 (2021-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
-
Bugfix 🐛:
- Message states cosmetic changes (#3007)
Translations 🗣:
-
SDK API changes ⚠️:
-
Build 🧱:
- Upgrade to gradle 7
Test:
-
Other changes:
- New store descriptions
Changes in Element 1.1.6 (2021-04-16)
===================================================
Bugfix 🐛:
- Fix crash on the timeline
- App crashes on "troubleshoot notifications" button (#3187)
Changes in Element 1.1.5 (2021-04-15)
===================================================
Bugfix 🐛:
- Fix crash during Realm migration
- Fix crash when playing video (#3179)
Changes in Element 1.1.4 (2021-04-09)
===================================================
Improvements 🙌:
- Split network request `/keys/query` into smaller requests (250 users max) (#2925)
- Crypto improvement | Bulk send NO_OLM withheld code
@ -18,6 +56,8 @@ Improvements 🙌:
- Room list improvements (paging)
- Fix quick click action (#3127)
- Get Event after a Push for a faster notification display in some conditions
- Always try to retry Http requests in case of 429 (#1300)
- registration availability endpoint added to matrix-sdk
Bugfix 🐛:
- Fix bad theme change for the MainActivity
@ -26,9 +66,7 @@ Bugfix 🐛:
- Fix avatar rendering for DMs, after initial sync (#2693)
- Fix mandatory parameter in API (#3065)
- If signout request fails, do not start LoginActivity, but restart the app (#3099)
Translations 🗣:
-
- Retain keyword order in emoji import script, and update the generated file (#3147)
SDK API changes ⚠️:
- Several Services have been migrated to coroutines (#2449)
@ -37,9 +75,6 @@ SDK API changes ⚠️:
Build 🧱:
- Properly exclude gms dependencies in fdroid build flavour which were pulled in through the jitsi SDK (#3125)
Test:
-
Other changes:
- Add version details on the login screen, in debug or developer mode
- Migrate Retrofit interface to coroutine calls

View file

@ -69,7 +69,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation 'com.google.android.material:material:1.3.0'
}

View file

@ -17,7 +17,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.3'
classpath "com.likethesalad.android:string-reference:1.2.1"
classpath "com.likethesalad.android:string-reference:1.2.2"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -51,7 +51,7 @@ allprojects {
}
}
maven {
url "http://dl.bintray.com/piasy/maven"
url "https://dl.bintray.com/piasy/maven"
content {
includeGroupByRegex "com\\.github\\.piasy"
}

View file

@ -0,0 +1,2 @@
Main changes in this version: performance improvement and bug fixes!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.4

View file

@ -0,0 +1,2 @@
Main changes in this version: hot fixes for 1.1.4
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.5

View file

@ -0,0 +1,2 @@
Main changes in this version: hot fixes for 1.1.5
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.6

View file

@ -1,30 +1,39 @@
Element is a new type of messenger and collaboration app that:
Element is both a secure messenger and a productivity team collaboration app that is ideal for group chats while remote working. This chat app uses end-to-end encryption to provide powerful video conferencing, file sharing and voice calls.
1. Puts you in control to preserve your privacy
2. Lets you communicate with anyone in the Matrix network, and even beyond by integrating with apps such as Slack
3. Protects you from advertising, datamining and walled gardens
4. Secures you through end-to-end encryption, with cross-signing to verify others
<b>Elements features include:</b>
- Advanced online communication tools
- Fully encrypted messages to allow safer corporate communication, even for remote workers
- Decentralized chat based on the Matrix open source framework
- File sharing securely with encrypted data while managing projects
- Video chats with Voice over IP and screen sharing
- Easy integration with your favourite online collaboration tools, project management tools, VoIP services and other team messaging apps
Element is completely different from other messaging and collaboration apps because it is decentralised and open source.
Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages.
Element lets you self-host - or choose a host - so that you have privacy, ownership and control of your data and conversations. It gives you access to an open network; so youre not just stuck speaking to other Element users only. And it is very secure.
<b>Privacy and encrypted messaging</b>
Element protects you from unwanted ads, data mining and walled gardens. It also secures all your data, one-to-one video and voice communication through end-to-end encryption and cross-signed device verification.
Element is able to do all this because it operates on Matrix - the standard for open, decentralised communication.
Element gives you control over your privacy while allowing you to communicate securely with anyone on the Matrix network, or other business collaboration tools by integrating with apps such as Slack.
Element puts you in control by letting you choose who hosts your conversations. From the Element app, you can choose to host in different ways:
<b>Element can be self-hosted</b>
To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility.
<b>Own your data</b>
You decide where to keep your data and messages. Without the risk of data mining or access from third parties.
Element puts you in control in different ways:
1. Get a free account on the matrix.org public server hosted by the Matrix developers, or choose from thousands of public servers hosted by volunteers
2. Self-host your account by running a server on your own hardware
2. Self-host your account by running a server on your own IT infrastructure
3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform
<b>Why choose Element?</b>
<b>Open messaging and collaboration</b>
You can chat with anyone on the Matrix network, whether theyre using Element, another Matrix app or even if they are using a different messaging app.
<b>OWN YOUR DATA</b>: You decide where to keep your data and messages. You own it and control it, not some MEGACORP that mines your data or gives access to third parties.
<b>Super secure</b>
Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signed device verification.
<b>OPEN MESSAGING AND COLLABORATION</b>: You can chat with anyone else in the Matrix network, whether theyre using Element or another Matrix app, and even if they are using a different messaging system of the likes of Slack, IRC or XMPP.
<b>Complete communication and integration</b>
Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done.
<b>SUPER-SECURE</b>: Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signing to verify the devices of conversation participants.
<b>COMPLETE COMMUNICATION</b>: Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done.
<b>EVERYWHERE YOU ARE</b>: Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io.
<b>Pick up where you left off</b>
Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io

View file

@ -1 +1 @@
Secure decentralised chat & VoIP. Keep your data safe from third parties.
Group messenger - encrypted messaging, group chat and video calls

View file

@ -1 +1 @@
Element (previously Riot.im)
Element - Secure Messenger

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9af5c8e7e2cd1a3b0f694a4ac262b9f38c75262e74a9e8b5101af302a6beadd7
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
distributionSha256Sum=81003f83b0056d20eedf48cddd4f52a9813163d4ba185bcf8abd34b8eeea4cbd
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -1,2 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.matrix.android.sdk.rx" />
<manifest package="org.matrix.android.sdk.rx" />

View file

@ -124,8 +124,8 @@ class RxSession(private val session: Session) {
.startWithCallable { session.getPendingThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
fun createRoom(roomParams: CreateRoomParams): Single<String> = rxSingle {
session.createRoom(roomParams)
}
fun searchUsersDirectory(search: String,
@ -136,13 +136,13 @@ class RxSession(private val session: Session) {
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
session.joinRoom(roomIdOrAlias, reason, viaServers, it)
viaServers: List<String> = emptyList()): Single<Unit> = rxSingle {
session.joinRoom(roomIdOrAlias, reason, viaServers)
}
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = rxSingle {
session.getRoomIdByAlias(roomAlias, searchOnServer)
}
fun getProfileInfo(userId: String): Single<JsonDict> = rxSingle {

View file

@ -6,10 +6,13 @@ apply plugin: 'realm-android'
buildscript {
repositories {
mavenCentral()
// mavenCentral()
//noinspection GrDeprecatedAPIUsage
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:10.4.0"
// Stick to this version until https://github.com/realm/realm-java/issues/7402 is fixed
classpath "io.realm:realm-gradle-plugin:10.3.1"
}
}
@ -126,7 +129,7 @@ dependencies {
def lifecycle_version = '2.2.0'
def arch_version = '2.1.0'
def markwon_version = '3.1.0'
def daggerVersion = '2.33'
def daggerVersion = '2.34'
def work_version = '2.5.0'
def retrofit_version = '2.9.0'

View file

@ -66,8 +66,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
val roomId = mTestHelper.runBlockingTest {
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" })
}
if (encryptedRoom) {
@ -135,7 +135,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
bobRoomSummariesLive.observeForever(roomJoinedObserver)
}
mTestHelper.doSync<Unit> { bobSession.joinRoom(aliceRoomId, callback = it) }
mTestHelper.runBlockingTest { bobSession.joinRoom(aliceRoomId) }
mTestHelper.await(lock)
@ -176,8 +176,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
room.invite(samSession.myUserId, null)
}
mTestHelper.doSync<Unit> {
samSession.joinRoom(room.roomId, null, emptyList(), it)
mTestHelper.runBlockingTest {
samSession.joinRoom(room.roomId, null, emptyList())
}
return samSession
@ -256,8 +256,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
}
fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.doSync<String> {
alice.createDirectRoom(bob.myUserId, it)
val roomId = mTestHelper.runBlockingTest {
alice.createDirectRoom(bob.myUserId)
}
mTestHelper.waitWithLatch { latch ->
@ -300,7 +300,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
bobRoomSummariesLive.observeForever(newRoomObserver)
}
mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
mTestHelper.runBlockingTest { bob.joinRoom(roomId) }
}
return roomId
@ -398,8 +398,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
val roomId = mTestHelper.runBlockingTest {
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" })
}
val room = aliceSession.getRoom(roomId)!!
@ -412,7 +412,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val session = mTestHelper.createAccount("User_$index", defaultSessionParams)
mTestHelper.runBlockingTest(timeout = 600_000) { room.invite(session.myUserId, null) }
println("TEST -> " + session.myUserId + " invited")
mTestHelper.doSync<Unit> { session.joinRoom(room.roomId, null, emptyList(), it) }
mTestHelper.runBlockingTest { session.joinRoom(room.roomId, null, emptyList()) }
println("TEST -> " + session.myUserId + " joined")
sessions.add(session)
}

View file

@ -66,6 +66,138 @@ class KeyShareTests : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Test
fun test_DoNotSelfShareIfNotTrusted() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
// Create an encrypted room and add a message
val roomId = mTestHelper.runBlockingTest {
aliceSession.createRoom(
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
}
)
}
val room = aliceSession.getRoom(roomId)
assertNotNull(room)
Thread.sleep(4_000)
assertTrue(room?.isEncrypted() == true)
val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
// Open a new sessionx
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId)
assertNotNull(receivedEvent)
assert(receivedEvent!!.isEncrypted())
try {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
fail("should fail")
} catch (failure: Throwable) {
}
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
// Try to request
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
val waitLatch = CountDownLatch(1)
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
var outGoingRequestId: String? = null
mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
.filter { req ->
// filter out request that was known before
!outgoingRequestsBefore.any { req.requestId == it.requestId }
}
.let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
outGoingRequestId = outgoing?.requestId
outgoing != null
}
}
mTestHelper.await(waitLatch)
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
// We should have a new request
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestsBefore.size)
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
// The first session should see an incoming request
// the request should be refused, because the device is not trusted
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
Log.v("TEST", "=========================")
it.forEach { keyRequest ->
Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}")
}
Log.v("TEST", "=========================")
}
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming?.state == GossipingRequestState.REJECTED
}
}
try {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
fail("should fail")
} catch (failure: Throwable) {
}
// Mark the device as trusted
aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession2.sessionParams.deviceId ?: "")
// Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1")
Log.v("TEST", "=========================")
it.forEach {
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
}
Log.v("TEST", "=========================")
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
}
}
}
Thread.sleep(6_000)
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
}
}
}
try {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
} catch (failure: Throwable) {
fail("should have been able to decrypt")
}
mTestHelper.signOutAndClose(aliceSession)
mTestHelper.signOutAndClose(aliceSession2)
}
@Test
fun test_ShareSSSSSecret() {
val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
@ -199,13 +331,12 @@ class KeyShareTests : InstrumentedTest {
}
// Create an encrypted room and send a couple of messages
val roomId = mTestHelper.doSync<String> {
val roomId = mTestHelper.runBlockingTest {
aliceSession.createRoom(
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
},
it
}
)
}
val roomAlicePov = aliceSession.getRoom(roomId)
@ -238,8 +369,8 @@ class KeyShareTests : InstrumentedTest {
roomAlicePov.invite(bobSession.myUserId, null)
}
mTestHelper.doSync<Unit> {
bobSession.joinRoom(roomAlicePov.roomId, null, emptyList(), it)
mTestHelper.runBlockingTest {
bobSession.joinRoom(roomAlicePov.roomId, null, emptyList())
}
// we want to discard alice outbound session

View file

@ -16,12 +16,10 @@
package org.matrix.android.sdk.api.auth.data
sealed class LoginFlowResult {
data class Success(
val supportedLoginTypes: List<String>,
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
) : LoginFlowResult()
}
data class LoginFlowResult(
val supportedLoginTypes: List<String>,
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
)

View file

@ -0,0 +1,24 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.auth.registration
import org.matrix.android.sdk.api.failure.Failure
sealed class RegistrationAvailability {
object Available : RegistrationAvailability()
data class NotAvailable(val failure: Failure.ServerError) : RegistrationAvailability()
}

View file

@ -36,6 +36,8 @@ interface RegistrationWizard {
suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult
suspend fun registrationAvailable(userName: String): RegistrationAvailability
val currentThreePid: String?
// True when login and password has been sent with success to the homeserver

View file

@ -65,13 +65,16 @@ fun Throwable.isInvalidUIAAuth(): Boolean {
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/
fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
return if (this is Failure.OtherServerError && httpCode == 401) {
return if (this is Failure.OtherServerError
&& httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */) {
tryOrNull {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(errorBody)
}
} else if (this is Failure.ServerError && httpCode == 401 && error.code == MatrixError.M_FORBIDDEN) {
} else if (this is Failure.ServerError
&& httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& error.code == MatrixError.M_FORBIDDEN) {
// This happens when the submission for this stage was bad (like bad password)
if (error.session != null && error.flows != null) {
RegistrationFlowResponse(
@ -87,3 +90,11 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
null
}
}
fun Throwable.isRegistrationAvailabilityError(): Boolean {
return this is Failure.ServerError
&& httpCode == HttpsURLConnection.HTTP_BAD_REQUEST /* 400 */
&& (error.code == MatrixError.M_USER_IN_USE
|| error.code == MatrixError.M_INVALID_USERNAME
|| error.code == MatrixError.M_EXCLUSIVE)
}

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
@ -26,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
@ -38,22 +36,19 @@ interface RoomService {
/**
* Create a room asynchronously
*/
fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback<String>): Cancelable
suspend fun createRoom(createRoomParams: CreateRoomParams): String
/**
* Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters
*/
fun createDirectRoom(otherUserId: String,
callback: MatrixCallback<String>): Cancelable {
suspend fun createDirectRoom(otherUserId: String): String {
return createRoom(
CreateRoomParams()
.apply {
invitedUserIds.add(otherUserId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = true
},
callback
}
)
}
@ -63,10 +58,9 @@ interface RoomService {
* @param reason optional reason for joining the room
* @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
*/
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable
suspend fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList())
/**
* Get a room from a roomId
@ -112,20 +106,18 @@ interface RoomService {
* Inform the Matrix SDK that a room is displayed.
* The SDK will update the breadcrumbs in the user account data
*/
fun onRoomDisplayed(roomId: String): Cancelable
suspend fun onRoomDisplayed(roomId: String)
/**
* Mark all rooms as read
*/
fun markAllAsRead(roomIds: List<String>,
callback: MatrixCallback<Unit>): Cancelable
suspend fun markAllAsRead(roomIds: List<String>)
/**
* Resolve a room alias to a room ID.
*/
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean,
callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable
suspend fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Optional<RoomAliasDescription>
/**
* Delete a room alias
@ -172,14 +164,14 @@ interface RoomService {
/**
* Get some state events about a room
*/
fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>)
suspend fun getRoomState(roomId: String): List<Event>
/**
* Use this if you want to get information from a room that you are not yet in (or invited)
* It might be possible to get some information on this room if it is public or if guest access is allowed
* This call will try to gather some information on this room, but it could fail and get nothing more
*/
fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>)
suspend fun peekRoom(roomIdOrAlias: String): PeekResult
/**
* TODO Doc

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.internal.auth.data.Availability
import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse
import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams
import org.matrix.android.sdk.internal.auth.data.RiotConfig
@ -34,6 +35,7 @@ import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Url
/**
@ -65,6 +67,12 @@ internal interface AuthAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
suspend fun register(@Body registrationParams: RegistrationParams): Credentials
/**
* Checks to see if a username is available, and valid, for the server.
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/available")
suspend fun registerAvailable(@Query("username") username: String): Availability
/**
* Add 3Pid during registration
* Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928

View file

@ -144,16 +144,14 @@ internal class DefaultAuthenticationService @Inject constructor(
}
return result.fold(
{
if (it is LoginFlowResult.Success) {
// The homeserver exists and up to date, keep the config
// Homeserver url may have been changed, if it was a Riot url
val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = Uri.parse(it.homeServerUrl)
)
// The homeserver exists and up to date, keep the config
// Homeserver url may have been changed, if it was a Riot url
val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = Uri.parse(it.homeServerUrl)
)
pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig)
.also { data -> pendingSessionStore.savePendingSessionData(data) }
}
pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig)
.also { data -> pendingSessionStore.savePendingSessionData(data) }
it
},
{
@ -307,12 +305,12 @@ internal class DefaultAuthenticationService @Inject constructor(
val loginFlowResponse = executeRequest(null) {
authAPI.getLoginFlows()
}
return LoginFlowResult.Success(
loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl,
!versions.isSupportedBySdk()
return LoginFlowResult(
supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl,
isOutdatedHomeserver = !versions.isSupportedBySdk()
)
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class Availability(
/**
* A flag to indicate that the username is available. This should always be true when the server replies with 200 OK.
*/
@Json(name = "available")
val available: Boolean? = null
)

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.auth.registration
import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.toFlowResult
@ -40,9 +41,10 @@ internal class DefaultRegistrationWizard(
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val registerTask = DefaultRegisterTask(authAPI)
private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI)
private val validateCodeTask = DefaultValidateCodeTask(authAPI)
private val registerTask: RegisterTask = DefaultRegisterTask(authAPI)
private val registerAvailableTask: RegisterAvailableTask = DefaultRegisterAvailableTask(authAPI)
private val registerAddThreePidTask: RegisterAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI)
private val validateCodeTask: ValidateCodeTask = DefaultValidateCodeTask(authAPI)
override val currentThreePid: String?
get() {
@ -203,4 +205,8 @@ internal class DefaultRegistrationWizard(
val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
return RegistrationResult.Success(session)
}
override suspend fun registrationAvailable(userName: String): RegistrationAvailability {
return registerAvailableTask.execute(RegisterAvailableTask.Params(userName))
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.auth.registration
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isRegistrationAvailabilityError
import org.matrix.android.sdk.internal.auth.AuthAPI
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
internal interface RegisterAvailableTask : Task<RegisterAvailableTask.Params, RegistrationAvailability> {
data class Params(
val userName: String
)
}
internal class DefaultRegisterAvailableTask(private val authAPI: AuthAPI) : RegisterAvailableTask {
override suspend fun execute(params: RegisterAvailableTask.Params): RegistrationAvailability {
return try {
executeRequest(null) {
authAPI.registerAvailable(params.userName)
}
RegistrationAvailability.Available
} catch (exception: Throwable) {
if (exception.isRegistrationAvailabilityError()) {
RegistrationAvailability.NotAvailable(exception as Failure.ServerError)
} else {
throw exception
}
}
}
}

View file

@ -44,7 +44,7 @@ data class CryptoDeviceInfo(
*/
fun fingerprint(): String? {
return keys
?.takeIf { !deviceId.isBlank() }
?.takeIf { deviceId.isNotBlank() }
?.get("ed25519:$deviceId")
}
@ -53,7 +53,7 @@ data class CryptoDeviceInfo(
*/
fun identityKey(): String? {
return keys
?.takeIf { !deviceId.isBlank() }
?.takeIf { deviceId.isNotBlank() }
?.get("curve25519:$deviceId")
}

View file

@ -103,7 +103,7 @@ data class MXDeviceInfo(
*/
fun fingerprint(): String? {
return keys
?.takeIf { !deviceId.isBlank() }
?.takeIf { deviceId.isNotBlank() }
?.get("ed25519:$deviceId")
}
@ -112,7 +112,7 @@ data class MXDeviceInfo(
*/
fun identityKey(): String? {
return keys
?.takeIf { !deviceId.isBlank() }
?.takeIf { deviceId.isNotBlank() }
?.get("curve25519:$deviceId")
}

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.network
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.getRetryDelay
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil
@ -28,22 +29,21 @@ import java.io.IOException
/**
* Execute a request from the requestBlock and handle some of the Exception it could generate
* Ref: https://github.com/matrix-org/matrix-js-sdk/blob/develop/src/scheduler.js#L138-L175
*
* @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError]
* @param canRetry if set to true, the request will be executed again in case of error, after a delay
* @param initialDelayBeforeRetry the first delay to wait before a request is retried. Will be doubled after each retry
* @param maxDelayBeforeRetry the max delay to wait before a retry
* @param maxRetriesCount the max number of retries
* @param requestBlock a suspend lambda to perform the network request
*/
internal suspend inline fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
canRetry: Boolean = false,
initialDelayBeforeRetry: Long = 100L,
maxDelayBeforeRetry: Long = 10_000L,
maxRetriesCount: Int = Int.MAX_VALUE,
maxDelayBeforeRetry: Long = 32_000L,
maxRetriesCount: Int = 4,
noinline requestBlock: suspend () -> DATA): DATA {
var currentRetryCount = 0
var currentDelay = initialDelayBeforeRetry
var currentDelay = 1_000L
while (true) {
try {
@ -72,9 +72,16 @@ internal suspend inline fun <DATA> executeRequest(globalErrorReceiver: GlobalErr
// }
?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException }
if (canRetry && currentRetryCount++ < maxRetriesCount && exception.shouldBeRetried()) {
// In case of 429, ensure we wait enough
delay(currentDelay.coerceAtLeast(exception.getRetryDelay(0)))
currentRetryCount++
if (exception is Failure.ServerError
&& exception.httpCode == 429
&& exception.error.code == MatrixError.M_LIMIT_EXCEEDED
&& currentRetryCount < maxRetriesCount) {
// 429, we can retry
delay(exception.getRetryDelay(1_000))
} else if (canRetry && currentRetryCount < maxRetriesCount && exception.shouldBeRetried()) {
delay(currentDelay)
currentDelay = currentDelay.times(2L).coerceAtMost(maxDelayBeforeRetry)
// Try again (loop)
} else {

View file

@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService
@ -32,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.asDomain
@ -50,8 +48,6 @@ import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.fetchCopied
import javax.inject.Inject
@ -67,16 +63,11 @@ internal class DefaultRoomService @Inject constructor(
private val peekRoomTask: PeekRoomTask,
private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
) : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
return createRoomTask
.configureWith(createRoomParams) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
return createRoomTask.execute(createRoomParams)
}
override fun getRoom(roomId: String): Room? {
@ -121,34 +112,20 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getBreadcrumbsLive(queryParams)
}
override fun onRoomDisplayed(roomId: String): Cancelable {
return updateBreadcrumbsTask
.configureWith(UpdateBreadcrumbsTask.Params(roomId))
.executeBy(taskExecutor)
override suspend fun onRoomDisplayed(roomId: String) {
updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId))
}
override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>) {
joinRoomTask.execute(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers))
}
override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return markAllRoomsReadTask
.configureWith(MarkAllRoomsReadTask.Params(roomIds)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun markAllAsRead(roomIds: List<String>) {
markAllRoomsReadTask.execute(MarkAllRoomsReadTask.Params(roomIds))
}
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable {
return roomIdByAliasTask
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean): Optional<RoomAliasDescription> {
return roomIdByAliasTask.execute(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer))
}
override suspend fun deleteRoomAlias(roomAlias: String) {
@ -179,19 +156,11 @@ internal class DefaultRoomService @Inject constructor(
}
}
override fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) {
resolveRoomStateTask
.configureWith(ResolveRoomStateTask.Params(roomId)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun getRoomState(roomId: String): List<Event> {
return resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId))
}
override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) {
peekRoomTask
.configureWith(PeekRoomTask.Params(roomIdOrAlias)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun peekRoom(roomIdOrAlias: String): PeekResult {
return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias))
}
}

View file

@ -91,7 +91,7 @@ internal class SendEventWorker(context: Context,
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
return Result.success()
Result.success()
} else {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
Result.retry()

View file

@ -114,11 +114,13 @@ for emoji in emoji_picker_datasource_emojis:
# If additional keywords exist, add them to emoji_picker_datasource_emojis
# Avoid duplicates and keep order. Put official unicode.com keywords first and extend up with emojilib ones.
new_keywords = OrderedDict.fromkeys(emoji_picker_datasource_emojis[emoji]["j"] + emoji_additional_keywords).keys()
new_keywords = OrderedDict.fromkeys(emoji_picker_datasource_emojis[emoji]["j"] + emoji_additional_keywords)
# Remove the ones derived from the unicode name
new_keywords = new_keywords - {emoji.replace("-", "_")} - {emoji.replace("-", " ")} - {emoji_name}
for keyword in [emoji.replace("-", "_")] + [emoji.replace("-", " ")] + [emoji_name]:
if keyword in new_keywords:
new_keywords.pop(keyword)
# Write new keywords back
emoji_picker_datasource_emojis[emoji]["j"] = list(new_keywords)
emoji_picker_datasource_emojis[emoji]["j"] = list(new_keywords.keys())
# Filter out components from unicode 13.1 (as they are not suitable for single-emoji reactions)
emoji_picker_datasource['categories'] = [x for x in emoji_picker_datasource['categories'] if x['id'] != 'component']

View file

@ -14,7 +14,7 @@ kapt {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 1
ext.versionPatch = 4
ext.versionPatch = 7
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@ -297,7 +297,7 @@ dependencies {
def big_image_viewer_version = '1.7.1'
def glide_version = '4.12.0'
def moshi_version = '1.12.0'
def daggerVersion = '2.33'
def daggerVersion = '2.34'
def autofill_version = "1.1.0"
def work_version = '2.5.0'
def arch_version = '2.1.0'
@ -320,13 +320,13 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.sharetarget:sharetarget:1.1.0"
implementation 'androidx.core:core-ktx:1.3.2'
implementation "androidx.media:media:1.2.1"
implementation "androidx.media:media:1.3.0"
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.9.0"
@ -423,7 +423,7 @@ dependencies {
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
// gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:21.0.1') {
gplayImplementation('com.google.firebase:firebase-messaging:21.1.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomdirectory
import im.vector.app.InstrumentedTest
import im.vector.app.core.utils.AssetReader
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class ExplicitTermFilterTest : InstrumentedTest {
private val explicitTermFilter = ExplicitTermFilter(AssetReader(context()))
@Test
fun isValidEmptyTrue() {
explicitTermFilter.isValid("") shouldBe true
}
@Test
fun isValidTrue() {
explicitTermFilter.isValid("Hello") shouldBe true
}
@Test
fun isValidFalse() {
explicitTermFilter.isValid("nsfw") shouldBe false
}
@Test
fun isValidUpCaseFalse() {
explicitTermFilter.isValid("Nsfw") shouldBe false
}
@Test
fun isValidMultilineTrue() {
explicitTermFilter.isValid("Hello\nWorld") shouldBe true
}
@Test
fun isValidMultilineFalse() {
explicitTermFilter.isValid("Hello\nnsfw") shouldBe false
}
@Test
fun isValidMultilineFalse2() {
explicitTermFilter.isValid("nsfw\nHello") shouldBe false
}
@Test
fun isValidAnalFalse() {
explicitTermFilter.isValid("anal") shouldBe false
}
@Test
fun isValidAnal2False() {
explicitTermFilter.isValid("There is some anal in this room") shouldBe false
}
@Test
fun isValidAnalysisTrue() {
explicitTermFilter.isValid("analysis") shouldBe true
}
@Test
fun isValidAnalysis2True() {
explicitTermFilter.isValid("There is some analysis in the room") shouldBe true
}
@Test
fun isValidSpecialCharFalse() {
explicitTermFilter.isValid("18+") shouldBe false
}
@Test
fun isValidSpecialChar2False() {
explicitTermFilter.isValid("This is a room with 18+ content") shouldBe false
}
@Test
fun isValidOtherSpecialCharFalse() {
explicitTermFilter.isValid("strap-on") shouldBe false
}
@Test
fun isValidOtherSpecialChar2False() {
explicitTermFilter.isValid("This is a room with strap-on content") shouldBe false
}
@Test
fun isValid18True() {
explicitTermFilter.isValid("18") shouldBe true
}
@Test
fun isValidLastFalse() {
explicitTermFilter.isValid("zoo") shouldBe false
}
@Test
fun canSearchForFalse() {
explicitTermFilter.canSearchFor("zoo") shouldBe false
}
@Test
fun canSearchForTrue() {
explicitTermFilter.canSearchFor("android") shouldBe true
}
}

View file

@ -78,6 +78,7 @@ class UiAllScreensSanityTest {
// Last passing:
// 2020-11-09
// 2020-12-16 After ViewBinding huge change
// 2021-04-08 Testing 429 change
@Test
fun allScreensTest() {
// Create an account

View file

@ -39,7 +39,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="16sp"
tools:text="@string/verification_emoji_wrench" />
tools:text="@string/verification_emoji_spanner" />
<TextView
android:id="@+id/sas_emoji_text_id"
@ -47,7 +47,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="12sp"
tools:text="verification_emoji_wrench" />
tools:text="verification_emoji_spanner" />
</LinearLayout>

View file

@ -24,9 +24,11 @@ import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.pushers.PushGatewayFailure
import javax.inject.Inject
@ -47,22 +49,26 @@ class TestPushFromPushGateway @Inject constructor(private val context: AppCompat
return
}
action = GlobalScope.launch {
status = runCatching { pushersManager.testPush(fcmToken) }
.fold(
{
// Wait for the push to be received
description = stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_waiting_for_push)
TestStatus.RUNNING
},
{
description = if (it is PushGatewayFailure.PusherRejected) {
stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_failed)
} else {
errorFormatter.toHumanReadable(it)
val result = runCatching { pushersManager.testPush(fcmToken) }
withContext(Dispatchers.Main) {
status = result
.fold(
{
// Wait for the push to be received
description = stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_waiting_for_push)
TestStatus.RUNNING
},
{
description = if (it is PushGatewayFailure.PusherRejected) {
stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_failed)
} else {
errorFormatter.toHumanReadable(it)
}
TestStatus.FAILED
}
TestStatus.FAILED
}
)
)
}
}
}

View file

@ -0,0 +1,68 @@
anal
bbw
bdsm
beast
bestiality
blowjob
bondage
boobs
clit
cock
cuck
cum
cunt
daddy
dick
dildo
erotic
exhibitionism
faggot
femboy
fisting
flogging
fmf
foursome
futa
gangbang
gore
h3ntai
handjob
hentai
incest
jizz
kink
loli
m4f
masturbate
masturbation
mfm
milf
moresome
naked
neet
nsfw
nude
nudity
orgy
pedo
pegging
penis
petplay
porn
pussy
rape
rimming
sadism
sadomasochism
sexy
shota
spank
squirt
strap-on
threesome
vagina
vibrator
voyeur
watersports
xxx
zoo

View file

@ -28,8 +28,10 @@ import com.bumptech.glide.signature.ObjectKey
import im.vector.app.core.extensions.vectorComponent
import im.vector.app.core.files.LocalFilesHelper
import im.vector.app.features.media.ImageContentRenderer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import timber.log.Timber
import java.io.IOException
@ -121,10 +123,12 @@ class VectorGlideDataFetcher(context: Context,
url = data.url,
elementToDecrypt = data.elementToDecrypt)
}
result.fold(
{ callback.onDataReady(it.inputStream()) },
{ callback.onLoadFailed(it as? Exception ?: IOException(it.localizedMessage)) }
)
withContext(Dispatchers.Main) {
result.fold(
{ callback.onDataReady(it.inputStream()) },
{ callback.onLoadFailed(it as? Exception ?: IOException(it.localizedMessage)) }
)
}
}
// val url = contentUrlResolver.resolveFullSize(data.url)
// ?: return

View file

@ -23,7 +23,7 @@ import javax.inject.Inject
class AppNameProvider @Inject constructor(private val context: Context) {
fun getAppName(): String {
try {
return try {
val appPackageName = context.applicationContext.packageName
val pm = context.packageManager
val appInfo = pm.getApplicationInfo(appPackageName, 0)
@ -33,10 +33,10 @@ class AppNameProvider @Inject constructor(private val context: Context) {
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
appName = appPackageName
}
return appName
appName
} catch (e: Exception) {
Timber.e(e, "## AppNameProvider() : failed")
return "ElementAndroid"
"ElementAndroid"
}
}
}

View file

@ -146,7 +146,7 @@ class DialPadFragment : Fragment() {
}
private fun poll() {
if (!input.isEmpty()) {
if (input.isNotEmpty()) {
input = input.substring(0, input.length - 1)
formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
if (formatAsYouType) {

View file

@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.internal.util.awaitCallback
import javax.inject.Inject
class DirectRoomHelper @Inject constructor(
@ -45,9 +44,7 @@ class DirectRoomHelper @Inject constructor(
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
roomId = awaitCallback {
session.createRoom(roomParams, it)
}
roomId = session.createRoom(roomParams)
}
return roomId
}

View file

@ -60,7 +60,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
// TODO maybe check also if
val uid = "kvr_${tx.transactionId}"
when (tx.state) {
is VerificationTxState.OnStarted -> {
is VerificationTxState.OnStarted -> {
// Add a notification for every incoming request
val user = session?.getUser(tx.otherUserId)
val name = user?.getBestName() ?: tx.otherUserId
@ -119,6 +119,13 @@ class IncomingVerificationRequestHandler @Inject constructor(
Timber.v("## SAS verificationRequestCreated ${pr.transactionId}")
// For incoming request we should prompt (if not in activity where this request apply)
if (pr.isIncoming) {
// if it's a self verification for my devices, we can discard the review login alert
// if not this request will be underneath and not visible by the user...
// it will re-appear later
if (pr.otherUserId == session?.myUserId) {
// XXX this is a bit hard coded :/
popupAlertManager.cancelAlert("review_login")
}
val user = session?.getUser(pr.otherUserId)?.toMatrixItem()
val name = user?.getBestName() ?: pr.otherUserId
val description = if (name == pr.otherUserId) {

View file

@ -249,32 +249,34 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
pendingRequest = Loading()
)
}
session.createDirectRoom(otherUserId, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
setState {
copy(
roomId = data,
pendingRequest = Success(
session
.cryptoService()
.verificationService()
.requestKeyVerificationInDMs(
supportedVerificationMethodsProvider.provide(),
otherUserId,
data,
pendingLocalId
)
viewModelScope.launch {
val result = runCatching { session.createDirectRoom(otherUserId) }
result.fold(
{ data ->
setState {
copy(
roomId = data,
pendingRequest = Success(
session
.cryptoService()
.verificationService()
.requestKeyVerificationInDMs(
supportedVerificationMethodsProvider.provide(),
otherUserId,
data,
pendingLocalId
)
)
)
)
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(pendingRequest = Fail(failure))
}
}
})
}
},
{ failure ->
setState {
copy(pendingRequest = Fail(failure))
}
}
)
}
} else {
setState {
copy(

View file

@ -203,9 +203,8 @@ class HomeActivityViewModel @AssistedInject constructor(
_viewEvents.post(
HomeActivityViewEvents.OnNewSession(
session.getUser(session.myUserId)?.toMatrixItem(),
// If it's an old unverified, we should send requests
// instead of waiting for an incoming one
reAuthHelper.data != null
// Always send request instead of waiting for an incoming as per recent EW changes
false
)
)
}

View file

@ -35,7 +35,6 @@ import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.asObservable
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
@ -109,9 +108,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
)
.map { it.roomId }
try {
awaitCallback<Unit> {
session.markAllAsRead(roomIds, it)
}
session.markAllAsRead(roomIds)
} catch (failure: Throwable) {
Timber.d(failure, "Failed to mark all as read")
}

View file

@ -177,14 +177,16 @@ class RoomDetailViewModel @AssistedInject constructor(
observePowerLevel()
updateShowDialerOptionState()
room.getRoomSummaryLive()
viewModelScope.launch {
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
}
// Inform the SDK that the room is displayed
viewModelScope.launch {
try {
room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT)
session.onRoomDisplayed(initialState.roomId)
} catch (_: Exception) {
}
}
// Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId)
callManager.addPstnSupportListener(this)
callManager.checkForPSTNSupportIfNeeded()
chatEffectManager.delegate = this
@ -546,10 +548,7 @@ class RoomDetailViewModel @AssistedInject constructor(
if (trackUnreadMessages.getAndSet(false)) {
mostRecentDisplayedEvent?.root?.eventId?.also {
viewModelScope.launch {
try {
room.setReadMarker(it)
} catch (_: Exception) {
}
tryOrNull { room.setReadMarker(it) }
}
}
mostRecentDisplayedEvent = null
@ -902,19 +901,19 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
session.getRoomSummary(command.roomAlias)
?.roomId
?.let {
_viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it))
}
}
override fun onFailure(failure: Throwable) {
viewModelScope.launch {
try {
session.joinRoom(command.roomAlias, command.reason, emptyList())
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
return@launch
}
})
session.getRoomSummary(command.roomAlias)
?.roomId
?.let {
_viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it))
}
}
}
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
@ -1254,7 +1253,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
viewModelScope.launch {
room.setReadReceipt(eventId)
tryOrNull { room.setReadReceipt(eventId) }
}
}
})
@ -1263,10 +1262,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleMarkAllAsRead() {
viewModelScope.launch {
try {
room.markAsRead(ReadService.MarkAsReadParams.BOTH)
} catch (_: Exception) {
}
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.BOTH) }
}
}

View file

@ -91,18 +91,25 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
SignMode.SignUp -> {
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP
}
SignMode.SignIn,
SignMode.SignInWithMatrixId -> {
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN
}
}.exhaustive
}
}
private fun setupSocialLoginButtons(state: LoginViewState) {
views.loginSocialLoginButtons.mode = when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
SignMode.SignIn,
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
}.exhaustive
}
private fun submit() {
cleanupUi()
@ -277,6 +284,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
setupUi(state)
setupAutoFill(state)
setupSocialLoginButtons(state)
setupButtons(state)
when (state.asyncLoginAction) {

View file

@ -43,7 +43,6 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.FlowResult
@ -773,34 +772,34 @@ class LoginViewModel @AssistedInject constructor(
null
}
if (data is LoginFlowResult.Success) {
// Valid Homeserver, add it to the history.
// Note: we add what the user has input, data.homeServerUrl can be different
rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString())
data ?: return@launch
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO)
&& data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
// Valid Homeserver, add it to the history.
// Note: we add what the user has input, data.homeServerUrl can be different
rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString())
// FIXME We should post a view event here normally?
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported)
|| data.isOutdatedHomeserver) {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
}
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO)
&& data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
// FIXME We should post a view event here normally?
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported)
|| data.isOutdatedHomeserver) {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
}
}
}

View file

@ -31,8 +31,10 @@ import im.vector.lib.attachmentviewer.AttachmentInfo
import im.vector.lib.attachmentviewer.AttachmentSourceProvider
import im.vector.lib.attachmentviewer.ImageLoaderTarget
import im.vector.lib.attachmentviewer.VideoLoaderTarget
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -162,10 +164,12 @@ abstract class BaseAttachmentProvider<Type>(
elementToDecrypt = data.elementToDecrypt
)
}
result.fold(
{ target.onVideoFileReady(info.uid, it) },
{ target.onVideoFileLoadFailed(info.uid) }
)
withContext(Dispatchers.Main) {
result.fold(
{ target.onVideoFileReady(info.uid, it) },
{ target.onVideoFileLoadFailed(info.uid) }
)
}
}
}
}

View file

@ -19,8 +19,10 @@ package im.vector.app.features.media
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.lib.attachmentviewer.AttachmentInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -87,7 +89,9 @@ class DataAttachmentRoomProvider(
elementToDecrypt = item.elementToDecrypt
)
}
callback(result.getOrNull())
withContext(Dispatchers.Main) {
callback(result.getOrNull())
}
}
}
}

View file

@ -19,8 +19,10 @@ package im.vector.app.features.media
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.lib.attachmentviewer.AttachmentInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@ -134,7 +136,9 @@ class RoomEventsAttachmentProvider(
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt())
}
callback(result.getOrNull())
withContext(Dispatchers.Main) {
callback(result.getOrNull())
}
}
}
}

View file

@ -25,8 +25,10 @@ import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.files.LocalFilesHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import timber.log.Timber
@ -83,21 +85,23 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc
url = data.url,
elementToDecrypt = data.elementToDecrypt)
}
result.fold(
{ data ->
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true
withContext(Dispatchers.Main) {
result.fold(
{ data ->
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true
videoView.setVideoPath(data.path)
videoView.start()
},
{
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(it)
}
)
videoView.setVideoPath(data.path)
videoView.start()
},
{
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(it)
}
)
}
}
}
} else {
@ -124,21 +128,23 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc
url = data.url,
elementToDecrypt = null)
}
result.fold(
{ data ->
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true
withContext(Dispatchers.Main) {
result.fold(
{ data ->
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true
videoView.setVideoPath(data.path)
videoView.start()
},
{
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(it)
}
)
videoView.setVideoPath(data.path)
videoView.start()
},
{
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(it)
}
)
}
}
}
}

View file

@ -25,6 +25,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.vectorComponent
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.read.ReadService
@ -78,7 +79,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
val room = session.getRoom(roomId)
if (room != null) {
GlobalScope.launch {
room.join()
tryOrNull { room.join() }
}
}
}
@ -89,7 +90,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
val room = session.getRoom(roomId)
if (room != null) {
GlobalScope.launch {
room.leave()
tryOrNull { room.leave() }
}
}
}
@ -100,10 +101,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
val room = session.getRoom(roomId)
if (room != null) {
GlobalScope.launch {
try {
room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT)
} catch (_: Exception) {
}
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomdirectory
import im.vector.app.core.utils.AssetReader
import javax.inject.Inject
class ExplicitTermFilter @Inject constructor(
assetReader: AssetReader
) {
// List of forbidden terms is in file asset forbidden_terms.txt, in lower case
private val explicitTerms = assetReader.readAssetFile("forbidden_terms.txt")
.orEmpty()
.split("\n")
.map { it.trim() }
.distinct()
.filter { it.isNotEmpty() }
private val explicitContentRegex = explicitTerms
.joinToString(prefix = ".*\\b(", separator = "|", postfix = ")\\b.*")
.toRegex(RegexOption.IGNORE_CASE)
fun canSearchFor(term: String): Boolean {
return term !in explicitTerms && term != "18+"
}
fun isValid(str: String): Boolean {
return explicitContentRegex.matches(str.replace("\n", " ")).not()
// Special treatment for "18+" since word boundaries does not work here
&& str.contains("18+").not()
}
}

View file

@ -31,7 +31,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
@ -42,12 +41,12 @@ import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryDat
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import java.util.Locale
class RoomDirectoryViewModel @AssistedInject constructor(
@Assisted initialState: PublicRoomsViewState,
vectorPreferences: VectorPreferences,
private val session: Session
private val session: Session,
private val explicitTermFilter: ExplicitTermFilter
) : VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, RoomDirectoryViewEvents>(initialState) {
@AssistedFactory
@ -58,11 +57,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(
companion object : MvRxViewModelFactory<RoomDirectoryViewModel, PublicRoomsViewState> {
private const val PUBLIC_ROOMS_LIMIT = 20
// List of forbidden terms, in lower case
private val explicitContentTerms = listOf(
"nsfw"
)
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: PublicRoomsViewState): RoomDirectoryViewModel? {
val activity: RoomDirectoryActivity = (viewModelContext as ActivityViewModelContext).activity()
@ -166,6 +160,17 @@ class RoomDirectoryViewModel @AssistedInject constructor(
}
private fun load(filter: String, roomDirectoryData: RoomDirectoryData) {
if (!showAllRooms && !explicitTermFilter.canSearchFor(filter)) {
setState {
copy(
asyncPublicRoomsRequest = Success(Unit),
publicRooms = emptyList(),
hasMore = false
)
}
return
}
currentJob = viewModelScope.launch {
val data = try {
session.getPublicRooms(roomDirectoryData.homeServer,
@ -202,11 +207,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(
// Filter
val newPublicRooms = data.chunk.orEmpty()
.filter {
showAllRooms
|| "${it.name.orEmpty()} ${it.topic.orEmpty()} ${it.canonicalAlias.orEmpty()}".toLowerCase(Locale.ROOT)
.let { str ->
explicitContentTerms.all { term -> term !in str }
}
showAllRooms || explicitTermFilter.isValid("${it.name.orEmpty()} ${it.topic.orEmpty()}")
}
setState {
@ -232,17 +233,16 @@ class RoomDirectoryViewModel @AssistedInject constructor(
val viaServers = state.roomDirectoryData.homeServer
?.let { listOf(it) }
.orEmpty()
session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
viewModelScope.launch {
try {
session.joinRoom(action.roomId, viaServers = viaServers)
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
// Notify the user
_viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
}
})
}
}
override fun onCleared() {

View file

@ -34,7 +34,6 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
@ -216,19 +215,22 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
}
}
session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
setState {
copy(asyncCreateRoomRequest = Success(data))
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(asyncCreateRoomRequest = Fail(failure))
}
_viewEvents.post(CreateRoomViewEvents.Failure(failure))
}
})
// TODO: Should this be non-cancellable?
viewModelScope.launch {
val result = runCatching { session.createRoom(createRoomParams) }
result.fold(
{ roomId ->
setState {
copy(asyncCreateRoomRequest = Success(roomId))
}
},
{ failure ->
setState {
copy(asyncCreateRoomRequest = Fail(failure))
}
_viewEvents.post(CreateRoomViewEvents.Failure(failure))
}
)
}
}
}

View file

@ -31,7 +31,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.roomdirectory.JoinState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
@ -39,7 +38,6 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
@ -77,9 +75,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
}
viewModelScope.launch(Dispatchers.IO) {
val peekResult = tryOrNull {
awaitCallback<PeekResult> {
session.peekRoom(initialState.roomAlias ?: initialState.roomId, it)
}
session.peekRoom(initialState.roomAlias ?: initialState.roomId)
}
when (peekResult) {
@ -177,15 +173,14 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
session.joinRoom(state.roomId, viaServers = state.homeServers, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
viewModelScope.launch {
try {
session.joinRoom(state.roomId, viaServers = state.homeServers)
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
setState { copy(lastError = failure) }
}
})
}
}
}

View file

@ -20,8 +20,11 @@ import androidx.activity.result.ActivityResultLauncher
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind
import javax.inject.Inject
@ -50,10 +53,12 @@ class TestAccountSettings @Inject constructor(private val stringProvider: String
if (manager?.diagStatus == TestStatus.RUNNING) return // wait before all is finished
GlobalScope.launch {
runCatching {
tryOrNull {
session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled)
}
manager?.retry(activityResultLauncher)
withContext(Dispatchers.Main) {
manager?.retry(activityResultLauncher)
}
}
}
}

View file

@ -33,7 +33,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.login.LoginMode
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
@ -100,21 +99,21 @@ class SoftLogoutViewModel @AssistedInject constructor(
null
}
if (data is LoginFlowResult.Success) {
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO)
&& data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
data ?: return@launch
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
}
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO)
&& data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
}
}
}

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -145,6 +145,7 @@
android:layout_marginBottom="4dp"
android:contentDescription="@string/event_status_a11y_sending"
android:src="@drawable/ic_sending_message"
android:tint="?riotx_text_tertiary"
android:visibility="gone"
tools:visibility="visible" />
@ -158,6 +159,7 @@
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:tint="?riotx_text_tertiary"
android:visibility="gone"
tools:visibility="visible" />

File diff suppressed because one or more lines are too long

View file

@ -198,10 +198,8 @@
<attr name="riotx_text_tertiary" format="color" />
<color name="riotx_text_tertiary_light">#FF8D99A5</color>
<!-- TODO Pick color from Figma, I do not know where to find it -->
<color name="riotx_text_tertiary_dark">#FF8D99A5</color>
<!-- TODO Pick color from Figma, I do not know where to find it -->
<color name="riotx_text_tertiary_black">#FF8D99A5</color>
<color name="riotx_text_tertiary_dark">#FF8E99A4</color>
<color name="riotx_text_tertiary_black">#FF8E99A4</color>
<attr name="riotx_text_primary_body_contrast" format="color" />
<color name="riotx_text_primary_body_contrast_light">#FF61708B</color>