mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 08:12:46 +02:00
Compare commits
145 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
358fcb6b34 | ||
|
92315a4189 | ||
|
156cc1aa4a | ||
|
0d36e9d8a6 | ||
|
13439769a1 | ||
|
bf69810f8f | ||
|
bb9510e59b | ||
|
4b0dfa49f4 | ||
|
6652965e48 | ||
|
b0ff2cb4bb | ||
|
648691656a | ||
|
7eae85a394 | ||
|
123ffe9f9c | ||
|
c48a439eea | ||
|
9d26ba3186 | ||
|
08970ad8c1 | ||
|
79f11ad686 | ||
|
7fa76b9d35 | ||
|
65faedb06b | ||
|
1ceddd9607 | ||
|
42cdb1db11 | ||
|
1c727c1ee4 | ||
|
a4aa38ee43 | ||
|
4a11d028c0 | ||
|
c286f2a744 | ||
|
e2b4899b36 | ||
|
aa82cd2064 | ||
|
bc568343a2 | ||
|
abf0796794 | ||
|
02febfb01b | ||
|
91c98d4bfb | ||
|
cfee6a43fd | ||
|
f14f1db0e0 | ||
|
3feb2d8980 | ||
|
9fc3093c2c | ||
|
7d910f2566 | ||
|
0a0eda3e34 | ||
|
cecef5b8da | ||
|
c9ed95ed21 | ||
|
3dfd6f5a69 | ||
|
8fc1400bab | ||
|
3e4b07cec3 | ||
|
fbb1846694 | ||
|
b435212c87 | ||
|
1108ad5705 | ||
|
f073342954 | ||
|
38b40efac3 | ||
|
e60bda7806 | ||
|
92e60c939d | ||
|
6e4830e325 | ||
|
c6b98f3654 | ||
|
12d54140e5 | ||
|
1de85daad9 | ||
|
050519e998 | ||
|
1af44ce5f7 | ||
|
8d1a36425d | ||
|
4e74b545ad | ||
|
183d6b53bd | ||
|
14562f7285 | ||
|
17bcd680b0 | ||
|
954019547d | ||
|
782635ec8e | ||
|
e609f4a57e | ||
|
907fa35547 | ||
|
00d0c34363 | ||
|
6811d31a6d | ||
|
a464c910f8 | ||
|
d69881f321 | ||
|
efc1f38f8c | ||
|
b9e8da1fbb | ||
|
d2fea275d8 | ||
|
a5af949c15 | ||
|
261b4be287 | ||
|
205fc0d9d6 | ||
|
7699560458 | ||
|
a193b2659d | ||
|
bb85d41f05 | ||
|
9bfe904745 | ||
|
284dc8602f | ||
|
29087d4a87 | ||
|
18649ebddb | ||
|
d5935a13ac | ||
|
670d4dc34e | ||
|
5435a1739e | ||
|
853518fbb2 | ||
|
0b93f34fa0 | ||
|
902a9aa243 | ||
|
5915cebd6d | ||
|
d91ff87fb9 | ||
|
79ef055bfb | ||
|
3a761be6b4 | ||
|
a9e2c31c32 | ||
|
3ac53d20e9 | ||
|
3c18fd5335 | ||
|
f00f34b244 | ||
|
63e0b15f3d | ||
|
80306f20df | ||
|
2972177541 | ||
|
1ad8f47dc1 | ||
|
8527d3f162 | ||
|
99423bacb2 | ||
|
edc6c3dd4f | ||
|
a761a0dbd2 | ||
|
d431ab23c8 | ||
|
f0aa34774e | ||
|
742136abe8 | ||
|
36aba8554d | ||
|
da14ae432a | ||
|
9a01b4ace9 | ||
|
109c1fe482 | ||
|
dbd4525404 | ||
|
c714266a81 | ||
|
8b1701e537 | ||
|
41d1b77370 | ||
|
ac75fe12bf | ||
|
2f26f4b8bb | ||
|
6d82ac7c59 | ||
|
411afb0bf3 | ||
|
57354cbd69 | ||
|
03d51281a2 | ||
|
415511f3e0 | ||
|
e0e778909d | ||
|
b9efc9f4bd | ||
|
872b14373b | ||
|
d28700e2bf | ||
|
18beef14cf | ||
|
e73923dca3 | ||
|
94afd3e66d | ||
|
5f540a5b45 | ||
|
a41617e8aa | ||
|
ff1745b5dc | ||
|
8e3e9876b8 | ||
|
9a4d8f87f6 | ||
|
aef76241a3 | ||
|
0768bd5c88 | ||
|
65333e6031 | ||
|
849e7c613c | ||
|
60169d53d7 | ||
|
4234c27af9 | ||
|
b9eb85e0a6 | ||
|
5373d9aa21 | ||
|
ad4d5e5c02 | ||
|
4ff12605e9 | ||
|
7c561ae622 | ||
|
cec08a20e5 |
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
@@ -18,6 +18,7 @@
|
||||
<w>pbkdf</w>
|
||||
<w>pkcs</w>
|
||||
<w>signin</w>
|
||||
<w>signout</w>
|
||||
<w>signup</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
41
CHANGES.md
41
CHANGES.md
@@ -1,3 +1,44 @@
|
||||
Changes in RiotX 0.11.0 (2019-12-19)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Implement soft logout (#281)
|
||||
|
||||
Improvements 🙌:
|
||||
- Handle navigation to room via room alias (#201)
|
||||
- Open matrix.to link in RiotX (#57)
|
||||
- Limit sticker size in the timeline
|
||||
|
||||
Other changes:
|
||||
- Use same default room colors than Riot-Web
|
||||
|
||||
Bugfix 🐛:
|
||||
- Scroll breadcrumbs to top when opened
|
||||
- Render default room name when it starts with an emoji (#477)
|
||||
- Do not display " (IRC)" in display names https://github.com/vector-im/riot-android/issues/444
|
||||
- Fix rendering issue with HTML formatted body
|
||||
- Disable click on Stickers (#703)
|
||||
|
||||
Build 🧱:
|
||||
- Include diff-match-patch sources as dependency
|
||||
|
||||
Changes in RiotX 0.10.0 (2019-12-10)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Breadcrumbs: switch from one room to another quickly (#571)
|
||||
|
||||
Improvements 🙌:
|
||||
- Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744)
|
||||
|
||||
Other changes:
|
||||
- Add reason for all membership events (https://github.com/matrix-org/matrix-doc/pull/2367)
|
||||
|
||||
Bugfix 🐛:
|
||||
- When automardown is ON, pills are sent as MD in body (#739)
|
||||
- "ban" event are not rendered correctly (#716)
|
||||
- Fix crash when rotating screen in Room timeline
|
||||
|
||||
Changes in RiotX 0.9.1 (2019-12-05)
|
||||
===================================================
|
||||
|
||||
|
@@ -10,7 +10,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
@@ -45,12 +45,6 @@ allprojects {
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://repo.adobe.com/nexus/content/repositories/public/'
|
||||
content {
|
||||
includeGroupByRegex "diff_match_patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).all {
|
||||
|
1
diff-match-patch/.gitignore
vendored
Normal file
1
diff-match-patch/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
8
diff-match-patch/build.gradle
Normal file
8
diff-match-patch/build.gradle
Normal file
@@ -0,0 +1,8 @@
|
||||
apply plugin: 'java-library'
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
sourceCompatibility = "8"
|
||||
targetCompatibility = "8"
|
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ Client request the sign-up flows, once the homeserver is chosen by the user and
|
||||
}
|
||||
```
|
||||
|
||||
We get the flows with a 401, which also means the the registration is possible on this homeserver.
|
||||
We get the flows with a 401, which also means that the registration is possible on this homeserver.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@@ -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.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.CompletableEmitter
|
||||
|
||||
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
completableEmitter.onComplete()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
completableEmitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
|
||||
completableEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
|
||||
fun <T> singleBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Single<T> = Single.create {
|
||||
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
|
||||
override fun onSuccess(data: T) {
|
||||
it.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
it.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
val cancelable = builder(callback)
|
||||
it.setCancellable {
|
||||
cancelable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create {
|
||||
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
|
||||
override fun onSuccess(data: T) {
|
||||
it.onComplete()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
it.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
val cancelable = builder(callback)
|
||||
it.setCancellable {
|
||||
cancelable.cancel()
|
||||
}
|
||||
}
|
@@ -53,12 +53,13 @@ class RxRoom(private val room: Room) {
|
||||
return room.getMyReadReceiptLive().asObservable()
|
||||
}
|
||||
|
||||
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
|
||||
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
|
||||
fun loadRoomMembersIfNeeded(): Single<Unit> = singleBuilder {
|
||||
room.loadRoomMembersIfNeeded(it)
|
||||
}
|
||||
|
||||
fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
fun joinRoom(reason: String? = null,
|
||||
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
|
||||
room.join(reason, viaServers, it)
|
||||
}
|
||||
|
||||
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
|
||||
|
@@ -38,6 +38,10 @@ class RxSession(private val session: Session) {
|
||||
return session.liveGroupSummaries().asObservable()
|
||||
}
|
||||
|
||||
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
|
||||
return session.liveBreadcrumbs().asObservable()
|
||||
}
|
||||
|
||||
fun liveSyncState(): Observable<SyncState> {
|
||||
return session.syncState().asObservable()
|
||||
}
|
||||
@@ -62,18 +66,25 @@ class RxSession(private val session: Session) {
|
||||
return session.livePagedUsers(filter).asObservable()
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
|
||||
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
|
||||
session.createRoom(roomParams, it)
|
||||
}
|
||||
|
||||
fun searchUsersDirectory(search: String,
|
||||
limit: Int,
|
||||
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
|
||||
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
|
||||
excludedUserIds: Set<String>): Single<List<User>> = singleBuilder {
|
||||
session.searchUsersDirectory(search, limit, excludedUserIds, it)
|
||||
}
|
||||
|
||||
fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
fun joinRoom(roomId: String,
|
||||
reason: String? = null,
|
||||
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
|
||||
session.joinRoom(roomId, reason, viaServers, it)
|
||||
}
|
||||
|
||||
fun getRoomIdByAlias(roomAlias: String,
|
||||
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
|
||||
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
sealed class LoginFlowResult {
|
||||
data class Success(
|
||||
val loginFlowResponse: LoginFlowResponse,
|
||||
val isLoginAndRegistrationSupported: Boolean
|
||||
val isLoginAndRegistrationSupported: Boolean,
|
||||
val homeServerUrl: String
|
||||
) : LoginFlowResult()
|
||||
|
||||
object OutdatedHomeserver : LoginFlowResult()
|
||||
|
@@ -22,5 +22,6 @@ package im.vector.matrix.android.api.auth.data
|
||||
*/
|
||||
data class SessionParams(
|
||||
val credentials: Credentials,
|
||||
val homeServerConnectionConfig: HomeServerConnectionConfig
|
||||
val homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
val isTokenValid: Boolean
|
||||
)
|
||||
|
@@ -36,7 +36,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
||||
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
|
||||
// When server send an error, but it cannot be interpreted as a MatrixError
|
||||
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))
|
||||
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
|
||||
|
||||
data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))
|
||||
|
||||
|
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.failure
|
||||
|
||||
// This class will be sent to the bus
|
||||
sealed class GlobalError {
|
||||
data class InvalidToken(val softLogout: Boolean) : GlobalError()
|
||||
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
|
||||
}
|
@@ -22,45 +22,112 @@ import com.squareup.moshi.JsonClass
|
||||
/**
|
||||
* This data class holds the error defined by the matrix specifications.
|
||||
* You shouldn't have to instantiate it.
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MatrixError(
|
||||
/** unique string which can be used to handle an error message */
|
||||
@Json(name = "errcode") val code: String,
|
||||
/** human-readable error message */
|
||||
@Json(name = "error") val message: String,
|
||||
|
||||
// For M_CONSENT_NOT_GIVEN
|
||||
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||
// RESOURCE_LIMIT_EXCEEDED data
|
||||
// For M_RESOURCE_LIMIT_EXCEEDED
|
||||
@Json(name = "limit_type") val limitType: String? = null,
|
||||
@Json(name = "admin_contact") val adminUri: String? = null,
|
||||
// For LIMIT_EXCEEDED
|
||||
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
|
||||
// For M_LIMIT_EXCEEDED
|
||||
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
|
||||
// For M_UNKNOWN_TOKEN
|
||||
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val FORBIDDEN = "M_FORBIDDEN"
|
||||
const val UNKNOWN = "M_UNKNOWN"
|
||||
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||
const val MISSING_TOKEN = "M_MISSING_TOKEN"
|
||||
const val BAD_JSON = "M_BAD_JSON"
|
||||
const val NOT_JSON = "M_NOT_JSON"
|
||||
const val NOT_FOUND = "M_NOT_FOUND"
|
||||
const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||
const val USER_IN_USE = "M_USER_IN_USE"
|
||||
const val ROOM_IN_USE = "M_ROOM_IN_USE"
|
||||
const val BAD_PAGINATION = "M_BAD_PAGINATION"
|
||||
const val UNAUTHORIZED = "M_UNAUTHORIZED"
|
||||
const val OLD_VERSION = "M_OLD_VERSION"
|
||||
const val UNRECOGNIZED = "M_UNRECOGNIZED"
|
||||
/** Forbidden access, e.g. joining a room without permission, failed login. */
|
||||
const val M_FORBIDDEN = "M_FORBIDDEN"
|
||||
/** An unknown error has occurred. */
|
||||
const val M_UNKNOWN = "M_UNKNOWN"
|
||||
/** The access token specified was not recognised. */
|
||||
const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||
/** No access token was specified for the request. */
|
||||
const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
|
||||
/** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
|
||||
const val M_BAD_JSON = "M_BAD_JSON"
|
||||
/** Request did not contain valid JSON. */
|
||||
const val M_NOT_JSON = "M_NOT_JSON"
|
||||
/** No resource was found for this request. */
|
||||
const val M_NOT_FOUND = "M_NOT_FOUND"
|
||||
/** Too many requests have been sent in a short period of time. Wait a while then try again. */
|
||||
const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||
|
||||
const val LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
|
||||
const val THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
||||
// Error code returned by the server when no account matches the given 3pid
|
||||
const val THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
|
||||
const val THREEPID_IN_USE = "M_THREEPID_IN_USE"
|
||||
const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
|
||||
const val TOO_LARGE = "M_TOO_LARGE"
|
||||
/* ==========================================================================================
|
||||
* Other error codes the client might encounter are
|
||||
* ========================================================================================== */
|
||||
|
||||
/** Encountered when trying to register a user ID which has been taken. */
|
||||
const val M_USER_IN_USE = "M_USER_IN_USE"
|
||||
/** Sent when the room alias given to the createRoom API is already in use. */
|
||||
const val M_ROOM_IN_USE = "M_ROOM_IN_USE"
|
||||
/** (Not documented yet) */
|
||||
const val M_BAD_PAGINATION = "M_BAD_PAGINATION"
|
||||
/** The request was not correctly authorized. Usually due to login failures. */
|
||||
const val M_UNAUTHORIZED = "M_UNAUTHORIZED"
|
||||
/** (Not documented yet) */
|
||||
const val M_OLD_VERSION = "M_OLD_VERSION"
|
||||
/** The server did not understand the request. */
|
||||
const val M_UNRECOGNIZED = "M_UNRECOGNIZED"
|
||||
/** (Not documented yet) */
|
||||
const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
|
||||
/** Authentication could not be performed on the third party identifier. */
|
||||
const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
||||
/** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */
|
||||
const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
|
||||
/** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */
|
||||
const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE"
|
||||
/** The client's request used a third party server, eg. identity server, that this server does not trust. */
|
||||
const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
|
||||
/** The request or entity was too large. */
|
||||
const val M_TOO_LARGE = "M_TOO_LARGE"
|
||||
/** (Not documented yet) */
|
||||
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
/** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
|
||||
* a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory
|
||||
* or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach
|
||||
* out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account
|
||||
* data, etc) and not routes which only read state (eg: /sync, get account data, etc). */
|
||||
const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||
/** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */
|
||||
const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED"
|
||||
/** Encountered when trying to register a user ID which is not valid. */
|
||||
const val M_INVALID_USERNAME = "M_INVALID_USERNAME"
|
||||
/** Sent when the initial state given to the createRoom API is invalid. */
|
||||
const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"
|
||||
/** The server does not permit this third party identifier. This may happen if the server only permits,
|
||||
* for example, email addresses from a particular domain. */
|
||||
const val M_THREEPID_DENIED = "M_THREEPID_DENIED"
|
||||
/** The client's request to create a room used a room version that the server does not support. */
|
||||
const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
|
||||
/** The client attempted to join a room that has a version the server does not support.
|
||||
* Inspect the room_version property of the error response for the room's version. */
|
||||
const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
|
||||
/** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */
|
||||
const val M_BAD_STATE = "M_BAD_STATE"
|
||||
/** The room or resource does not permit guests to access it. */
|
||||
const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
|
||||
/** A Captcha is required to complete the request. */
|
||||
const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||
/** The Captcha provided did not match what was expected. */
|
||||
const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||
/** A required parameter was missing from the request. */
|
||||
const val M_MISSING_PARAM = "M_MISSING_PARAM"
|
||||
/** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */
|
||||
const val M_INVALID_PARAM = "M_INVALID_PARAM"
|
||||
/** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */
|
||||
const val M_EXCLUSIVE = "M_EXCLUSIVE"
|
||||
/** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */
|
||||
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
|
||||
/** (Not documented yet) */
|
||||
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
|
||||
// Possible value for "limit_type"
|
||||
const val LIMIT_TYPE_MAU = "monthly_active_user"
|
||||
|
@@ -24,9 +24,7 @@ import android.net.Uri
|
||||
*/
|
||||
sealed class PermalinkData {
|
||||
|
||||
data class EventLink(val roomIdOrAlias: String, val eventId: String) : PermalinkData()
|
||||
|
||||
data class RoomLink(val roomIdOrAlias: String) : PermalinkData()
|
||||
data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData()
|
||||
|
||||
data class UserLink(val userId: String) : PermalinkData()
|
||||
|
||||
|
@@ -60,16 +60,21 @@ object PermalinkParser {
|
||||
return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
return when {
|
||||
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
|
||||
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
|
||||
MatrixPatterns.isRoomId(identifier) -> {
|
||||
if (!extraParameter.isNullOrEmpty() && MatrixPatterns.isEventId(extraParameter)) {
|
||||
PermalinkData.EventLink(roomIdOrAlias = identifier, eventId = extraParameter)
|
||||
} else {
|
||||
PermalinkData.RoomLink(roomIdOrAlias = identifier)
|
||||
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
|
||||
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
|
||||
MatrixPatterns.isRoomId(identifier) -> {
|
||||
val eventId = extraParameter.takeIf {
|
||||
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
|
||||
}
|
||||
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId)
|
||||
}
|
||||
else -> PermalinkData.FallbackLink(uri)
|
||||
MatrixPatterns.isRoomAlias(identifier) -> {
|
||||
val eventId = extraParameter.takeIf {
|
||||
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
|
||||
}
|
||||
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId)
|
||||
}
|
||||
else -> PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import im.vector.matrix.android.api.failure.GlobalError
|
||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
@@ -62,6 +62,11 @@ interface Session :
|
||||
*/
|
||||
val sessionParams: SessionParams
|
||||
|
||||
/**
|
||||
* The session is valid, i.e. it has a valid token so far
|
||||
*/
|
||||
val isOpenable: Boolean
|
||||
|
||||
/**
|
||||
* Useful shortcut to get access to the userId
|
||||
*/
|
||||
@@ -81,7 +86,7 @@ interface Session :
|
||||
|
||||
/**
|
||||
* Launches infinite periodic background syncs
|
||||
* THis does not work in doze mode :/
|
||||
* This does not work in doze mode :/
|
||||
* If battery optimization is on it can work in app standby but that's all :/
|
||||
*/
|
||||
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
|
||||
@@ -136,13 +141,10 @@ interface Session :
|
||||
*/
|
||||
interface Listener {
|
||||
/**
|
||||
* The access token is not valid anymore
|
||||
* Possible cases:
|
||||
* - The access token is not valid anymore,
|
||||
* - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
|
||||
*/
|
||||
fun onInvalidToken()
|
||||
|
||||
/**
|
||||
* A M_CONSENT_NOT_GIVEN error has been received from the homeserver
|
||||
*/
|
||||
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
|
||||
fun onGlobalError(globalError: GlobalError)
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ data class ContentAttachmentData(
|
||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
val name: String? = null,
|
||||
val path: String,
|
||||
val mimeType: String,
|
||||
val mimeType: String?,
|
||||
val type: Type
|
||||
) : Parcelable {
|
||||
|
||||
|
@@ -22,6 +22,8 @@ interface ContentUploadStateTracker {
|
||||
|
||||
fun untrack(key: String, updateListener: UpdateListener)
|
||||
|
||||
fun clear()
|
||||
|
||||
interface UpdateListener {
|
||||
fun onUpdate(state: State)
|
||||
}
|
||||
|
@@ -30,12 +30,16 @@ interface RoomDirectoryService {
|
||||
/**
|
||||
* Get rooms from directory
|
||||
*/
|
||||
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
fun getPublicRooms(server: String?,
|
||||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
*/
|
||||
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
fun joinRoom(roomId: String,
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
|
@@ -21,6 +21,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
|
||||
/**
|
||||
* This interface defines methods to get rooms. It's implemented at the session level.
|
||||
@@ -30,14 +31,17 @@ interface RoomService {
|
||||
/**
|
||||
* Create a room asynchronously
|
||||
*/
|
||||
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable
|
||||
fun createRoom(createRoomParams: CreateRoomParams,
|
||||
callback: MatrixCallback<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
* @param roomId the roomId of the room to join
|
||||
* @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(roomId: String,
|
||||
reason: String? = null,
|
||||
viaServers: List<String> = emptyList(),
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
@@ -54,8 +58,28 @@ interface RoomService {
|
||||
*/
|
||||
fun liveRoomSummaries(): LiveData<List<RoomSummary>>
|
||||
|
||||
/**
|
||||
* Get a live list of Breadcrumbs
|
||||
* @return the [LiveData] of [RoomSummary]
|
||||
*/
|
||||
fun liveBreadcrumbs(): LiveData<List<RoomSummary>>
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Mark all rooms as read
|
||||
*/
|
||||
fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable
|
||||
fun markAllAsRead(roomIds: List<String>,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Resolve a room alias to a room ID.
|
||||
*/
|
||||
fun getRoomIdByAlias(roomAlias: String,
|
||||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
}
|
||||
|
@@ -52,16 +52,20 @@ interface MembershipService {
|
||||
/**
|
||||
* Invite a user in the room
|
||||
*/
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
fun invite(userId: String,
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Join the room, or accept an invitation.
|
||||
*/
|
||||
|
||||
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable
|
||||
fun join(reason: String? = null,
|
||||
viaServers: List<String> = emptyList(),
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Leave the room, or reject an invitation.
|
||||
*/
|
||||
fun leave(callback: MatrixCallback<Unit>): Cancelable
|
||||
fun leave(reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
@@ -26,9 +26,13 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomMember(
|
||||
@Json(name = "membership") val membership: Membership,
|
||||
@Json(name = "reason") val reason: String? = null,
|
||||
@Json(name = "displayname") val displayName: String? = null,
|
||||
@Json(name = "avatar_url") val avatarUrl: String? = null,
|
||||
@Json(name = "is_direct") val isDirect: Boolean = false,
|
||||
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
|
||||
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
|
||||
)
|
||||
) {
|
||||
val safeReason
|
||||
get() = reason?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
@@ -29,6 +29,8 @@ data class RoomSummary(
|
||||
val displayName: String = "",
|
||||
val topic: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val canonicalAlias: String? = null,
|
||||
val aliases: List<String> = emptyList(),
|
||||
val isDirect: Boolean = false,
|
||||
val latestPreviewableEvent: TimelineEvent? = null,
|
||||
val otherMemberIds: List<String> = emptyList(),
|
||||
|
@@ -25,7 +25,7 @@ data class VideoInfo(
|
||||
/**
|
||||
* The mimetype of the video e.g. "video/mp4".
|
||||
*/
|
||||
@Json(name = "mimetype") val mimeType: String,
|
||||
@Json(name = "mimetype") val mimeType: String?,
|
||||
|
||||
/**
|
||||
* The width of the video in pixels.
|
||||
|
@@ -50,6 +50,7 @@ interface RelationService {
|
||||
|
||||
/**
|
||||
* Sends a reaction (emoji) to the targetedEvent.
|
||||
* It has no effect if the user has already added the same reaction to the event.
|
||||
* @param targetEventId the id of the event being reacted
|
||||
* @param reaction the reaction (preferably emoji)
|
||||
*/
|
||||
|
@@ -16,11 +16,12 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
|
||||
/**
|
||||
* Tag class for spans that should mention a user.
|
||||
* These Spans will be transformed into pills when detected in message to send
|
||||
*/
|
||||
interface UserMentionSpan {
|
||||
val displayName: String
|
||||
val userId: String
|
||||
val matrixItem: MatrixItem
|
||||
}
|
||||
|
@@ -104,7 +104,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
root.getClearContent().toModel<MessageStickerContent>()
|
||||
} else {
|
||||
annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
|
||||
|
||||
if (lastMessageContent != null) {
|
||||
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
|
||||
?: lastMessageContent.body
|
||||
?: lastMessageContent.body
|
||||
}
|
||||
|
||||
return null
|
||||
|
@@ -17,14 +17,31 @@
|
||||
package im.vector.matrix.android.api.session.signout
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines a method to sign out. It's implemented at the session level.
|
||||
* This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
|
||||
*/
|
||||
interface SignOutService {
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
* Ask the homeserver for a new access token.
|
||||
* The same deviceId will be used
|
||||
*/
|
||||
fun signOut(callback: MatrixCallback<Unit>)
|
||||
fun signInAgain(password: String,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the session with credentials received after SSO
|
||||
*/
|
||||
fun updateCredentials(credentials: Credentials,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Sign out, and release the session, clear all the session data, including crypto data
|
||||
* @param sigOutFromHomeserver true if the sign out request has to be done
|
||||
*/
|
||||
fun signOut(sigOutFromHomeserver: Boolean,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
@@ -17,10 +17,11 @@
|
||||
package im.vector.matrix.android.api.session.sync
|
||||
|
||||
sealed class SyncState {
|
||||
object IDLE : SyncState()
|
||||
data class RUNNING(val afterPause: Boolean) : SyncState()
|
||||
object PAUSED : SyncState()
|
||||
object KILLING : SyncState()
|
||||
object KILLED : SyncState()
|
||||
object NO_NETWORK : SyncState()
|
||||
object Idle : SyncState()
|
||||
data class Running(val afterPause: Boolean) : SyncState()
|
||||
object Paused : SyncState()
|
||||
object Killing : SyncState()
|
||||
object Killed : SyncState()
|
||||
object NoNetwork : SyncState()
|
||||
object InvalidToken : SyncState()
|
||||
}
|
||||
|
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import java.util.*
|
||||
|
||||
sealed class MatrixItem(
|
||||
open val id: String,
|
||||
open val displayName: String?,
|
||||
open val avatarUrl: String?
|
||||
) {
|
||||
data class UserItem(override val id: String,
|
||||
override val displayName: String? = null,
|
||||
override val avatarUrl: String? = null)
|
||||
: MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
|
||||
init {
|
||||
if (BuildConfig.DEBUG) checkId()
|
||||
}
|
||||
}
|
||||
|
||||
data class EventItem(override val id: String,
|
||||
override val displayName: String? = null,
|
||||
override val avatarUrl: String? = null)
|
||||
: MatrixItem(id, displayName, avatarUrl) {
|
||||
init {
|
||||
if (BuildConfig.DEBUG) checkId()
|
||||
}
|
||||
}
|
||||
|
||||
data class RoomItem(override val id: String,
|
||||
override val displayName: String? = null,
|
||||
override val avatarUrl: String? = null)
|
||||
: MatrixItem(id, displayName, avatarUrl) {
|
||||
init {
|
||||
if (BuildConfig.DEBUG) checkId()
|
||||
}
|
||||
}
|
||||
|
||||
data class RoomAliasItem(override val id: String,
|
||||
override val displayName: String? = null,
|
||||
override val avatarUrl: String? = null)
|
||||
: MatrixItem(id, displayName, avatarUrl) {
|
||||
init {
|
||||
if (BuildConfig.DEBUG) checkId()
|
||||
}
|
||||
}
|
||||
|
||||
data class GroupItem(override val id: String,
|
||||
override val displayName: String? = null,
|
||||
override val avatarUrl: String? = null)
|
||||
: MatrixItem(id, displayName, avatarUrl) {
|
||||
init {
|
||||
if (BuildConfig.DEBUG) checkId()
|
||||
}
|
||||
}
|
||||
|
||||
fun getBestName(): String {
|
||||
return displayName?.takeIf { it.isNotBlank() } ?: id
|
||||
}
|
||||
|
||||
protected fun checkId() {
|
||||
if (!id.startsWith(getIdPrefix())) {
|
||||
error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the prefix as defined in the matrix spec (and not extracted from the id)
|
||||
*/
|
||||
fun getIdPrefix() = when (this) {
|
||||
is UserItem -> '@'
|
||||
is EventItem -> '$'
|
||||
is RoomItem -> '!'
|
||||
is RoomAliasItem -> '#'
|
||||
is GroupItem -> '+'
|
||||
}
|
||||
|
||||
fun firstLetterOfDisplayName(): String {
|
||||
return getBestName()
|
||||
.let { dn ->
|
||||
var startIndex = 0
|
||||
val initial = dn[startIndex]
|
||||
|
||||
if (initial in listOf('@', '#', '+') && dn.length > 1) {
|
||||
startIndex++
|
||||
}
|
||||
|
||||
var length = 1
|
||||
var first = dn[startIndex]
|
||||
|
||||
// LEFT-TO-RIGHT MARK
|
||||
if (dn.length >= 2 && 0x200e == first.toInt()) {
|
||||
startIndex++
|
||||
first = dn[startIndex]
|
||||
}
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) {
|
||||
val second = dn[startIndex + 1]
|
||||
if (second.toInt() in 0xDC00..0xDFFF) {
|
||||
length++
|
||||
}
|
||||
}
|
||||
|
||||
dn.substring(startIndex, startIndex + length)
|
||||
}
|
||||
.toUpperCase(Locale.ROOT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ircPattern = " (IRC)"
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Extensions to create MatrixItem
|
||||
* ========================================================================================== */
|
||||
|
||||
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
|
||||
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
|
||||
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)
|
@@ -20,6 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.Versions
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.auth.data.RiotConfig
|
||||
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
|
||||
import im.vector.matrix.android.internal.auth.registration.*
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
@@ -31,6 +32,12 @@ import retrofit2.http.*
|
||||
*/
|
||||
internal interface AuthAPI {
|
||||
|
||||
/**
|
||||
* Get a Riot config file
|
||||
*/
|
||||
@GET("config.json")
|
||||
fun getRiotConfig(): Call<RiotConfig>
|
||||
|
||||
/**
|
||||
* Get the version information of the homeserver
|
||||
*/
|
||||
|
@@ -53,7 +53,7 @@ internal abstract class AuthModule {
|
||||
.name("matrix-sdk-auth.realm")
|
||||
.modules(AuthRealmModule())
|
||||
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
|
||||
.migration(AuthRealmMigration())
|
||||
.migration(AuthRealmMigration)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
@@ -16,16 +16,19 @@
|
||||
|
||||
package im.vector.matrix.android.internal.auth
|
||||
|
||||
import android.net.Uri
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.api.auth.data.*
|
||||
import im.vector.matrix.android.api.auth.login.LoginWizard
|
||||
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
import im.vector.matrix.android.internal.auth.data.RiotConfig
|
||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
|
||||
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
|
||||
@@ -40,6 +43,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
|
||||
private val okHttpClient: Lazy<OkHttpClient>,
|
||||
@@ -84,7 +88,12 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
|
||||
{
|
||||
if (it is LoginFlowResult.Success) {
|
||||
// The homeserver exists and up to date, keep the config
|
||||
pendingSessionData = PendingSessionData(homeServerConnectionConfig)
|
||||
// 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) }
|
||||
}
|
||||
callback.onSuccess(it)
|
||||
@@ -97,20 +106,71 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
|
||||
.toCancelable()
|
||||
}
|
||||
|
||||
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
|
||||
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
||||
|
||||
// First check the homeserver version
|
||||
runCatching {
|
||||
executeRequest<Versions> {
|
||||
apiCall = authAPI.versions()
|
||||
}
|
||||
}
|
||||
.map { versions ->
|
||||
// Ok, it seems that the homeserver url is valid
|
||||
getLoginFlowResult(authAPI, versions, homeServerConnectionConfig.homeServerUri.toString())
|
||||
}
|
||||
.fold(
|
||||
{
|
||||
it
|
||||
},
|
||||
{
|
||||
if (it is Failure.OtherServerError
|
||||
&& it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) {
|
||||
// It's maybe a Riot url?
|
||||
getRiotLoginFlowInternal(homeServerConnectionConfig)
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
|
||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
||||
|
||||
// First check the homeserver version
|
||||
val versions = executeRequest<Versions> {
|
||||
apiCall = authAPI.versions()
|
||||
// Ok, try to get the config.json file of a RiotWeb client
|
||||
val riotConfig = executeRequest<RiotConfig> {
|
||||
apiCall = authAPI.getRiotConfig()
|
||||
}
|
||||
|
||||
if (versions.isSupportedBySdk()) {
|
||||
if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) {
|
||||
// Ok, good sign, we got a default hs url
|
||||
val newHomeServerConnectionConfig = homeServerConnectionConfig.copy(
|
||||
homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl)
|
||||
)
|
||||
|
||||
val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig)
|
||||
|
||||
val versions = executeRequest<Versions> {
|
||||
apiCall = newAuthAPI.versions()
|
||||
}
|
||||
|
||||
return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl)
|
||||
} else {
|
||||
// Config exists, but there is no default homeserver url (ex: https://riot.im/app)
|
||||
throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult {
|
||||
return if (versions.isSupportedBySdk()) {
|
||||
// Get the login flow
|
||||
val loginFlowResponse = executeRequest<LoginFlowResponse> {
|
||||
apiCall = authAPI.getLoginFlows()
|
||||
}
|
||||
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk())
|
||||
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
|
||||
} else {
|
||||
// Not supported
|
||||
LoginFlowResult.OutdatedHomeserver
|
||||
|
@@ -60,7 +60,8 @@ internal class DefaultSessionCreator @Inject constructor(
|
||||
?.also { Timber.d("Overriding identity server url to $it") }
|
||||
?.let { Uri.parse(it) }
|
||||
?: homeServerConnectionConfig.identityServerUri
|
||||
))
|
||||
),
|
||||
isTokenValid = true)
|
||||
|
||||
sessionParamsStore.save(sessionParams)
|
||||
return sessionManager.getOrCreateSession(sessionParams)
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.auth
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
|
||||
internal interface SessionParamsStore {
|
||||
@@ -28,6 +29,10 @@ internal interface SessionParamsStore {
|
||||
|
||||
suspend fun save(sessionParams: SessionParams)
|
||||
|
||||
suspend fun setTokenInvalid(userId: String)
|
||||
|
||||
suspend fun updateCredentials(newCredentials: Credentials)
|
||||
|
||||
suspend fun delete(userId: String)
|
||||
|
||||
suspend fun deleteAll()
|
||||
|
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.auth.data
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RiotConfig(
|
||||
// There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting
|
||||
// Ex: "brand", "branding", etc.
|
||||
@Json(name = "default_hs_url")
|
||||
val defaultHomeServerUrl: String?
|
||||
)
|
@@ -20,12 +20,10 @@ import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AuthRealmMigration : RealmMigration {
|
||||
internal object AuthRealmMigration : RealmMigration {
|
||||
|
||||
companion object {
|
||||
// Current schema version
|
||||
const val SCHEMA_VERSION = 1L
|
||||
}
|
||||
// Current schema version
|
||||
const val SCHEMA_VERSION = 2L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
|
||||
@@ -46,5 +44,14 @@ internal class AuthRealmMigration : RealmMigration {
|
||||
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
|
||||
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
|
||||
}
|
||||
|
||||
if (oldVersion <= 1) {
|
||||
Timber.d("Step 1 -> 2")
|
||||
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
|
||||
|
||||
realm.schema.get("SessionParamsEntity")
|
||||
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
|
||||
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
@@ -75,6 +76,53 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTokenInvalid(userId: String) {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
val currentSessionParams = realm
|
||||
.where(SessionParamsEntity::class.java)
|
||||
.equalTo(SessionParamsEntityFields.USER_ID, userId)
|
||||
.findAll()
|
||||
.firstOrNull()
|
||||
|
||||
if (currentSessionParams == null) {
|
||||
// Should not happen
|
||||
"Session param not found for user $userId"
|
||||
.let { Timber.w(it) }
|
||||
.also { error(it) }
|
||||
} else {
|
||||
currentSessionParams.isTokenValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateCredentials(newCredentials: Credentials) {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
val currentSessionParams = realm
|
||||
.where(SessionParamsEntity::class.java)
|
||||
.equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId)
|
||||
.findAll()
|
||||
.map { mapper.map(it) }
|
||||
.firstOrNull()
|
||||
|
||||
if (currentSessionParams == null) {
|
||||
// Should not happen
|
||||
"Session param not found for user ${newCredentials.userId}"
|
||||
.let { Timber.w(it) }
|
||||
.also { error(it) }
|
||||
} else {
|
||||
val newSessionParams = currentSessionParams.copy(
|
||||
credentials = newCredentials,
|
||||
isTokenValid = true
|
||||
)
|
||||
|
||||
val entity = mapper.map(newSessionParams)
|
||||
if (entity != null) {
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(userId: String) {
|
||||
awaitTransaction(realmConfiguration) {
|
||||
it.where(SessionParamsEntity::class.java)
|
||||
|
@@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
|
||||
internal open class SessionParamsEntity(
|
||||
@PrimaryKey var userId: String = "",
|
||||
var credentialsJson: String = "",
|
||||
var homeServerConnectionConfigJson: String = ""
|
||||
var homeServerConnectionConfigJson: String = "",
|
||||
// Set to false when the token is invalid and the user has been soft logged out
|
||||
// In case of hard logout, this object is deleted from DB
|
||||
var isTokenValid: Boolean = true
|
||||
) : RealmObject()
|
||||
|
@@ -36,7 +36,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||
if (credentials == null || homeServerConnectionConfig == null) {
|
||||
return null
|
||||
}
|
||||
return SessionParams(credentials, homeServerConnectionConfig)
|
||||
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
|
||||
}
|
||||
|
||||
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
|
||||
@@ -48,6 +48,10 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||
if (credentialsJson == null || homeServerConnectionConfigJson == null) {
|
||||
return null
|
||||
}
|
||||
return SessionParamsEntity(sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson)
|
||||
return SessionParamsEntity(
|
||||
sessionParams.credentials.userId,
|
||||
credentialsJson,
|
||||
homeServerConnectionConfigJson,
|
||||
sessionParams.isTokenValid)
|
||||
}
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ object MXEncryptedAttachments {
|
||||
* @param mimetype the mime type
|
||||
* @return the encryption file info
|
||||
*/
|
||||
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult {
|
||||
fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult {
|
||||
val t0 = System.currentTimeMillis()
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
|
@@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError
|
||||
&& failure.error.code == MatrixError.NOT_FOUND) {
|
||||
&& failure.error.code == MatrixError.M_NOT_FOUND) {
|
||||
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
||||
callback.onSuccess(null)
|
||||
} else {
|
||||
@@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError
|
||||
&& failure.error.code == MatrixError.NOT_FOUND) {
|
||||
&& failure.error.code == MatrixError.M_NOT_FOUND) {
|
||||
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
||||
callback.onSuccess(null)
|
||||
} else {
|
||||
@@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
|
||||
Timber.e(failure, "backupKeys: backupKeys failed.")
|
||||
|
||||
when (failure.error.code) {
|
||||
MatrixError.NOT_FOUND,
|
||||
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
|
||||
MatrixError.M_NOT_FOUND,
|
||||
MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
|
||||
// Backup has been deleted on the server, or we are not using the last backup version
|
||||
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
|
||||
backupAllGroupSessionsCallback?.onFailure(failure)
|
||||
|
@@ -19,16 +19,14 @@ package im.vector.matrix.android.internal.crypto.store.db.query
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
/**
|
||||
* Get or create a room
|
||||
*/
|
||||
internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity {
|
||||
return getById(realm, roomId)
|
||||
?: let {
|
||||
realm.createObject(CryptoRoomEntity::class.java, roomId)
|
||||
}
|
||||
return getById(realm, roomId) ?: realm.createObject(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,18 +20,20 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
/**
|
||||
* Get or create a device info
|
||||
*/
|
||||
internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity {
|
||||
val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId)
|
||||
|
||||
return realm.where<DeviceInfoEntity>()
|
||||
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
|
||||
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
?: let {
|
||||
realm.createObject(DeviceInfoEntity::class.java, DeviceInfoEntity.createPrimaryKey(userId, deviceId)).apply {
|
||||
this.deviceId = deviceId
|
||||
}
|
||||
}
|
||||
?: realm.createObject<DeviceInfoEntity>(key)
|
||||
.apply {
|
||||
this.deviceId = deviceId
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db.query
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
/**
|
||||
@@ -28,9 +29,7 @@ internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): Use
|
||||
return realm.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
?: let {
|
||||
realm.createObject(UserEntity::class.java, userId)
|
||||
}
|
||||
?: realm.createObject(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -68,7 +68,9 @@ internal class RoomSummaryMapper @Inject constructor(
|
||||
membership = roomSummaryEntity.membership,
|
||||
versioningState = roomSummaryEntity.versioningState,
|
||||
readMarkerId = roomSummaryEntity.readMarkerId,
|
||||
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList()
|
||||
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
|
||||
canonicalAlias = roomSummaryEntity.canonicalAlias,
|
||||
aliases = roomSummaryEntity.aliases.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.database.model
|
||||
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
|
||||
internal open class BreadcrumbsEntity(
|
||||
var recentRoomIds: RealmList<String> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
}
|
@@ -38,7 +38,11 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
||||
var readMarkerId: String? = null,
|
||||
var hasUnreadMessages: Boolean = false,
|
||||
var tags: RealmList<RoomTagEntity> = RealmList(),
|
||||
var userDrafts: UserDraftsEntity? = null
|
||||
var userDrafts: UserDraftsEntity? = null,
|
||||
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||
var canonicalAlias: String? = null,
|
||||
var aliases: RealmList<String> = RealmList(),
|
||||
var flatAliases: String = ""
|
||||
) : RealmObject() {
|
||||
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
@@ -59,5 +63,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
||||
versioningStateStr = value.name
|
||||
}
|
||||
|
||||
companion object
|
||||
companion object {
|
||||
const val NOT_IN_BREADCRUMBS = -1
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
|
||||
SyncEntity::class,
|
||||
UserEntity::class,
|
||||
IgnoredUserEntity::class,
|
||||
BreadcrumbsEntity::class,
|
||||
EventAnnotationsSummaryEntity::class,
|
||||
ReactionAggregatedSummaryEntity::class,
|
||||
EditAggregatedSummaryEntity::class,
|
||||
|
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.database.query
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? {
|
||||
return realm.where<BreadcrumbsEntity>().findFirst()
|
||||
}
|
||||
|
||||
internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity {
|
||||
return get(realm) ?: realm.createObject()
|
||||
}
|
@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> {
|
||||
@@ -28,6 +29,5 @@ internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): Rea
|
||||
}
|
||||
|
||||
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
|
||||
return where(realm, roomId).findFirst()
|
||||
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
|
||||
return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
|
||||
@@ -44,10 +45,11 @@ internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId
|
||||
|
||||
internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
|
||||
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
|
||||
?: realm.createObject(ReadReceiptEntity::class.java, buildPrimaryKey(roomId, userId)).apply {
|
||||
this.roomId = roomId
|
||||
this.userId = userId
|
||||
}
|
||||
?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId))
|
||||
.apply {
|
||||
this.roomId = roomId
|
||||
this.userId = userId
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"
|
||||
|
@@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
|
||||
@@ -31,9 +32,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
|
||||
return query
|
||||
}
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? {
|
||||
val roomSummary = realm.where<RoomSummaryEntity>()
|
||||
.equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias)
|
||||
.findFirst()
|
||||
if (roomSummary != null) {
|
||||
return roomSummary
|
||||
}
|
||||
return realm.where<RoomSummaryEntity>()
|
||||
.contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias")
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
|
||||
return where(realm, roomId).findFirst()
|
||||
?: realm.createObject(RoomSummaryEntity::class.java, roomId)
|
||||
return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
}
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
|
||||
|
@@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
|
||||
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.UserAccountDataFallback
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
|
||||
@@ -34,6 +35,7 @@ object MoshiProvider {
|
||||
.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)
|
||||
.registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT)
|
||||
|
@@ -16,19 +16,29 @@
|
||||
|
||||
package im.vector.matrix.android.internal.network
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AccessTokenInterceptor @Inject constructor(private val credentials: Credentials) : Interceptor {
|
||||
internal class AccessTokenInterceptor @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val sessionParamsStore: SessionParamsStore) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
val newRequestBuilder = request.newBuilder()
|
||||
// Add the access token to all requests if it is set
|
||||
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken)
|
||||
request = newRequestBuilder.build()
|
||||
|
||||
accessToken?.let {
|
||||
val newRequestBuilder = request.newBuilder()
|
||||
// Add the access token to all requests if it is set
|
||||
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
|
||||
request = newRequestBuilder.build()
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private val accessToken
|
||||
get() = sessionParamsStore.get(userId)?.credentials?.accessToken
|
||||
}
|
||||
|
@@ -19,8 +19,9 @@
|
||||
package im.vector.matrix.android.internal.network
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import com.squareup.moshi.JsonEncodingException
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.GlobalError
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
@@ -31,6 +32,7 @@ import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@@ -98,7 +100,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
|
||||
if (matrixError != null) {
|
||||
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
|
||||
// Also send this error to the bus, for a global management
|
||||
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri))
|
||||
EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
|
||||
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
|
||||
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
|
||||
// Also send this error to the bus, for a global management
|
||||
EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
|
||||
}
|
||||
|
||||
return Failure.ServerError(matrixError, httpCode)
|
||||
@@ -106,6 +112,9 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
|
||||
} catch (ex: JsonDataException) {
|
||||
// This is not a MatrixError
|
||||
Timber.w("The error returned by the server is not a MatrixError")
|
||||
} catch (ex: JsonEncodingException) {
|
||||
// This is not a MatrixError, HTML code?
|
||||
Timber.w("The error returned by the server is not a MatrixError, probably HTML string")
|
||||
}
|
||||
|
||||
return Failure.OtherServerError(errorBodyStr, httpCode)
|
||||
|
@@ -83,15 +83,13 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
|
||||
|
||||
if (elementToDecrypt != null) {
|
||||
Timber.v("## decrypt file")
|
||||
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error")
|
||||
} else {
|
||||
inputStream
|
||||
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||
?: throw IllegalStateException("Decryption error")
|
||||
}
|
||||
|
||||
writeToFile(inputStream, destFile)
|
||||
destFile
|
||||
}
|
||||
.map { inputStream ->
|
||||
writeToFile(inputStream, destFile)
|
||||
destFile
|
||||
}
|
||||
} else {
|
||||
Try.just(destFile)
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import im.vector.matrix.android.api.failure.GlobalError
|
||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
@@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.session.sync.job.SyncThread
|
||||
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
@@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
||||
private val secureStorageService: Lazy<SecureStorageService>,
|
||||
private val syncThreadProvider: Provider<SyncThread>,
|
||||
private val contentUrlResolver: ContentUrlResolver,
|
||||
private val sessionParamsStore: SessionParamsStore,
|
||||
private val contentUploadProgressTracker: ContentUploadStateTracker,
|
||||
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
|
||||
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
|
||||
@@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
||||
|
||||
private var syncThread: SyncThread? = null
|
||||
|
||||
override val isOpenable: Boolean
|
||||
get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
|
||||
|
||||
@MainThread
|
||||
override fun open() {
|
||||
assertMainThread()
|
||||
@@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
|
||||
sessionListeners.dispatchConsentNotGiven(consentNotGivenError)
|
||||
fun onGlobalError(globalError: GlobalError) {
|
||||
if (globalError is GlobalError.InvalidToken
|
||||
&& globalError.softLogout) {
|
||||
// Mark the token has invalid
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
sessionParamsStore.setTokenInvalid(myUserId)
|
||||
}
|
||||
}
|
||||
|
||||
sessionListeners.dispatchGlobalError(globalError)
|
||||
}
|
||||
|
||||
override fun contentUrlResolver() = contentUrlResolver
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session
|
||||
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import im.vector.matrix.android.api.failure.GlobalError
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) {
|
||||
fun dispatchGlobalError(globalError: GlobalError) {
|
||||
synchronized(listeners) {
|
||||
listeners.forEach {
|
||||
it.onConsentNotGivenError(consentNotGivenError)
|
||||
it.onGlobalError(globalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -42,6 +42,10 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
listeners.clear()
|
||||
}
|
||||
|
||||
internal fun setFailure(key: String, throwable: Throwable) {
|
||||
val failure = ContentUploadStateTracker.State.Failure(throwable)
|
||||
updateState(key, failure)
|
||||
|
@@ -43,9 +43,9 @@ internal class FileUploader @Inject constructor(@Authenticated
|
||||
|
||||
suspend fun uploadFile(file: File,
|
||||
filename: String?,
|
||||
mimeType: String,
|
||||
mimeType: String?,
|
||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||
val uploadBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
||||
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
|
||||
return upload(uploadBody, filename, progressListener)
|
||||
}
|
||||
|
||||
|
@@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return joinRoomTask
|
||||
.configureWith(JoinRoomTask.Params(roomId)) {
|
||||
.configureWith(JoinRoomTask.Params(roomId, reason)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
|
@@ -25,14 +25,17 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.VersioningState
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.Realm
|
||||
@@ -43,6 +46,8 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
||||
private val createRoomTask: CreateRoomTask,
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
||||
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
|
||||
private val roomIdByAliasTask: GetRoomIdByAliasTask,
|
||||
private val roomFactory: RoomFactory,
|
||||
private val taskExecutor: TaskExecutor) : RoomService {
|
||||
|
||||
@@ -75,9 +80,28 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
||||
)
|
||||
}
|
||||
|
||||
override fun joinRoom(roomId: String, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override fun liveBreadcrumbs(): LiveData<List<RoomSummary>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ realm ->
|
||||
RoomSummaryEntity.where(realm)
|
||||
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
|
||||
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
|
||||
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
|
||||
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
|
||||
},
|
||||
{ roomSummaryMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRoomDisplayed(roomId: String): Cancelable {
|
||||
return updateBreadcrumbsTask
|
||||
.configureWith(UpdateBreadcrumbsTask.Params(roomId))
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun joinRoom(roomId: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return joinRoomTask
|
||||
.configureWith(JoinRoomTask.Params(roomId, viaServers)) {
|
||||
.configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@@ -90,4 +114,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
return roomIdByAliasTask
|
||||
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room
|
||||
|
||||
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.internal.session.room.alias.RoomAliasDescription
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
@@ -217,7 +218,7 @@ internal interface RoomAPI {
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
|
||||
fun join(@Path("roomId") roomId: String,
|
||||
@Query("server_name") viaServers: List<String>,
|
||||
@Body params: Map<String, String>): Call<Unit>
|
||||
@Body params: Map<String, String?>): Call<Unit>
|
||||
|
||||
/**
|
||||
* Leave the given room.
|
||||
@@ -227,7 +228,7 @@ internal interface RoomAPI {
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
|
||||
fun leave(@Path("roomId") roomId: String,
|
||||
@Body params: Map<String, String>): Call<Unit>
|
||||
@Body params: Map<String, String?>): Call<Unit>
|
||||
|
||||
/**
|
||||
* Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room.
|
||||
@@ -258,4 +259,12 @@ internal interface RoomAPI {
|
||||
fun reportContent(@Path("roomId") roomId: String,
|
||||
@Path("eventId") eventId: String,
|
||||
@Body body: ReportContentBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Get the room ID associated to the room alias.
|
||||
*
|
||||
* @param roomAlias the room alias.
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
||||
}
|
||||
|
@@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.internal.session.DefaultFileService
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask
|
||||
@@ -133,4 +135,7 @@ internal abstract class RoomModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ import com.zhuinden.monarchy.Monarchy
|
||||
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.Membership
|
||||
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.RoomTopicContent
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
@@ -68,7 +70,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
||||
updateMembers: Boolean = false) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
if (roomSummary != null) {
|
||||
if (roomSummary.heroes.isNotEmpty()) {
|
||||
@@ -91,15 +93,24 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
|
||||
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
|
||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
|
||||
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
|
||||
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
|
||||
|
||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
|
||||
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
||||
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
|
||||
?.canonicalAlias
|
||||
|
||||
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList()
|
||||
roomSummaryEntity.aliases.clear()
|
||||
roomSummaryEntity.aliases.addAll(roomAliases)
|
||||
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
||||
|
||||
if (updateMembers) {
|
||||
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||
|
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.alias
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.findByAlias
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import io.realm.Realm
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
|
||||
data class Params(
|
||||
val roomAlias: String,
|
||||
val searchOnServer: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultGetRoomIdByAliasTask @Inject constructor(private val monarchy: Monarchy,
|
||||
private val roomAPI: RoomAPI) : GetRoomIdByAliasTask {
|
||||
|
||||
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> {
|
||||
var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
|
||||
}
|
||||
return if (roomId != null) {
|
||||
Optional.from(roomId)
|
||||
} else if (!params.searchOnServer) {
|
||||
Optional.from<String>(null)
|
||||
} else {
|
||||
roomId = executeRequest<RoomAliasDescription> {
|
||||
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
|
||||
}.roomId
|
||||
Optional.from(roomId)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.alias
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class RoomAliasDescription(
|
||||
/**
|
||||
* The room ID for this alias.
|
||||
*/
|
||||
@Json(name = "room_id") val roomId: String,
|
||||
|
||||
/**
|
||||
* A list of servers that are aware of this room ID.
|
||||
*/
|
||||
@Json(name = "servers") val servers: List<String> = emptyList()
|
||||
)
|
@@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
|
||||
apiCall = roomAPI.createRoom(params)
|
||||
}
|
||||
val roomId = createRoomResponse.roomId!!
|
||||
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
|
||||
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
|
||||
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
|
||||
realm.where(RoomEntity::class.java)
|
||||
.equalTo(RoomEntityFields.ROOM_ID, roomId)
|
||||
|
@@ -83,8 +83,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
|
||||
return result
|
||||
}
|
||||
|
||||
override fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = InviteTask.Params(roomId, userId)
|
||||
override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = InviteTask.Params(roomId, userId, reason)
|
||||
return inviteTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
@@ -92,8 +92,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = JoinRoomTask.Params(roomId, viaServers)
|
||||
override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = JoinRoomTask.Params(roomId, reason, viaServers)
|
||||
return joinTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
@@ -101,8 +101,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun leave(callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = LeaveRoomTask.Params(roomId)
|
||||
override fun leave(reason: String?, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = LeaveRoomTask.Params(roomId, reason)
|
||||
return leaveRoomTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
|
@@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InviteBody(
|
||||
@Json(name = "user_id") val userId: String
|
||||
@Json(name = "user_id") val userId: String,
|
||||
@Json(name = "reason") val reason: String?
|
||||
)
|
||||
|
@@ -24,7 +24,8 @@ import javax.inject.Inject
|
||||
internal interface InviteTask : Task<InviteTask.Params, Unit> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val userId: String
|
||||
val userId: String,
|
||||
val reason: String?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ internal class DefaultInviteTask @Inject constructor(private val roomAPI: RoomAP
|
||||
|
||||
override suspend fun execute(params: InviteTask.Params) {
|
||||
return executeRequest {
|
||||
val body = InviteBody(params.userId)
|
||||
val body = InviteBody(params.userId, params.reason)
|
||||
apiCall = roomAPI.invite(params.roomId, body)
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ import javax.inject.Inject
|
||||
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val reason: String?,
|
||||
val viaServers: List<String> = emptyList()
|
||||
)
|
||||
}
|
||||
@@ -43,7 +44,7 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
|
||||
|
||||
override suspend fun execute(params: JoinRoomTask.Params) {
|
||||
executeRequest<Unit> {
|
||||
apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap())
|
||||
apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason))
|
||||
}
|
||||
val roomId = params.roomId
|
||||
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
|
||||
|
@@ -23,7 +23,8 @@ import javax.inject.Inject
|
||||
|
||||
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
|
||||
data class Params(
|
||||
val roomId: String
|
||||
val roomId: String,
|
||||
val reason: String?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ internal class DefaultLeaveRoomTask @Inject constructor(private val roomAPI: Roo
|
||||
|
||||
override suspend fun execute(params: LeaveRoomTask.Params) {
|
||||
return executeRequest {
|
||||
apiCall = roomAPI.leave(params.roomId, HashMap())
|
||||
apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -30,10 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.NoOpCancellable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
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.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
|
||||
@@ -44,6 +47,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEvent
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.CancelableWork
|
||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
private val cryptoService: CryptoService,
|
||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor)
|
||||
: RelationService {
|
||||
@@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
}
|
||||
|
||||
override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
|
||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||
.also { saveLocalEcho(it) }
|
||||
val sendRelationWork = createSendEventWork(event, true)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
|
||||
return CancelableWork(context, sendRelationWork.id)
|
||||
return if (monarchy
|
||||
.fetchCopyMap(
|
||||
{ realm ->
|
||||
TimelineEventEntity.where(realm, roomId, targetEventId).findFirst()
|
||||
},
|
||||
{ entity, _ ->
|
||||
timelineEventMapper.map(entity)
|
||||
})
|
||||
?.annotations
|
||||
?.reactionsSummary
|
||||
.orEmpty()
|
||||
.none { it.addedByMe && it.key == reaction }) {
|
||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||
.also { saveLocalEcho(it) }
|
||||
val sendRelationWork = createSendEventWork(event, true)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
|
||||
CancelableWork(context, sendRelationWork.id)
|
||||
} else {
|
||||
Timber.w("Reaction already added")
|
||||
NoOpCancellable
|
||||
}
|
||||
}
|
||||
|
||||
override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
|
||||
|
@@ -78,7 +78,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
val htmlText = renderer.render(document)
|
||||
|
||||
if (isFormattedTextPertinent(source, htmlText)) {
|
||||
return TextContent(source, htmlText)
|
||||
return TextContent(text.toString(), htmlText)
|
||||
}
|
||||
} else {
|
||||
// Try to detect pills
|
||||
@@ -251,7 +251,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
type = MessageType.MSGTYPE_AUDIO,
|
||||
body = attachment.name ?: "audio",
|
||||
audioInfo = AudioInfo(
|
||||
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||
size = attachment.size
|
||||
),
|
||||
url = attachment.path
|
||||
@@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
type = MessageType.MSGTYPE_FILE,
|
||||
body = attachment.name ?: "file",
|
||||
info = FileInfo(
|
||||
mimeType = attachment.mimeType.takeIf { it.isNotBlank() }
|
||||
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
|
||||
?: "application/octet-stream",
|
||||
size = attachment.size
|
||||
),
|
||||
|
@@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
||||
|
||||
private fun Throwable.shouldBeRetried(): Boolean {
|
||||
return this is Failure.NetworkConnection
|
||||
|| (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED)
|
||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||
}
|
||||
|
||||
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
|
||||
|
@@ -65,7 +65,7 @@ internal class TextPillsUtils @Inject constructor(
|
||||
// append text before pill
|
||||
append(text, currIndex, start)
|
||||
// append the pill
|
||||
append(String.format(template, urlSpan.userId, urlSpan.displayName))
|
||||
append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
|
||||
currIndex = end
|
||||
}
|
||||
// append text after the last pill
|
||||
|
@@ -289,6 +289,9 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
|
||||
if (listeners.contains(listener)) {
|
||||
return false
|
||||
}
|
||||
listeners.add(listener).also {
|
||||
postSnapshot()
|
||||
}
|
||||
@@ -494,9 +497,9 @@ internal class DefaultTimeline(
|
||||
return
|
||||
}
|
||||
val params = PaginationTask.Params(roomId = roomId,
|
||||
from = token,
|
||||
direction = direction.toPaginationDirection(),
|
||||
limit = limit)
|
||||
from = token,
|
||||
direction = direction.toPaginationDirection(),
|
||||
limit = limit)
|
||||
|
||||
Timber.v("Should fetch $limit items $direction")
|
||||
cancelableBag += paginationTask
|
||||
@@ -571,7 +574,7 @@ internal class DefaultTimeline(
|
||||
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||
|
||||
if (timelineEvent.isEncrypted()
|
||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
||||
}
|
||||
|
||||
|
@@ -17,17 +17,43 @@
|
||||
package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
|
||||
private val signInAgainTask: SignInAgainTask,
|
||||
private val sessionParamsStore: SessionParamsStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor) : SignOutService {
|
||||
|
||||
override fun signOut(callback: MatrixCallback<Unit>) {
|
||||
signOutTask
|
||||
.configureWith {
|
||||
override fun signInAgain(password: String,
|
||||
callback: MatrixCallback<Unit>): Cancelable {
|
||||
return signInAgainTask
|
||||
.configureWith(SignInAgainTask.Params(password)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun updateCredentials(credentials: Credentials,
|
||||
callback: MatrixCallback<Unit>): Cancelable {
|
||||
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
sessionParamsStore.updateCredentials(credentials)
|
||||
}
|
||||
}
|
||||
|
||||
override fun signOut(sigOutFromHomeserver: Boolean,
|
||||
callback: MatrixCallback<Unit>): Cancelable {
|
||||
return signOutTask
|
||||
.configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
|
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SignInAgainTask : Task<SignInAgainTask.Params, Unit> {
|
||||
data class Params(
|
||||
val password: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSignInAgainTask @Inject constructor(
|
||||
private val signOutAPI: SignOutAPI,
|
||||
private val sessionParams: SessionParams,
|
||||
private val sessionParamsStore: SessionParamsStore) : SignInAgainTask {
|
||||
|
||||
override suspend fun execute(params: SignInAgainTask.Params) {
|
||||
val newCredentials = executeRequest<Credentials> {
|
||||
apiCall = signOutAPI.loginAgain(
|
||||
PasswordLoginParams.userIdentifier(
|
||||
// Reuse the same userId
|
||||
sessionParams.credentials.userId,
|
||||
params.password,
|
||||
// The spec says the initial device name will be ignored
|
||||
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
|
||||
// but https://github.com/matrix-org/synapse/issues/6525
|
||||
// Reuse the same deviceId
|
||||
deviceId = sessionParams.credentials.deviceId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
sessionParamsStore.updateCredentials(newCredentials)
|
||||
}
|
||||
}
|
@@ -16,12 +16,27 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
internal interface SignOutAPI {
|
||||
|
||||
/**
|
||||
* Attempt to login again to the same account.
|
||||
* Set all the timeouts to 1 minute
|
||||
* It is similar to [AuthAPI.login]
|
||||
*
|
||||
* @param loginParams the login parameters
|
||||
*/
|
||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||
fun loginAgain(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
||||
|
||||
/**
|
||||
* Invalidate the access token, so that it can no longer be used for authorization.
|
||||
*/
|
||||
|
@@ -37,8 +37,11 @@ internal abstract class SignOutModule {
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask
|
||||
abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService
|
||||
abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService
|
||||
}
|
||||
|
@@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||
@@ -32,9 +34,14 @@ import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SignOutTask : Task<Unit, Unit>
|
||||
internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
|
||||
data class Params(
|
||||
val sigOutFromHomeserver: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSignOutTask @Inject constructor(private val context: Context,
|
||||
@UserId private val userId: String,
|
||||
@@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
|
||||
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
|
||||
@UserMd5 private val userMd5: String) : SignOutTask {
|
||||
|
||||
override suspend fun execute(params: Unit) {
|
||||
Timber.d("SignOut: send request...")
|
||||
executeRequest<Unit> {
|
||||
apiCall = signOutAPI.signOut()
|
||||
override suspend fun execute(params: SignOutTask.Params) {
|
||||
// It should be done even after a soft logout, to be sure the deviceId is deleted on the
|
||||
if (params.sigOutFromHomeserver) {
|
||||
Timber.d("SignOut: send request...")
|
||||
try {
|
||||
executeRequest<Unit> {
|
||||
apiCall = signOutAPI.signOut()
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
// Maybe due to https://github.com/matrix-org/synapse/issues/5755
|
||||
if (throwable is Failure.ServerError
|
||||
&& throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
|
||||
&& throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) {
|
||||
// Also throwable.error.isSoftLogout should be true
|
||||
// Ignore
|
||||
Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
|
||||
} else {
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("SignOut: release session...")
|
||||
|
@@ -17,8 +17,6 @@
|
||||
package im.vector.matrix.android.internal.session.sync
|
||||
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
@@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
|
||||
initialSyncProgressService.endAll()
|
||||
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
|
||||
}
|
||||
val syncResponse = try {
|
||||
executeRequest<SyncResponse> {
|
||||
apiCall = syncAPI.sync(requestParams)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
// Intercept 401
|
||||
if (throwable is Failure.ServerError
|
||||
&& throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
|
||||
sessionParamsStore.delete(userId)
|
||||
}
|
||||
throw throwable
|
||||
val syncResponse = executeRequest<SyncResponse> {
|
||||
apiCall = syncAPI.sync(requestParams)
|
||||
}
|
||||
syncResponseHandler.handleResponse(syncResponse, token)
|
||||
syncTokenStore.saveToken(syncResponse.nextBatch)
|
||||
|
@@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.*
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
@@ -44,6 +45,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
|
||||
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
||||
private val savePushRulesTask: SavePushRulesTask,
|
||||
private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
|
||||
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) {
|
||||
@@ -52,6 +54,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
|
||||
is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
|
||||
is UserAccountDataPushRules -> handlePushRules(it)
|
||||
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it)
|
||||
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(it)
|
||||
is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}")
|
||||
else -> error("Missing code here!")
|
||||
}
|
||||
@@ -130,4 +133,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
|
||||
.executeBy(taskExecutor)
|
||||
// TODO If not initial sync, we should execute a init sync
|
||||
}
|
||||
|
||||
private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
|
||||
saveBreadcrumbsTask
|
||||
.configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds))
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
@@ -147,7 +147,7 @@ open class SyncService : Service() {
|
||||
}
|
||||
|
||||
if (failure is Failure.ServerError
|
||||
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
|
||||
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
|
||||
// No token or invalid token, stop the thread
|
||||
stopSelf()
|
||||
}
|
||||
|
@@ -44,19 +44,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
|
||||
private var state: SyncState = SyncState.IDLE
|
||||
private var state: SyncState = SyncState.Idle
|
||||
private var liveState = MutableLiveData<SyncState>()
|
||||
private val lock = Object()
|
||||
private var cancelableTask: Cancelable? = null
|
||||
|
||||
private var isStarted = false
|
||||
private var isTokenValid = true
|
||||
|
||||
init {
|
||||
updateStateTo(SyncState.IDLE)
|
||||
updateStateTo(SyncState.Idle)
|
||||
}
|
||||
|
||||
fun setInitialForeground(initialForeground: Boolean) {
|
||||
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED
|
||||
val newState = if (initialForeground) SyncState.Idle else SyncState.Paused
|
||||
updateStateTo(newState)
|
||||
}
|
||||
|
||||
@@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
if (!isStarted) {
|
||||
Timber.v("Resume sync...")
|
||||
isStarted = true
|
||||
// Check again the token validity
|
||||
isTokenValid = true
|
||||
lock.notify()
|
||||
}
|
||||
}
|
||||
@@ -78,7 +81,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
|
||||
fun kill() = synchronized(lock) {
|
||||
Timber.v("Kill sync...")
|
||||
updateStateTo(SyncState.KILLING)
|
||||
updateStateTo(SyncState.Killing)
|
||||
cancelableTask?.cancel()
|
||||
lock.notify()
|
||||
}
|
||||
@@ -100,26 +103,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
networkConnectivityChecker.register(this)
|
||||
backgroundDetectionObserver.register(this)
|
||||
|
||||
while (state != SyncState.KILLING) {
|
||||
while (state != SyncState.Killing) {
|
||||
Timber.v("Entering loop, state: $state")
|
||||
|
||||
if (!networkConnectivityChecker.hasInternetAccess) {
|
||||
Timber.v("No network. Waiting...")
|
||||
updateStateTo(SyncState.NO_NETWORK)
|
||||
updateStateTo(SyncState.NoNetwork)
|
||||
synchronized(lock) { lock.wait() }
|
||||
Timber.v("...unlocked")
|
||||
} else if (!isStarted) {
|
||||
Timber.v("Sync is Paused. Waiting...")
|
||||
updateStateTo(SyncState.PAUSED)
|
||||
updateStateTo(SyncState.Paused)
|
||||
synchronized(lock) { lock.wait() }
|
||||
Timber.v("...unlocked")
|
||||
} else if (!isTokenValid) {
|
||||
Timber.v("Token is invalid. Waiting...")
|
||||
updateStateTo(SyncState.InvalidToken)
|
||||
synchronized(lock) { lock.wait() }
|
||||
Timber.v("...unlocked")
|
||||
} else {
|
||||
if (state !is SyncState.RUNNING) {
|
||||
updateStateTo(SyncState.RUNNING(afterPause = true))
|
||||
if (state !is SyncState.Running) {
|
||||
updateStateTo(SyncState.Running(afterPause = true))
|
||||
}
|
||||
|
||||
// No timeout after a pause
|
||||
val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
|
||||
val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
|
||||
|
||||
Timber.v("Execute sync request with timeout $timeout")
|
||||
val latch = CountDownLatch(1)
|
||||
@@ -141,10 +149,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
} else if (failure is Failure.Cancelled) {
|
||||
Timber.v("Cancelled")
|
||||
} else if (failure is Failure.ServerError
|
||||
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
|
||||
// No token or invalid token, stop the thread
|
||||
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
|
||||
// No token or invalid token
|
||||
Timber.w(failure)
|
||||
updateStateTo(SyncState.KILLING)
|
||||
isTokenValid = false
|
||||
isStarted = false
|
||||
} else {
|
||||
Timber.e(failure)
|
||||
|
||||
@@ -163,8 +172,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
|
||||
latch.await()
|
||||
state.let {
|
||||
if (it is SyncState.RUNNING && it.afterPause) {
|
||||
updateStateTo(SyncState.RUNNING(afterPause = false))
|
||||
if (it is SyncState.Running && it.afterPause) {
|
||||
updateStateTo(SyncState.Running(afterPause = false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +181,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
}
|
||||
}
|
||||
Timber.v("Sync killed")
|
||||
updateStateTo(SyncState.KILLED)
|
||||
updateStateTo(SyncState.Killed)
|
||||
backgroundDetectionObserver.unregister(this)
|
||||
networkConnectivityChecker.unregister(this)
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ internal abstract class UserAccountData {
|
||||
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"
|
||||
|
@@ -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.internal.session.sync.model.accountdata
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
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)
|
||||
internal data class BreadcrumbsContent(
|
||||
@Json(name = "recent_rooms") val recentRoomIds: List<String> = emptyList()
|
||||
)
|
@@ -35,5 +35,11 @@ internal abstract class AccountDataModule {
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
|
||||
abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
|
||||
}
|
||||
|
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.user.accountdata
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import io.realm.RealmList
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Save the Breadcrumbs roomId list in DB, either from the sync, or updated locally
|
||||
*/
|
||||
internal interface SaveBreadcrumbsTask : Task<SaveBreadcrumbsTask.Params, Unit> {
|
||||
data class Params(
|
||||
val recentRoomIds: List<String>
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSaveBreadcrumbsTask @Inject constructor(
|
||||
private val monarchy: Monarchy
|
||||
) : SaveBreadcrumbsTask {
|
||||
|
||||
override suspend fun execute(params: SaveBreadcrumbsTask.Params) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
// Get or create a breadcrumbs entity
|
||||
val entity = BreadcrumbsEntity.getOrCreate(realm)
|
||||
|
||||
// And save the new received list
|
||||
entity.recentRoomIds = RealmList<String>().apply { addAll(params.recentRoomIds) }
|
||||
|
||||
// Update the room summaries
|
||||
// Reset all the indexes...
|
||||
RoomSummaryEntity.where(realm)
|
||||
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
|
||||
.findAll()
|
||||
.forEach {
|
||||
it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
|
||||
}
|
||||
|
||||
// ...and apply new indexes
|
||||
params.recentRoomIds.forEachIndexed { index, roomId ->
|
||||
RoomSummaryEntity.where(realm, roomId)
|
||||
.findFirst()
|
||||
?.breadcrumbsIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.user.accountdata
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
|
||||
import im.vector.matrix.android.internal.database.query.get
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import javax.inject.Inject
|
||||
|
||||
// Use the same arbitrary value than Riot-Web
|
||||
private const val MAX_BREADCRUMBS_ROOMS_NUMBER = 20
|
||||
|
||||
internal interface UpdateBreadcrumbsTask : Task<UpdateBreadcrumbsTask.Params, Unit> {
|
||||
data class Params(
|
||||
val newTopRoomId: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultUpdateBreadcrumbsTask @Inject constructor(
|
||||
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
|
||||
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
||||
private val monarchy: Monarchy
|
||||
) : UpdateBreadcrumbsTask {
|
||||
|
||||
override suspend fun execute(params: UpdateBreadcrumbsTask.Params) {
|
||||
val newBreadcrumbs =
|
||||
// Get the breadcrumbs entity, if any
|
||||
monarchy.fetchCopied { BreadcrumbsEntity.get(it) }
|
||||
?.recentRoomIds
|
||||
?.apply {
|
||||
// Modify the list to add the newTopRoomId first
|
||||
// Ensure the newTopRoomId is not already in the list
|
||||
remove(params.newTopRoomId)
|
||||
// Add the newTopRoomId at first position
|
||||
add(0, params.newTopRoomId)
|
||||
}
|
||||
?.take(MAX_BREADCRUMBS_ROOMS_NUMBER)
|
||||
?: listOf(params.newTopRoomId)
|
||||
|
||||
// Update the DB locally, do not wait for the sync
|
||||
saveBreadcrumbsTask.execute(SaveBreadcrumbsTask.Params(newBreadcrumbs))
|
||||
|
||||
// FIXME It can remove the previous breadcrumbs, if not synced yet
|
||||
// And update account data
|
||||
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams(
|
||||
breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs)
|
||||
))
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata
|
||||
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
@@ -38,6 +39,15 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
|
||||
return directMessages
|
||||
}
|
||||
}
|
||||
|
||||
data class BreadcrumbsParams(override val type: String = UserAccountData.TYPE_BREADCRUMBS,
|
||||
private val breadcrumbsContent: BreadcrumbsContent
|
||||
) : Params {
|
||||
|
||||
override fun getData(): Any {
|
||||
return breadcrumbsContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI,
|
||||
|
@@ -16,9 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.util
|
||||
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Convert a string to an UTF8 String
|
||||
@@ -51,10 +49,3 @@ fun convertFromUTF8(s: String): String {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.firstLetterOfDisplayName(): String {
|
||||
if (this.isNullOrEmpty()) return ""
|
||||
val isUserId = MatrixPatterns.isUserId(this)
|
||||
val firstLetterIndex = if (isUserId) 1 else 0
|
||||
return this[firstLetterIndex].toString().toUpperCase(Locale.ROOT)
|
||||
}
|
||||
|
@@ -82,4 +82,86 @@
|
||||
|
||||
<string name="room_displayname_empty_room">Prázdná místnost</string>
|
||||
|
||||
<string name="notice_room_update">%s upravil/a tuto místnost.</string>
|
||||
|
||||
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Zpráva smazána [smazal/a %1$s] [důvod: %2$s]</string>
|
||||
<string name="notice_room_third_party_revoked_invite">"%1$s obnovil/a pozvánku do místnosti pro %2$s"</string>
|
||||
<string name="verification_emoji_cat">Kočka</string>
|
||||
<string name="verification_emoji_lion">Lev</string>
|
||||
<string name="verification_emoji_horse">Kůň</string>
|
||||
<string name="verification_emoji_unicorn">Jednorožec</string>
|
||||
<string name="verification_emoji_pig">Prase</string>
|
||||
<string name="verification_emoji_elephant">Slon</string>
|
||||
<string name="verification_emoji_rabbit">Králík</string>
|
||||
<string name="verification_emoji_panda">Panda</string>
|
||||
<string name="verification_emoji_rooster">Kohout</string>
|
||||
<string name="verification_emoji_penguin">Tučnák</string>
|
||||
<string name="verification_emoji_turtle">Želva</string>
|
||||
<string name="verification_emoji_fish">Ryba</string>
|
||||
<string name="verification_emoji_octopus">Chobotnice</string>
|
||||
<string name="verification_emoji_butterfly">Motýl</string>
|
||||
<string name="verification_emoji_flower">Květina</string>
|
||||
<string name="verification_emoji_tree">Strom</string>
|
||||
<string name="verification_emoji_cactus">Kaktus</string>
|
||||
<string name="verification_emoji_mushroom">Houba</string>
|
||||
<string name="verification_emoji_globe">Glóbus</string>
|
||||
<string name="verification_emoji_moon">Měsíc</string>
|
||||
<string name="verification_emoji_cloud">Mrak</string>
|
||||
<string name="verification_emoji_fire">Oheň</string>
|
||||
<string name="verification_emoji_banana">Banán</string>
|
||||
<string name="verification_emoji_apple">Jablko</string>
|
||||
<string name="verification_emoji_strawberry">Jahoda</string>
|
||||
<string name="verification_emoji_corn">Kukuřice</string>
|
||||
<string name="verification_emoji_pizza">Pizza</string>
|
||||
<string name="verification_emoji_cake">Dort</string>
|
||||
<string name="verification_emoji_heart">Srdce</string>
|
||||
<string name="verification_emoji_smiley">Smajlík</string>
|
||||
<string name="verification_emoji_robot">Robot</string>
|
||||
<string name="verification_emoji_hat">Klobouk</string>
|
||||
<string name="verification_emoji_glasses">Brýle</string>
|
||||
<string name="verification_emoji_santa">Santa</string>
|
||||
<string name="verification_emoji_thumbsup">Zvednutý palec</string>
|
||||
<string name="verification_emoji_umbrella">Deštník</string>
|
||||
<string name="verification_emoji_hourglass">Přesípací hodiny</string>
|
||||
<string name="verification_emoji_clock">Hodiny</string>
|
||||
<string name="verification_emoji_gift">Dárek</string>
|
||||
<string name="verification_emoji_lightbulb">Žárovka</string>
|
||||
<string name="verification_emoji_book">Knížka</string>
|
||||
<string name="verification_emoji_pencil">Tužka</string>
|
||||
<string name="verification_emoji_paperclip">Sponka</string>
|
||||
<string name="verification_emoji_scissors">Nůžky</string>
|
||||
<string name="verification_emoji_lock">Zámek</string>
|
||||
<string name="verification_emoji_key">Klíč</string>
|
||||
<string name="verification_emoji_hammer">Kladivo</string>
|
||||
<string name="verification_emoji_telephone">Telefon</string>
|
||||
<string name="verification_emoji_flag">Vlajka</string>
|
||||
<string name="verification_emoji_train">Vlak</string>
|
||||
<string name="verification_emoji_bicycle">Kolo</string>
|
||||
<string name="verification_emoji_airplane">Letadlo</string>
|
||||
<string name="verification_emoji_rocket">Raketa</string>
|
||||
<string name="verification_emoji_trophy">Pohár</string>
|
||||
<string name="verification_emoji_ball">Míč</string>
|
||||
<string name="verification_emoji_guitar">Kytara</string>
|
||||
<string name="verification_emoji_trumpet">Trumpeta</string>
|
||||
<string name="verification_emoji_bell">Zvon</string>
|
||||
<string name="verification_emoji_anchor">Kotva</string>
|
||||
<string name="verification_emoji_headphone">Sluchátka</string>
|
||||
<string name="verification_emoji_folder">Složka</string>
|
||||
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
|
||||
\nStahuji účet…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Uvodní synchronizace:
|
||||
\nStahuji klíče</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Uvodní synchnizace:
|
||||
\nStahuji místnost</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Uvodní synchronizace:
|
||||
\nStahuji moje místnosti</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Uvodní synchonizace:
|
||||
\nStahuji místnosti, které jsem opustil/a</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Úvodní sychronizace:
|
||||
\nImportuji komunity</string>
|
||||
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
|
||||
\nImportuji data účtu</string>
|
||||
|
||||
<string name="event_status_sending_message">Posílám zprávu…</string>
|
||||
</resources>
|
||||
|
@@ -49,7 +49,7 @@
|
||||
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string>
|
||||
<string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">관련 대화</string>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user