Merge branch 'develop' into feature/bma/fix_crash
This commit is contained in:
commit
675e4579ac
11
AUTHORS.md
11
AUTHORS.md
@ -4,7 +4,7 @@ A full developer contributors list can be found [here](https://github.com/vector
|
||||
|
||||
Even if we try to be able to work on all the functionalities, we have more knowledge about what we have developed ourselves.
|
||||
|
||||
## Benoit: Android team leader
|
||||
## [Benoit](https://github.com/bmarty): Android team leader
|
||||
|
||||
[@benoit.marty:matrix.org](https://matrix.to/#/@benoit.marty:matrix.org)
|
||||
- Android team leader and project leader, Android developer, GitHub community manager.
|
||||
@ -12,7 +12,7 @@ Even if we try to be able to work on all the functionalities, we have more knowl
|
||||
- Reviewing and polishing developed features, code quality manager, PRs reviewer, GitHub community manager.
|
||||
- Release manager on the Play Store
|
||||
|
||||
## François: Software architect
|
||||
## [Ganfra](https://github.com/ganfra) (aka François): Software architect
|
||||
|
||||
[@ganfra:matrix.org](https://matrix.to/#/@ganfra:matrix.org)
|
||||
- Software architect, Android developer
|
||||
@ -20,12 +20,17 @@ Even if we try to be able to work on all the functionalities, we have more knowl
|
||||
- Work mainly on the global architecture of the project.
|
||||
- Specialist of the timeline, and lots of other features.
|
||||
|
||||
## Valere: Product manager, Android developer
|
||||
## [Valere](https://github.com/BillCarsonFr): Product manager, Android developer
|
||||
|
||||
[@valere35:matrix.org](https://matrix.to/#/@valere35:matrix.org)
|
||||
- Product manager, Android developer
|
||||
- Specialist on the crypto implementation.
|
||||
|
||||
## [Onuray](https://github.com/onurays): Android developer
|
||||
|
||||
[@onurays:matrix.org](https://matrix.to/#/@onurays:matrix.org)
|
||||
- Android developer
|
||||
|
||||
# Other contributors
|
||||
|
||||
First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.
|
||||
|
10
CHANGES.md
10
CHANGES.md
@ -5,12 +5,22 @@ Features ✨:
|
||||
-
|
||||
|
||||
Improvements 🙌:
|
||||
- New room creation tile with quick action (#2346)
|
||||
- Open an existing DM instead of creating a new one (#2319)
|
||||
- Use RoomMember instead of User in the context of a Room.
|
||||
- Ask for explicit user consent to send their contact details to the identity server (#2375)
|
||||
- Handle events of type "m.room.server_acl" (#890)
|
||||
- Move "Enable Encryption" from room setting screen to room profile screen (#2394)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix crash on AttachmentViewer (#2365)
|
||||
- Exclude yourself when decorating rooms which are direct or don't have more than 2 users (#2370)
|
||||
- F-Droid version: ensure timeout of sync request can be more than 60 seconds (#2169)
|
||||
- Fix issue when restoring draft after sharing (#2287)
|
||||
- Fix issue when updating the avatar of a room (new avatar vanishing)
|
||||
- Discard change dialog displayed by mistake when avatar has been updated
|
||||
- Try to fix cropped image in timeline (#2126)
|
||||
- Registration: annoying error message scares every new user when they add an email (#2391)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.pushers.Pusher
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
@ -92,6 +93,13 @@ class RxSession(private val session: Session) {
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomMember(userId: String, roomId: String): Observable<Optional<RoomMemberSummary>> {
|
||||
return session.getRoomMemberLive(userId, roomId).asObservable()
|
||||
.startWithCallable {
|
||||
session.getRoomMember(userId, roomId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
return session.getUsersLive().asObservable()
|
||||
}
|
||||
|
@ -68,8 +68,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
if (encryptedRoom) {
|
||||
val room = aliceSession.getRoom(roomId)!!
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
room.enableEncryption(callback = it)
|
||||
mTestHelper.runBlockingTest {
|
||||
room.enableEncryption()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,7 @@ class SearchMessagesTest : InstrumentedTest {
|
||||
commonTestHelper.await(lock)
|
||||
|
||||
lock = CountDownLatch(1)
|
||||
val data = commonTestHelper.runBlockingTest {
|
||||
aliceSession
|
||||
.searchService()
|
||||
.search(
|
||||
@ -81,10 +82,9 @@ class SearchMessagesTest : InstrumentedTest {
|
||||
beforeLimit = 10,
|
||||
orderByRecent = true,
|
||||
nextBatch = null,
|
||||
roomId = aliceRoomId,
|
||||
callback = object : MatrixCallback<SearchResult> {
|
||||
override fun onSuccess(data: SearchResult) {
|
||||
super.onSuccess(data)
|
||||
roomId = aliceRoomId
|
||||
)
|
||||
}
|
||||
assertTrue(data.results?.size == 2)
|
||||
assertTrue(
|
||||
data.results
|
||||
@ -92,17 +92,6 @@ class SearchMessagesTest : InstrumentedTest {
|
||||
(it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
|
||||
}.orFalse()
|
||||
)
|
||||
lock.countDown()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
super.onFailure(failure)
|
||||
fail(failure.localizedMessage)
|
||||
lock.countDown()
|
||||
}
|
||||
}
|
||||
)
|
||||
lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
|
||||
|
||||
aliceTimeline.removeAllListeners()
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
|
@ -22,3 +22,8 @@ fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
|
||||
else -> "$prefix$this"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a new line and then the provided string
|
||||
*/
|
||||
fun StringBuilder.appendNl(str: String) = append("\n").append(str)
|
||||
|
@ -15,11 +15,9 @@
|
||||
*/
|
||||
package org.matrix.android.sdk.api.pushrules
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
interface PushRuleService {
|
||||
/**
|
||||
@ -29,13 +27,13 @@ interface PushRuleService {
|
||||
|
||||
fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet
|
||||
|
||||
fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean)
|
||||
|
||||
fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun addPushRule(kind: RuleKind, pushRule: PushRule)
|
||||
|
||||
fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule)
|
||||
|
||||
fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
|
||||
|
||||
fun addPushRuleListener(listener: PushRuleListener)
|
||||
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.raw
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* Useful methods to fetch raw data from the server. The access token will not be used to fetched the data
|
||||
*/
|
||||
@ -26,17 +23,15 @@ interface RawService {
|
||||
/**
|
||||
* Get a URL, either from cache or from the remote server, depending on the cache strategy
|
||||
*/
|
||||
fun getUrl(url: String,
|
||||
rawCacheStrategy: RawCacheStrategy,
|
||||
matrixCallback: MatrixCallback<String>): Cancelable
|
||||
suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String
|
||||
|
||||
/**
|
||||
* Specific case for the well-known file. Cache validity is 8 hours
|
||||
*/
|
||||
fun getWellknown(userId: String, matrixCallback: MatrixCallback<String>): Cancelable
|
||||
suspend fun getWellknown(userId: String): String
|
||||
|
||||
/**
|
||||
* Clear all the cache data
|
||||
*/
|
||||
fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun clearCache()
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ object EventType {
|
||||
const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
|
||||
const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
|
||||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||
const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
|
||||
|
||||
// Call Events
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.group
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact within a group.
|
||||
*/
|
||||
@ -28,8 +25,7 @@ interface Group {
|
||||
/**
|
||||
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
|
||||
* The SDK also takes care of refreshing group data every hour.
|
||||
* @param callback : the matrix callback to be notified of success or failure
|
||||
* @return a Cancelable to be able to cancel requests.
|
||||
*/
|
||||
fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun fetchGroupData()
|
||||
}
|
||||
|
@ -92,9 +92,29 @@ interface IdentityService {
|
||||
|
||||
/**
|
||||
* Search MatrixId of users providing email and phone numbers
|
||||
* Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure
|
||||
* Application has to explicitly ask for the user consent, and the answer can be stored using [setUserConsent]
|
||||
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
|
||||
*/
|
||||
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the current user consent for the current identity server, which has been stored using [setUserConsent].
|
||||
* If [setUserConsent] has not been called, the returned value will be false.
|
||||
* Note that if the identity server is changed, the user consent is reset to false.
|
||||
* @return the value stored using [setUserConsent] or false if [setUserConsent] has never been called, or if the identity server
|
||||
* has been changed
|
||||
*/
|
||||
fun getUserConsent(): Boolean
|
||||
|
||||
/**
|
||||
* Set the user consent to the provided value. Application MUST explicitly ask for the user consent to send their private data
|
||||
* (email and phone numbers) to the identity server.
|
||||
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
|
||||
* @param newValue true if the user explicitly give their consent, false if the user wants to revoke their consent.
|
||||
*/
|
||||
fun setUserConsent(newValue: Boolean)
|
||||
|
||||
/**
|
||||
* Get the status of the current user's threePid
|
||||
* A lookup will be performed, but also pending binding state will be restored
|
||||
|
@ -24,6 +24,7 @@ sealed class IdentityServiceError : Failure.FeatureFailure() {
|
||||
object NoIdentityServerConfigured : IdentityServiceError()
|
||||
object TermsNotSignedException : IdentityServiceError()
|
||||
object BulkLookupSha256NotSupported : IdentityServiceError()
|
||||
object UserConsentNotProvided : IdentityServiceError()
|
||||
object BindingError : IdentityServiceError()
|
||||
object NoCurrentBindingError : IdentityServiceError()
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package org.matrix.android.sdk.api.session.permalinks
|
||||
|
||||
import android.text.Spannable
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
|
||||
/**
|
||||
* MatrixLinkify take a piece of text and turns all of the
|
||||
@ -35,7 +36,7 @@ object MatrixLinkify {
|
||||
* I disable it because it mess up with pills, and even with pills, it does not work correctly:
|
||||
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
|
||||
*/
|
||||
/*
|
||||
|
||||
// sanity checks
|
||||
if (spannable.isEmpty()) {
|
||||
return false
|
||||
@ -48,14 +49,21 @@ object MatrixLinkify {
|
||||
val startPos = match.range.first
|
||||
if (startPos == 0 || text[startPos - 1] != '/') {
|
||||
val endPos = match.range.last + 1
|
||||
val url = text.substring(match.range)
|
||||
var url = text.substring(match.range)
|
||||
if (MatrixPatterns.isUserId(url)
|
||||
|| MatrixPatterns.isRoomAlias(url)
|
||||
|| MatrixPatterns.isRoomId(url)
|
||||
|| MatrixPatterns.isGroupId(url)
|
||||
|| MatrixPatterns.isEventId(url)) {
|
||||
url = PermalinkService.MATRIX_TO_URL_BASE + url
|
||||
}
|
||||
val span = MatrixPermalinkSpan(url, callback)
|
||||
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasMatch
|
||||
*/
|
||||
return false
|
||||
|
||||
// return false
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
@ -141,4 +142,20 @@ interface RoomService {
|
||||
* - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
|
||||
*/
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): String?
|
||||
|
||||
/**
|
||||
* Get a room member for the tuple {userId,roomId}
|
||||
* @param userId the userId to look for.
|
||||
* @param roomId the roomId to look for.
|
||||
* @return the room member or null
|
||||
*/
|
||||
fun getRoomMember(userId: String, roomId: String): RoomMemberSummary?
|
||||
|
||||
/**
|
||||
* Observe a live room member for the tuple {userId,roomId}
|
||||
* @param userId the userId to look for.
|
||||
* @param roomId the roomId to look for.
|
||||
* @return a LiveData of the optional found room member
|
||||
*/
|
||||
fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>>
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
|
||||
interface RoomCryptoService {
|
||||
@ -30,6 +29,5 @@ interface RoomCryptoService {
|
||||
/**
|
||||
* Enable encryption of the room
|
||||
*/
|
||||
fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
callback: MatrixCallback<Unit>)
|
||||
suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM)
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class representing the EventType.STATE_ROOM_SERVER_ACL state event content
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-server-acl
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomServerAclContent(
|
||||
/**
|
||||
* True to allow server names that are IP address literals. False to deny.
|
||||
* Defaults to true if missing or otherwise not a boolean.
|
||||
* This is strongly recommended to be set to false as servers running with IP literal names are strongly
|
||||
* discouraged in order to require legitimate homeservers to be backed by a valid registered domain name.
|
||||
*/
|
||||
@Json(name = "allow_ip_literals")
|
||||
val allowIpLiterals: Boolean = true,
|
||||
|
||||
/**
|
||||
* The server names to allow in the room, excluding any port information. Wildcards may be used to cover
|
||||
* a wider range of hosts, where * matches zero or more characters and ? matches exactly one character.
|
||||
*
|
||||
* This defaults to an empty list when not provided, effectively disallowing every server.
|
||||
*/
|
||||
@Json(name = "allow")
|
||||
val allowList: List<String> = emptyList(),
|
||||
|
||||
/**
|
||||
* The server names to disallow in the room, excluding any port information. Wildcards may be used to cover
|
||||
* a wider range of hosts, where * matches zero or more characters and ? matches exactly one character.
|
||||
*
|
||||
* This defaults to an empty list when not provided.
|
||||
*/
|
||||
@Json(name = "deny")
|
||||
val denyList: List<String> = emptyList()
|
||||
|
||||
) {
|
||||
companion object {
|
||||
const val ALL = "*"
|
||||
}
|
||||
}
|
@ -17,12 +17,10 @@
|
||||
package org.matrix.android.sdk.api.session.room.notification
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
interface RoomPushRuleService {
|
||||
|
||||
fun getLiveRoomNotificationState(): LiveData<RoomNotificationState>
|
||||
|
||||
fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun setRoomNotificationState(roomNotificationState: RoomNotificationState)
|
||||
}
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.reporting
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to report content of an event.
|
||||
*/
|
||||
@ -28,5 +25,5 @@ interface ReportingService {
|
||||
* Report content
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
|
||||
*/
|
||||
fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun reportContent(eventId: String, score: Int, reason: String)
|
||||
}
|
||||
|
@ -17,8 +17,6 @@
|
||||
package org.matrix.android.sdk.api.session.room.send
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
interface DraftService {
|
||||
@ -26,12 +24,12 @@ interface DraftService {
|
||||
/**
|
||||
* Save or update a draft to the room
|
||||
*/
|
||||
fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun saveDraft(draft: UserDraft)
|
||||
|
||||
/**
|
||||
* Delete the last draft, basically just after sending the message
|
||||
*/
|
||||
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun deleteDraft()
|
||||
|
||||
/**
|
||||
* Return the current draft or null
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.tags
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle tags of a room. It's implemented at the room level.
|
||||
*/
|
||||
@ -26,10 +23,10 @@ interface TagsService {
|
||||
/**
|
||||
* Add a tag to a room
|
||||
*/
|
||||
fun addTag(tag: String, order: Double?, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun addTag(tag: String, order: Double?)
|
||||
|
||||
/**
|
||||
* Remove tag from a room
|
||||
*/
|
||||
fun deleteTag(tag: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun deleteTag(tag: String)
|
||||
}
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.search
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to search messages in rooms.
|
||||
*/
|
||||
@ -35,15 +32,13 @@ interface SearchService {
|
||||
* @param beforeLimit how many events before the result are returned.
|
||||
* @param afterLimit how many events after the result are returned.
|
||||
* @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned.
|
||||
* @param callback Callback to get the search result
|
||||
*/
|
||||
fun search(searchTerm: String,
|
||||
suspend fun search(searchTerm: String,
|
||||
roomId: String,
|
||||
nextBatch: String?,
|
||||
orderByRecent: Boolean,
|
||||
limit: Int,
|
||||
beforeLimit: Int,
|
||||
afterLimit: Int,
|
||||
includeProfile: Boolean,
|
||||
callback: MatrixCallback<SearchResult>): Cancelable
|
||||
includeProfile: Boolean): SearchResult
|
||||
}
|
||||
|
@ -241,9 +241,9 @@ internal class UpdateTrustWorker(context: Context,
|
||||
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
|
||||
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
|
||||
// The set of “all users” depends on the type of room:
|
||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
|
||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||
val listToCheck = if (roomSummaryEntity.isDirect) {
|
||||
val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) {
|
||||
activeMemberUserIds.filter { it != myUserId }
|
||||
} else {
|
||||
activeMemberUserIds
|
||||
|
@ -52,5 +52,8 @@ internal class TimeOutInterceptor @Inject constructor() : Interceptor {
|
||||
const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT"
|
||||
const val READ_TIMEOUT = "READ_TIMEOUT"
|
||||
const val WRITE_TIMEOUT = "WRITE_TIMEOUT"
|
||||
|
||||
// 1 minute
|
||||
const val DEFAULT_LONG_TIMEOUT: Long = 60_000
|
||||
}
|
||||
}
|
||||
|
@ -16,45 +16,28 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.raw
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.raw.RawCacheStrategy
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultRawService @Inject constructor(
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val getUrlTask: GetUrlTask,
|
||||
private val cleanRawCacheTask: CleanRawCacheTask
|
||||
) : RawService {
|
||||
override fun getUrl(url: String,
|
||||
rawCacheStrategy: RawCacheStrategy,
|
||||
matrixCallback: MatrixCallback<String>): Cancelable {
|
||||
return getUrlTask
|
||||
.configureWith(GetUrlTask.Params(url, rawCacheStrategy)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String {
|
||||
return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy))
|
||||
}
|
||||
|
||||
override fun getWellknown(userId: String,
|
||||
matrixCallback: MatrixCallback<String>): Cancelable {
|
||||
override suspend fun getWellknown(userId: String): String {
|
||||
val homeServerDomain = userId.substringAfter(":")
|
||||
return getUrl(
|
||||
"https://$homeServerDomain/.well-known/matrix/client",
|
||||
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false),
|
||||
matrixCallback
|
||||
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return cleanRawCacheTask
|
||||
.configureWith(Unit) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun clearCache() {
|
||||
cleanRawCacheTask.execute(Unit)
|
||||
}
|
||||
}
|
||||
|
@ -16,20 +16,13 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.session.group
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.group.Group
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
|
||||
internal class DefaultGroup(override val groupId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val getGroupDataTask: GetGroupDataTask) : Group {
|
||||
|
||||
override fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable {
|
||||
override suspend fun fetchGroupData() {
|
||||
val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId))
|
||||
return getGroupDataTask.configureWith(params) {
|
||||
this.callback = callback
|
||||
}.executeBy(taskExecutor)
|
||||
getGroupDataTask.execute(params)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.group
|
||||
|
||||
import org.matrix.android.sdk.api.session.group.Group
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GroupFactory {
|
||||
@ -26,14 +25,12 @@ internal interface GroupFactory {
|
||||
}
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask,
|
||||
private val taskExecutor: TaskExecutor) :
|
||||
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) :
|
||||
GroupFactory {
|
||||
|
||||
override fun create(groupId: String): Group {
|
||||
return DefaultGroup(
|
||||
groupId = groupId,
|
||||
taskExecutor = taskExecutor,
|
||||
getGroupDataTask = getGroupDataTask
|
||||
)
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.ensureProtocol
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
@ -243,7 +244,20 @@ internal class DefaultIdentityService @Inject constructor(
|
||||
))
|
||||
}
|
||||
|
||||
override fun getUserConsent(): Boolean {
|
||||
return identityStore.getIdentityData()?.userConsent.orFalse()
|
||||
}
|
||||
|
||||
override fun setUserConsent(newValue: Boolean) {
|
||||
identityStore.setUserConsent(newValue)
|
||||
}
|
||||
|
||||
override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
|
||||
if (!getUserConsent()) {
|
||||
callback.onFailure(IdentityServiceError.UserConsentNotProvided)
|
||||
return NoOpCancellable
|
||||
}
|
||||
|
||||
if (threePids.isEmpty()) {
|
||||
callback.onSuccess(emptyList())
|
||||
return NoOpCancellable
|
||||
@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable {
|
||||
// Note: we do not require user consent here, because it is used for emails and phone numbers that the user has already sent
|
||||
// to the home server, and not emails and phone numbers from the contact book of the user
|
||||
|
||||
if (threePids.isEmpty()) {
|
||||
callback.onSuccess(emptyMap())
|
||||
return NoOpCancellable
|
||||
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule
|
||||
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore
|
||||
import io.realm.RealmConfiguration
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration
|
||||
import java.io.File
|
||||
|
||||
@Module
|
||||
@ -59,6 +60,7 @@ internal abstract class IdentityModule {
|
||||
@SessionScope
|
||||
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
|
||||
@SessionFilesDirectory directory: File,
|
||||
migration: RealmIdentityStoreMigration,
|
||||
@UserMd5 userMd5: String): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
@ -66,6 +68,8 @@ internal abstract class IdentityModule {
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
||||
}
|
||||
.schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION)
|
||||
.migration(migration)
|
||||
.allowWritesOnUiThread(true)
|
||||
.modules(IdentityRealmModule())
|
||||
.build()
|
||||
|
@ -20,5 +20,6 @@ internal data class IdentityData(
|
||||
val identityServerUrl: String?,
|
||||
val token: String?,
|
||||
val hashLookupPepper: String?,
|
||||
val hashLookupAlgorithm: List<String>
|
||||
val hashLookupAlgorithm: List<String>,
|
||||
val userConsent: Boolean
|
||||
)
|
||||
|
@ -27,6 +27,8 @@ internal interface IdentityStore {
|
||||
|
||||
fun setToken(token: String?)
|
||||
|
||||
fun setUserConsent(consent: Boolean)
|
||||
|
||||
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
|
||||
|
||||
/**
|
||||
|
@ -23,7 +23,8 @@ internal open class IdentityDataEntity(
|
||||
var identityServerUrl: String? = null,
|
||||
var token: String? = null,
|
||||
var hashLookupPepper: String? = null,
|
||||
var hashLookupAlgorithm: RealmList<String> = RealmList()
|
||||
var hashLookupAlgorithm: RealmList<String> = RealmList(),
|
||||
var userConsent: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
@ -52,6 +52,13 @@ internal fun IdentityDataEntity.Companion.setToken(realm: Realm,
|
||||
}
|
||||
}
|
||||
|
||||
internal fun IdentityDataEntity.Companion.setUserConsent(realm: Realm,
|
||||
newConsent: Boolean) {
|
||||
get(realm)?.apply {
|
||||
userConsent = newConsent
|
||||
}
|
||||
}
|
||||
|
||||
internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm,
|
||||
pepper: String,
|
||||
algorithms: List<String>) {
|
||||
|
@ -26,7 +26,8 @@ internal object IdentityMapper {
|
||||
identityServerUrl = entity.identityServerUrl,
|
||||
token = entity.token,
|
||||
hashLookupPepper = entity.hashLookupPepper,
|
||||
hashLookupAlgorithm = entity.hashLookupAlgorithm.toList()
|
||||
hashLookupAlgorithm = entity.hashLookupAlgorithm.toList(),
|
||||
userConsent = entity.userConsent
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,14 @@ internal class RealmIdentityStore @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserConsent(consent: Boolean) {
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.executeTransaction { realm ->
|
||||
IdentityDataEntity.setUserConsent(realm, consent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) {
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.executeTransaction { realm ->
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.identity.db
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val IDENTITY_STORE_SCHEMA_VERSION = 1L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Identity from $oldVersion to $newVersion")
|
||||
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
Timber.d("Step 0 -> 1")
|
||||
Timber.d("Add field userConsent (Boolean) and set the value to false")
|
||||
|
||||
realm.schema.get("IdentityDataEntity")
|
||||
?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java)
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
package org.matrix.android.sdk.internal.session.notification
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||
import org.matrix.android.sdk.api.pushrules.RuleKind
|
||||
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
||||
@ -24,7 +23,6 @@ import org.matrix.android.sdk.api.pushrules.getActions
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper
|
||||
import org.matrix.android.sdk.internal.database.model.PushRulesEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
@ -103,37 +101,21 @@ internal class DefaultPushRuleService @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) {
|
||||
// The rules will be updated, and will come back from the next sync response
|
||||
return updatePushRuleEnableStatusTask
|
||||
.configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled))
|
||||
}
|
||||
|
||||
override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return addPushRuleTask
|
||||
.configureWith(AddPushRuleTask.Params(kind, pushRule)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) {
|
||||
addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule))
|
||||
}
|
||||
|
||||
override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return updatePushRuleActionsTask
|
||||
.configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) {
|
||||
updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule))
|
||||
}
|
||||
|
||||
override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return removePushRuleTask
|
||||
.configureWith(RemovePushRuleTask.Params(kind, pushRule)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
|
||||
removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule))
|
||||
}
|
||||
|
||||
override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {
|
||||
|
@ -101,13 +101,13 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||
return cryptoService.shouldEncryptForInvitedMembers(roomId)
|
||||
}
|
||||
|
||||
override fun enableEncryption(algorithm: String, callback: MatrixCallback<Unit>) {
|
||||
override suspend fun enableEncryption(algorithm: String) {
|
||||
when {
|
||||
isEncrypted() -> {
|
||||
callback.onFailure(IllegalStateException("Encryption is already enabled for this room"))
|
||||
throw IllegalStateException("Encryption is already enabled for this room")
|
||||
}
|
||||
algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> {
|
||||
callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported"))
|
||||
throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")
|
||||
}
|
||||
else -> {
|
||||
val params = SendStateTask.Params(
|
||||
@ -118,11 +118,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||
"algorithm" to algorithm
|
||||
))
|
||||
|
||||
sendStateTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
sendStateTask.execute(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,27 +17,37 @@
|
||||
package org.matrix.android.sdk.internal.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import org.matrix.android.sdk.internal.util.fetchCopied
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultRoomService @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val createRoomTask: CreateRoomTask,
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
||||
@ -118,4 +128,24 @@ internal class DefaultRoomService @Inject constructor(
|
||||
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
|
||||
return roomChangeMembershipStateDataSource.getLiveStates()
|
||||
}
|
||||
|
||||
override fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? {
|
||||
val roomMemberEntity = monarchy.fetchCopied {
|
||||
RoomMemberHelper(it, roomId).getLastRoomMember(userId)
|
||||
}
|
||||
return roomMemberEntity?.asDomain()
|
||||
}
|
||||
|
||||
override fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
{ realm ->
|
||||
RoomMemberHelper(realm, roomId).queryRoomMembersEvent()
|
||||
.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId)
|
||||
},
|
||||
{ it.asDomain() }
|
||||
)
|
||||
return Transformations.map(liveData) { results ->
|
||||
results.firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) {
|
||||
@ -46,12 +48,15 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId
|
||||
val roomMembers = RoomMemberHelper(realm, roomId)
|
||||
val members = roomMembers.queryActiveRoomMembersEvent().findAll()
|
||||
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
|
||||
val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false
|
||||
if (isDirectRoom) {
|
||||
if (members.size == 1) {
|
||||
res = members.firstOrNull()?.avatarUrl
|
||||
} else if (members.size == 2) {
|
||||
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst()
|
||||
res = firstOtherMember?.avatarUrl
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@
|
||||
package org.matrix.android.sdk.internal.session.room.alias
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.findByAlias
|
||||
@ -24,8 +27,6 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import io.realm.Realm
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
|
||||
@ -50,9 +51,11 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
|
||||
} else if (!params.searchOnServer) {
|
||||
Optional.from<String>(null)
|
||||
} else {
|
||||
roomId = executeRequest<RoomAliasDescription>(eventBus) {
|
||||
roomId = tryOrNull("## Failed to get roomId from alias") {
|
||||
executeRequest<RoomAliasDescription>(eventBus) {
|
||||
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
|
||||
}.roomId
|
||||
}
|
||||
}?.roomId
|
||||
Optional.from(roomId)
|
||||
}
|
||||
}
|
||||
|
@ -19,18 +19,14 @@ package org.matrix.android.sdk.internal.session.room.draft
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.session.room.send.DraftService
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val draftRepository: DraftRepository,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : DraftService {
|
||||
|
||||
@ -43,14 +39,14 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
|
||||
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
|
||||
* or even move an existing draft to the top of the list
|
||||
*/
|
||||
override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
override suspend fun saveDraft(draft: UserDraft) {
|
||||
withContext(coroutineDispatchers.main) {
|
||||
draftRepository.saveDraft(roomId, draft)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
override suspend fun deleteDraft() {
|
||||
withContext(coroutineDispatchers.main) {
|
||||
draftRepository.deleteDraft(roomId)
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,8 @@ internal class RoomDisplayNameResolver @Inject constructor(
|
||||
}
|
||||
} else if (roomEntity?.membership == Membership.JOIN) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
val invitedCount = roomSummary?.invitedMembersCount ?: 0
|
||||
val joinedCount = roomSummary?.joinedMembersCount ?: 0
|
||||
val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||
roomSummary.heroes.mapNotNull { userId ->
|
||||
roomMembers.getLastRoomMember(userId)?.takeIf {
|
||||
@ -102,22 +104,49 @@ internal class RoomDisplayNameResolver @Inject constructor(
|
||||
} else {
|
||||
activeMembers.where()
|
||||
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
|
||||
.limit(3)
|
||||
.limit(5)
|
||||
.findAll()
|
||||
.createSnapshot()
|
||||
}
|
||||
val otherMembersCount = otherMembersSubset.count()
|
||||
name = when (otherMembersCount) {
|
||||
0 -> stringProvider.getString(R.string.room_displayname_empty_room)
|
||||
0 -> {
|
||||
stringProvider.getString(R.string.room_displayname_empty_room)
|
||||
// TODO (was xx and yyy) ...
|
||||
}
|
||||
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
|
||||
2 -> stringProvider.getString(R.string.room_displayname_two_members,
|
||||
2 -> {
|
||||
stringProvider.getString(R.string.room_displayname_two_members,
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
|
||||
)
|
||||
else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||
roomMembers.getNumberOfJoinedMembers() - 1,
|
||||
}
|
||||
3 -> {
|
||||
stringProvider.getString(R.string.room_displayname_3_members,
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
roomMembers.getNumberOfJoinedMembers() - 1)
|
||||
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[2], roomMembers)
|
||||
)
|
||||
}
|
||||
4 -> {
|
||||
stringProvider.getString(R.string.room_displayname_4_members,
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[2], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[3], roomMembers)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val remainingCount = invitedCount + joinedCount - otherMembersCount + 1
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.room_displayname_four_and_more_members,
|
||||
remainingCount,
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[2], roomMembers),
|
||||
remainingCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return name ?: roomId
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.membership
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||
@ -25,8 +27,6 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
|
||||
/**
|
||||
* This class is an helper around STATE_ROOM_MEMBER events.
|
||||
|
@ -21,21 +21,16 @@ import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.pushrules.RuleScope
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.database.model.PushRuleEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
|
||||
internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val setRoomNotificationStateTask: SetRoomNotificationStateTask,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor)
|
||||
@SessionDatabase private val monarchy: Monarchy)
|
||||
: RoomPushRuleService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@ -49,12 +44,8 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted
|
||||
}
|
||||
}
|
||||
|
||||
override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return setRoomNotificationStateTask
|
||||
.configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) {
|
||||
this.callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun setRoomNotificationState(roomNotificationState: RoomNotificationState) {
|
||||
setRoomNotificationStateTask.execute(SetRoomNotificationStateTask.Params(roomId, roomNotificationState))
|
||||
}
|
||||
|
||||
private fun getPushRuleForRoom(): LiveData<RoomPushRule?> {
|
||||
|
@ -18,14 +18,9 @@ package org.matrix.android.sdk.internal.session.room.reporting
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.reporting.ReportingService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
|
||||
internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val reportContentTask: ReportContentTask
|
||||
) : ReportingService {
|
||||
|
||||
@ -34,13 +29,8 @@ internal class DefaultReportingService @AssistedInject constructor(@Assisted pri
|
||||
fun create(roomId: String): ReportingService
|
||||
}
|
||||
|
||||
override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override suspend fun reportContent(eventId: String, score: Int, reason: String) {
|
||||
val params = ReportContentTask.Params(roomId, eventId, score, reason)
|
||||
|
||||
return reportContentTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
reportContentTask.execute(params)
|
||||
}
|
||||
}
|
||||
|
@ -18,15 +18,10 @@ package org.matrix.android.sdk.internal.session.room.tags
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
|
||||
internal class DefaultTagsService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val addTagToRoomTask: AddTagToRoomTask,
|
||||
private val deleteTagFromRoomTask: DeleteTagFromRoomTask
|
||||
) : TagsService {
|
||||
@ -36,21 +31,13 @@ internal class DefaultTagsService @AssistedInject constructor(
|
||||
fun create(roomId: String): TagsService
|
||||
}
|
||||
|
||||
override fun addTag(tag: String, order: Double?, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override suspend fun addTag(tag: String, order: Double?) {
|
||||
val params = AddTagToRoomTask.Params(roomId, tag, order)
|
||||
return addTagToRoomTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
addTagToRoomTask.execute(params)
|
||||
}
|
||||
|
||||
override fun deleteTag(tag: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override suspend fun deleteTag(tag: String) {
|
||||
val params = DeleteTagFromRoomTask.Params(roomId, tag)
|
||||
return deleteTagFromRoomTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
deleteTagFromRoomTask.execute(params)
|
||||
}
|
||||
}
|
||||
|
@ -16,30 +16,23 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.session.search
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||
import org.matrix.android.sdk.api.session.search.SearchService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import javax.inject.Inject
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
|
||||
internal class DefaultSearchService @Inject constructor(
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val searchTask: SearchTask
|
||||
) : SearchService {
|
||||
|
||||
override fun search(searchTerm: String,
|
||||
override suspend fun search(searchTerm: String,
|
||||
roomId: String,
|
||||
nextBatch: String?,
|
||||
orderByRecent: Boolean,
|
||||
limit: Int,
|
||||
beforeLimit: Int,
|
||||
afterLimit: Int,
|
||||
includeProfile: Boolean,
|
||||
callback: MatrixCallback<SearchResult>): Cancelable {
|
||||
return searchTask
|
||||
.configureWith(SearchTask.Params(
|
||||
includeProfile: Boolean): SearchResult {
|
||||
return searchTask.execute(SearchTask.Params(
|
||||
searchTerm = searchTerm,
|
||||
roomId = roomId,
|
||||
nextBatch = nextBatch,
|
||||
@ -48,8 +41,6 @@ internal class DefaultSearchService @Inject constructor(
|
||||
beforeLimit = beforeLimit,
|
||||
afterLimit = afterLimit,
|
||||
includeProfile = includeProfile
|
||||
)) {
|
||||
this.callback = callback
|
||||
}.executeBy(taskExecutor)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -17,18 +17,21 @@
|
||||
package org.matrix.android.sdk.internal.session.sync
|
||||
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import org.matrix.android.sdk.internal.network.TimeOutInterceptor
|
||||
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.QueryMap
|
||||
|
||||
internal interface SyncAPI {
|
||||
|
||||
/**
|
||||
* Set all the timeouts to 1 minute
|
||||
* Set all the timeouts to 1 minute by default
|
||||
*/
|
||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync")
|
||||
fun sync(@QueryMap params: Map<String, String>): Call<SyncResponse>
|
||||
fun sync(@QueryMap params: Map<String, String>,
|
||||
@Header(TimeOutInterceptor.CONNECT_TIMEOUT) connectTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT,
|
||||
@Header(TimeOutInterceptor.READ_TIMEOUT) readTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT,
|
||||
@Header(TimeOutInterceptor.WRITE_TIMEOUT) writeTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT
|
||||
): Call<SyncResponse>
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.R
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.TimeOutInterceptor
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService
|
||||
import org.matrix.android.sdk.internal.session.filter.FilterRepository
|
||||
@ -78,8 +79,13 @@ internal class DefaultSyncTask @Inject constructor(
|
||||
// Maybe refresh the home server capabilities data we know
|
||||
getHomeServerCapabilitiesTask.execute(Unit)
|
||||
|
||||
val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
|
||||
|
||||
val syncResponse = executeRequest<SyncResponse>(eventBus) {
|
||||
apiCall = syncAPI.sync(requestParams)
|
||||
apiCall = syncAPI.sync(
|
||||
params = requestParams,
|
||||
readTimeOut = readTimeOut
|
||||
)
|
||||
}
|
||||
syncResponseHandler.handleResponse(syncResponse, token)
|
||||
if (isInitialSync) {
|
||||
@ -87,4 +93,8 @@ internal class DefaultSyncTask @Inject constructor(
|
||||
}
|
||||
Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MARGIN: Long = 10_000
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,23 @@
|
||||
<string name="notice_room_update_by_you">You upgraded this room.</string>
|
||||
<string name="notice_direct_room_update">%s upgraded here.</string>
|
||||
<string name="notice_direct_room_update_by_you">You upgraded here.</string>
|
||||
<string name="notice_room_server_acl_set_title">%s set the server ACLs for this room.</string>
|
||||
<string name="notice_room_server_acl_set_title_by_you">You set the server ACLs for this room.</string>
|
||||
<string name="notice_room_server_acl_set_banned">• Server matching %s are banned.</string>
|
||||
<string name="notice_room_server_acl_set_allowed">• Server matching %s are allowed.</string>
|
||||
<string name="notice_room_server_acl_set_ip_literals_allowed">• Server matching IP literals are allowed.</string>
|
||||
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Server matching IP literals are banned.</string>
|
||||
|
||||
<string name="notice_room_server_acl_updated_title">%s changed the server ACLs for this room.</string>
|
||||
<string name="notice_room_server_acl_updated_title_by_you">You changed the server ACLs for this room.</string>
|
||||
<string name="notice_room_server_acl_updated_banned">• Server matching %s are now banned.</string>
|
||||
<string name="notice_room_server_acl_updated_was_banned">• Server matching %s were removed from the ban list.</string>
|
||||
<string name="notice_room_server_acl_updated_allowed">• Server matching %s are now allowed.</string>
|
||||
<string name="notice_room_server_acl_updated_was_allowed">• Server matching %s were removed from the allowed list.</string>
|
||||
<string name="notice_room_server_acl_updated_ip_literals_allowed">• Server matching IP literals are now allowed.</string>
|
||||
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">• Server matching IP literals are now banned.</string>
|
||||
<string name="notice_room_server_acl_updated_no_change">No change.</string>
|
||||
<string name="notice_room_server_acl_allow_is_empty">🎉 All servers are banned from participating! This room can no longer be used.</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string>
|
||||
<string name="notice_requested_voip_conference_by_you">You requested a VoIP conference</string>
|
||||
@ -158,13 +175,22 @@
|
||||
|
||||
<!-- The 2 parameters will be members' name -->
|
||||
<string name="room_displayname_two_members">%1$s and %2$s</string>
|
||||
|
||||
<!-- The 3 parameters will be members' name -->
|
||||
<string name="room_displayname_3_members">%1$s, %2$s and %3$s</string>
|
||||
<!-- The 4 parameters will be members' name -->
|
||||
<string name="room_displayname_4_members">%1$s, %2$s, %3$s and %4$s</string>
|
||||
<!-- The 3 first parameters will be members' name -->
|
||||
<plurals name="room_displayname_four_and_more_members">
|
||||
<item quantity="one">%1$s, %2$s, %3$s and %4$d other</item>
|
||||
<item quantity="other">%1$s, %2$s, %3$s and %4$d others</item>
|
||||
</plurals>
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s and 1 other</item>
|
||||
<item quantity="other">%1$s and %2$d others</item>
|
||||
</plurals>
|
||||
|
||||
<string name="room_displayname_empty_room">Empty room</string>
|
||||
<string name="room_displayname_empty_room_was">Empty room (was %s)</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string>
|
||||
|
@ -19,6 +19,7 @@
|
||||
echo "Configure Element Template..."
|
||||
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
|
||||
{
|
||||
mkdir -p "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
|
||||
ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
|
||||
} && {
|
||||
echo "Please restart Android Studio."
|
||||
|
24
tools/templates/unconfigure.sh
Executable file
24
tools/templates/unconfigure.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Copyright 2020 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Template prevent from upgrading Android Studio, so this script de configure the template
|
||||
echo "Un-configure Element Template..."
|
||||
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
|
||||
|
||||
rm "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other/ElementFeature"
|
||||
rm -r "${ANDROID_STUDIO%/}/plugins/android/lib/templates"
|
@ -136,6 +136,7 @@ class DefaultErrorFormatter @Inject constructor(
|
||||
IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported
|
||||
IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error
|
||||
IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error
|
||||
IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -97,12 +97,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
||||
unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog()
|
||||
viewModelFactory = screenComponent.viewModelFactory()
|
||||
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
|
||||
injectWith(injector())
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
protected open fun injectWith(injector: ScreenComponent) = Unit
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -137,7 +137,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
session.callSignalingService().getCallWithId(it)?.let { mxCall ->
|
||||
this.call = mxCall
|
||||
mxCall.otherUserId
|
||||
val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem()
|
||||
val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()
|
||||
|
||||
mxCall.addListener(callStateListener)
|
||||
|
||||
|
@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.Camera1Enumerator
|
||||
@ -330,8 +331,8 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
currentCall?.mxCall
|
||||
?.takeIf { it.state is CallState.Connected }
|
||||
?.let { mxCall ->
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
?: mxCall.roomId
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
// Start background service with notification
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
@ -388,7 +389,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
val mxCall = callContext.mxCall
|
||||
// Update service state
|
||||
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.roomId
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
@ -576,7 +577,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
?.let { mxCall ->
|
||||
// Start background service with notification
|
||||
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
CallService.onOnGoingCallBackground(
|
||||
context = context,
|
||||
@ -650,7 +651,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
callAudioManager.startForCall(createdCall)
|
||||
currentCall = callContext
|
||||
|
||||
val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(createdCall.otherUserId, createdCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: createdCall.otherUserId
|
||||
CallService.onOutgoingCallRinging(
|
||||
context = context.applicationContext,
|
||||
@ -706,7 +707,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
}
|
||||
|
||||
// Start background service with notification
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
CallService.onIncomingCallRinging(
|
||||
context = context,
|
||||
@ -845,7 +846,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
}
|
||||
val mxCall = call.mxCall
|
||||
// Update service state
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
|
@ -46,7 +46,7 @@ class JitsiCallViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
val me = session.getUser(session.myUserId)?.toMatrixItem()
|
||||
val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem()
|
||||
val userInfo = JitsiMeetUserInfo().apply {
|
||||
displayName = me?.getBestName()
|
||||
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }
|
||||
|
@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||
sealed class ContactsBookAction : VectorViewModelAction {
|
||||
data class FilterWith(val filter: String) : ContactsBookAction()
|
||||
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
||||
object UserConsentGranted : ContactsBookAction()
|
||||
}
|
||||
|
@ -52,11 +52,10 @@ class ContactsBookController @Inject constructor(
|
||||
|
||||
override fun buildModels() {
|
||||
val currentState = state ?: return
|
||||
val hasSearch = currentState.searchTerm.isNotEmpty()
|
||||
when (val asyncMappedContacts = currentState.mappedContacts) {
|
||||
is Uninitialized -> renderEmptyState(false)
|
||||
is Loading -> renderLoading()
|
||||
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
|
||||
is Success -> renderSuccess(currentState)
|
||||
is Fail -> renderFailure(asyncMappedContacts.error)
|
||||
}
|
||||
}
|
||||
@ -75,13 +74,13 @@ class ContactsBookController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSuccess(mappedContacts: List<MappedContact>,
|
||||
hasSearch: Boolean,
|
||||
onlyBoundContacts: Boolean) {
|
||||
private fun renderSuccess(state: ContactsBookViewState) {
|
||||
val mappedContacts = state.filteredMappedContacts
|
||||
|
||||
if (mappedContacts.isEmpty()) {
|
||||
renderEmptyState(hasSearch)
|
||||
renderEmptyState(state.searchTerm.isNotEmpty())
|
||||
} else {
|
||||
renderContacts(mappedContacts, onlyBoundContacts)
|
||||
renderContacts(mappedContacts, state.onlyBoundContacts)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor(
|
||||
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
|
||||
setupRecyclerView()
|
||||
setupFilterView()
|
||||
setupConsentView()
|
||||
setupOnlyBoundContactsView()
|
||||
setupCloseView()
|
||||
}
|
||||
|
||||
private fun setupConsentView() {
|
||||
phoneBookSearchForMatrixContacts.setOnClickListener {
|
||||
withState(contactsBookViewModel) { state ->
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: ""))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOnlyBoundContactsView() {
|
||||
phoneBookOnlyBoundContacts.checkedChanges()
|
||||
.subscribe {
|
||||
@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(contactsBookViewModel) { state ->
|
||||
phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
|
||||
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
|
||||
contactsBookController.setData(state)
|
||||
}
|
||||
|
@ -38,11 +38,10 @@ import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.identity.FoundThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import timber.log.Timber
|
||||
|
||||
private typealias PhoneBookSearch = String
|
||||
|
||||
class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: ContactsBookViewState,
|
||||
private val contactsDataSource: ContactsDataSource,
|
||||
@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
private fun loadContacts() {
|
||||
setState {
|
||||
copy(
|
||||
mappedContacts = Loading()
|
||||
mappedContacts = Loading(),
|
||||
identityServerUrl = session.identityService().getCurrentIdentityServerUrl(),
|
||||
userConsent = session.identityService().getUserConsent()
|
||||
)
|
||||
}
|
||||
|
||||
@ -109,6 +110,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
}
|
||||
|
||||
private fun performLookup(data: List<MappedContact>) {
|
||||
if (!session.identityService().getUserConsent()) {
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val threePids = data.flatMap { contact ->
|
||||
contact.emails.map { ThreePid.Email(it.email) } +
|
||||
@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
}
|
||||
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// Ignore
|
||||
Timber.w(failure, "Unable to perform the lookup")
|
||||
|
||||
// Should not happen, but just to be sure
|
||||
if (failure is IdentityServiceError.UserConsentNotProvided) {
|
||||
setState {
|
||||
copy(userConsent = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: List<FoundThreePid>) {
|
||||
@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
when (action) {
|
||||
is ContactsBookAction.FilterWith -> handleFilterWith(action)
|
||||
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
|
||||
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUserConsentGranted() {
|
||||
session.identityService().setUserConsent(true)
|
||||
|
||||
setState {
|
||||
copy(userConsent = true)
|
||||
}
|
||||
|
||||
// Perform the lookup
|
||||
performLookup(allContacts)
|
||||
}
|
||||
|
||||
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
|
||||
setState {
|
||||
copy(
|
||||
|
@ -26,10 +26,14 @@ data class ContactsBookViewState(
|
||||
val mappedContacts: Async<List<MappedContact>> = Loading(),
|
||||
// Use to filter contacts by display name
|
||||
val searchTerm: String = "",
|
||||
// Tru to display only bound contacts with their bound 2pid
|
||||
// True to display only bound contacts with their bound 2pid
|
||||
val onlyBoundContacts: Boolean = false,
|
||||
// All contacts, filtered by searchTerm and onlyBoundContacts
|
||||
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||
// True when the identity service has return some data
|
||||
val isBoundRetrieved: Boolean = false
|
||||
val isBoundRetrieved: Boolean = false,
|
||||
// The current identity server url if any
|
||||
val identityServerUrl: String? = null,
|
||||
// User consent to perform lookup (send emails to the identity server)
|
||||
val userConsent: Boolean = false
|
||||
) : MvRxState
|
||||
|
@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
|
||||
|
||||
object DisconnectIdentityServer : DiscoverySettingsAction()
|
||||
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
|
||||
data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction()
|
||||
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||
|
@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor(
|
||||
buildIdentityServerSection(data)
|
||||
val hasIdentityServer = data.identityServer().isNullOrBlank().not()
|
||||
if (hasIdentityServer && !data.termsNotSigned) {
|
||||
buildConsentSection(data)
|
||||
buildEmailsSection(data.emailList)
|
||||
buildMsisdnSection(data.phoneNumbersList)
|
||||
}
|
||||
@ -72,6 +73,38 @@ class DiscoverySettingsController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConsentSection(data: DiscoverySettingsState) {
|
||||
settingsSectionTitleItem {
|
||||
id("idConsentTitle")
|
||||
titleResId(R.string.settings_discovery_consent_title)
|
||||
}
|
||||
|
||||
if (data.userConsent) {
|
||||
settingsInfoItem {
|
||||
id("idConsentInfo")
|
||||
helperTextResId(R.string.settings_discovery_consent_notice_on)
|
||||
}
|
||||
settingsButtonItem {
|
||||
id("idConsentButton")
|
||||
colorProvider(colorProvider)
|
||||
buttonTitleId(R.string.settings_discovery_consent_action_revoke)
|
||||
buttonStyle(ButtonStyle.DESTRUCTIVE)
|
||||
buttonClickListener { listener?.onTapUpdateUserConsent(false) }
|
||||
}
|
||||
} else {
|
||||
settingsInfoItem {
|
||||
id("idConsentInfo")
|
||||
helperTextResId(R.string.settings_discovery_consent_notice_off)
|
||||
}
|
||||
settingsButtonItem {
|
||||
id("idConsentButton")
|
||||
colorProvider(colorProvider)
|
||||
buttonTitleId(R.string.settings_discovery_consent_action_give_consent)
|
||||
buttonClickListener { listener?.onTapUpdateUserConsent(true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildIdentityServerSection(data: DiscoverySettingsState) {
|
||||
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none)
|
||||
|
||||
@ -359,6 +392,7 @@ class DiscoverySettingsController @Inject constructor(
|
||||
fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
|
||||
fun onTapChangeIdentityServer()
|
||||
fun onTapDisconnectIdentityServer()
|
||||
fun onTapUpdateUserConsent(newValue: Boolean)
|
||||
fun onTapRetryToRetrieveBindings()
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +170,23 @@ class DiscoverySettingsFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTapUpdateUserConsent(newValue: Boolean) {
|
||||
if (newValue) {
|
||||
withState(viewModel) { state ->
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke()))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true))
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTapRetryToRetrieveBindings() {
|
||||
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
|
||||
}
|
||||
|
@ -25,5 +25,6 @@ data class DiscoverySettingsState(
|
||||
val emailList: Async<List<PidInfo>> = Uninitialized,
|
||||
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
|
||||
// Can be true if terms are updated
|
||||
val termsNotSigned: Boolean = false
|
||||
val termsNotSigned: Boolean = false,
|
||||
val userConsent: Boolean = false
|
||||
) : MvRxState
|
||||
|
@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
||||
val currentIS = state.identityServer()
|
||||
setState {
|
||||
copy(identityServer = Success(identityServerUrl))
|
||||
copy(
|
||||
identityServer = Success(identityServerUrl),
|
||||
userConsent = false
|
||||
)
|
||||
}
|
||||
if (currentIS != identityServerUrl) retrieveBinding()
|
||||
}
|
||||
@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
|
||||
init {
|
||||
setState {
|
||||
copy(identityServer = Success(identityService.getCurrentIdentityServerUrl()))
|
||||
copy(
|
||||
identityServer = Success(identityService.getCurrentIdentityServerUrl()),
|
||||
userConsent = identityService.getUserConsent()
|
||||
)
|
||||
}
|
||||
startListenToIdentityManager()
|
||||
observeThreePids()
|
||||
@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
|
||||
DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer()
|
||||
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
|
||||
is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action)
|
||||
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
|
||||
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
|
||||
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true)
|
||||
@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) {
|
||||
identityService.setUserConsent(action.newConsent)
|
||||
setState { copy(userConsent = action.newConsent) }
|
||||
}
|
||||
|
||||
private fun disconnectIdentityServer() {
|
||||
setState { copy(identityServer = Loading()) }
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
awaitCallback<Unit> { session.identityService().disconnect(it) }
|
||||
setState { copy(identityServer = Success(null)) }
|
||||
setState {
|
||||
copy(
|
||||
identityServer = Success(null),
|
||||
userConsent = false
|
||||
)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(identityServer = Fail(failure)) }
|
||||
}
|
||||
@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
val data = awaitCallback<String?> {
|
||||
session.identityService().setNewIdentityServer(action.url, it)
|
||||
}
|
||||
setState { copy(identityServer = Success(data)) }
|
||||
setState {
|
||||
copy(
|
||||
identityServer = Success(data),
|
||||
userConsent = false
|
||||
)
|
||||
}
|
||||
retrieveBinding()
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(identityServer = Fail(failure)) }
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package im.vector.app.features.grouplist
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
@ -28,7 +29,7 @@ import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams
|
||||
@ -95,7 +96,9 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
|
||||
private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state ->
|
||||
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
|
||||
// We take care of refreshing group data when selecting to be sure we get all the rooms and users
|
||||
session.getGroup(action.groupSummary.groupId)?.fetchGroupData(NoOpMatrixCallback())
|
||||
viewModelScope.launch {
|
||||
session.getGroup(action.groupSummary.groupId)?.fetchGroupData()
|
||||
}
|
||||
setState { copy(selectedGroup = action.groupSummary) }
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package im.vector.app.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
@ -24,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||
@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
|
||||
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
|
||||
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
|
||||
object QuickActionInvitePeople : RoomDetailAction()
|
||||
object QuickActionSetAvatar : RoomDetailAction()
|
||||
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
|
||||
object QuickActionSetTopic : RoomDetailAction()
|
||||
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ import com.google.android.material.textfield.TextInputEditText
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||
import im.vector.app.core.dialogs.withColoredButton
|
||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
@ -82,6 +83,7 @@ import im.vector.app.core.extensions.showKeyboard
|
||||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.intent.getFilenameFromUri
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
@ -141,6 +143,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsB
|
||||
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillImageSpan
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.invite.VectorInviteView
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
@ -149,6 +152,7 @@ import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.permalink.NavigationInterceptor
|
||||
import im.vector.app.features.permalink.PermalinkHandler
|
||||
import im.vector.app.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.app.features.roomprofile.RoomProfileActivity
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import im.vector.app.features.share.SharedData
|
||||
@ -196,6 +200,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -221,7 +226,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory
|
||||
) :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
@ -229,7 +235,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
AttachmentsHelper.Callback,
|
||||
// RoomWidgetsBannerView.Callback,
|
||||
GalleryOrCameraDialogHelper.Listener,
|
||||
ActiveCallView.Callback {
|
||||
|
||||
companion object {
|
||||
@ -250,10 +256,15 @@ class RoomDetailFragment @Inject constructor(
|
||||
private const val ircPattern = " (IRC)"
|
||||
}
|
||||
|
||||
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
|
||||
|
||||
private val roomDetailArgs: RoomDetailArgs by args()
|
||||
private val glideRequests by lazy {
|
||||
GlideApp.with(this)
|
||||
}
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(roomDetailArgs.roomId)
|
||||
}
|
||||
|
||||
private val autoCompleter: AutoCompleter by lazy {
|
||||
autoCompleterFactory.create(roomDetailArgs.roomId)
|
||||
@ -364,6 +375,12 @@ class RoomDetailFragment @Inject constructor(
|
||||
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
|
||||
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
|
||||
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
|
||||
RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId)
|
||||
RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
|
||||
RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings()
|
||||
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
|
||||
navigator.openBigImageViewer(requireActivity(), it.view, item)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@ -372,6 +389,24 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri?) {
|
||||
uri ?: return
|
||||
roomDetailViewModel.handle(
|
||||
RoomDetailAction.SetAvatarAction(
|
||||
newAvatarUri = uri,
|
||||
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOpenRoomSettings() {
|
||||
navigator.openRoomProfile(
|
||||
requireContext(),
|
||||
roomDetailArgs.roomId,
|
||||
RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
|
||||
navigator.openRoom(requireContext(), openRoom.roomId, null)
|
||||
}
|
||||
@ -848,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
||||
formattedBody = eventHtmlRenderer.render(document)
|
||||
formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
|
||||
|
||||
|
@ -17,10 +17,12 @@
|
||||
package im.vector.app.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.command.Command
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import java.io.File
|
||||
|
||||
@ -43,6 +45,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
|
||||
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
|
||||
|
||||
object OpenInvitePeople : RoomDetailViewEvents()
|
||||
object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
|
||||
object OpenRoomSettings : RoomDetailViewEvents()
|
||||
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents()
|
||||
|
||||
object ShowWaitingView : RoomDetailViewEvents()
|
||||
object HideWaitingView : RoomDetailViewEvents()
|
||||
|
||||
|
@ -50,6 +50,7 @@ import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.commonmark.parser.Parser
|
||||
@ -99,6 +100,7 @@ import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.lang.Exception
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@ -164,7 +166,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
getUnreadState()
|
||||
observeSyncState()
|
||||
observeEventDisplayedActions()
|
||||
getDraftIfAny()
|
||||
loadDraftIfAny()
|
||||
observeUnreadState()
|
||||
observeMyRoomMember()
|
||||
observeActiveRoomWidgets()
|
||||
@ -275,9 +277,39 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
_viewEvents.post(
|
||||
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
||||
)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it)
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInvitePeople() {
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenInvitePeople)
|
||||
}
|
||||
|
||||
private fun handleQuickSetAvatar() {
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog)
|
||||
}
|
||||
|
||||
private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
|
||||
val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId)
|
||||
if (existingDmRoomId == null) {
|
||||
@ -475,28 +507,30 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
* Convert a send mode to a draft and save the draft
|
||||
*/
|
||||
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState {
|
||||
viewModelScope.launch(NonCancellable) {
|
||||
when {
|
||||
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
|
||||
room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback())
|
||||
room.saveDraft(UserDraft.REGULAR(action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.REPLY -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
|
||||
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.QUOTE -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
|
||||
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.EDIT -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
|
||||
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDraftIfAny() {
|
||||
val currentDraft = room.getDraft() ?: return
|
||||
private fun loadDraftIfAny() {
|
||||
val currentDraft = room.getDraft()
|
||||
setState {
|
||||
copy(
|
||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||
@ -517,6 +551,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
SendMode.EDIT(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: SendMode.REGULAR("", fromSharing = false)
|
||||
)
|
||||
}
|
||||
@ -772,11 +807,13 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
private fun popDraft() = withState {
|
||||
if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) {
|
||||
// If we were sharing, we want to get back our last value from draft
|
||||
getDraftIfAny()
|
||||
loadDraftIfAny()
|
||||
} else {
|
||||
// Otherwise we clear the composer and remove the draft from db
|
||||
setState { copy(sendMode = SendMode.REGULAR("", false)) }
|
||||
room.deleteDraft(NoOpMatrixCallback())
|
||||
viewModelScope.launch {
|
||||
room.deleteDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1111,15 +1148,15 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun handleReportContent(action: RoomDetailAction.ReportContent) {
|
||||
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
|
||||
viewModelScope.launch {
|
||||
val event = try {
|
||||
room.reportContent(action.eventId, -100, action.reason)
|
||||
RoomDetailViewEvents.ActionSuccess(action)
|
||||
} catch (failure: Exception) {
|
||||
RoomDetailViewEvents.ActionFailure(action, failure)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
|
||||
_viewEvents.post(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) {
|
||||
@ -1300,7 +1337,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
if (summary.membership == Membership.INVITE) {
|
||||
summary.inviterId?.let { inviterId ->
|
||||
session.getUser(inviterId)
|
||||
session.getRoomMember(inviterId, summary.roomId)
|
||||
}?.also {
|
||||
setState { copy(asyncInviter = Success(it)) }
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
|
||||
/**
|
||||
@ -60,7 +59,7 @@ data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
|
||||
val typingMessage: String? = null,
|
||||
|
@ -123,7 +123,7 @@ class SearchResultController @Inject constructor(
|
||||
.formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
|
||||
.spannable(spannable)
|
||||
.sender(eventAndSender.sender
|
||||
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
|
||||
?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem())
|
||||
.listener { listener?.onItemClicked(eventAndSender.event) }
|
||||
.let { result.add(it) }
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.reactions.data.EmojiDataSource
|
||||
@ -57,18 +58,22 @@ import java.util.ArrayList
|
||||
* Information related to an event and used to display preview in contextual bottom sheet.
|
||||
*/
|
||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: MessageActionState,
|
||||
private val initialState: MessageActionState,
|
||||
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
|
||||
private val htmlCompressor: VectorHtmlCompressor,
|
||||
private val session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
private val eventId = initialState.eventId
|
||||
private val informationData = initialState.informationData
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(initialState.roomId)
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
@ -172,7 +177,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
?.let { htmlCompressor.compress(it) }
|
||||
?: messageContent.body
|
||||
|
||||
eventHtmlRenderer.get().render(html)
|
||||
eventHtmlRenderer.get().render(html, pillsPostProcessor)
|
||||
} else if (messageContent is MessageVerificationRequestContent) {
|
||||
stringProvider.getString(R.string.verification_request)
|
||||
} else {
|
||||
@ -186,6 +191,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
EventType.STATE_ROOM_ALIASES,
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
EventType.STATE_ROOM_SERVER_ACL,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
|
@ -31,10 +31,14 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
|
||||
@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
collapsedEventIds.removeAll(mergedEventIds)
|
||||
}
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||
val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId
|
||||
?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) }
|
||||
?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel<PowerLevelsContent>() }
|
||||
?.let { PowerLevelsHelper(it) }
|
||||
val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
|
||||
val attributes = MergedRoomCreationItem.Attributes(
|
||||
isCollapsed = isCollapsed,
|
||||
mergeData = mergedData,
|
||||
@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
hasEncryptionEvent = hasEncryption,
|
||||
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
readReceiptsCallback = callback,
|
||||
currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
|
||||
callback = callback,
|
||||
currentUserId = currentUserId,
|
||||
roomSummary = roomSummaryHolder.roomSummary,
|
||||
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
|
||||
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
|
||||
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
|
||||
)
|
||||
MergedRoomCreationItem_()
|
||||
.id(mergeId)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(isCollapsed && highlighted)
|
||||
.attributes(attributes)
|
||||
.movementMethod(createLinkMovementMethod(callback))
|
||||
.also {
|
||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import im.vector.app.features.html.CodeVisitor
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
@ -106,15 +107,19 @@ class MessageItemFactory @Inject constructor(
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val session: Session) {
|
||||
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId)
|
||||
}
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.isRedacted()) {
|
||||
@ -217,13 +222,17 @@ class MessageItemFactory @Inject constructor(
|
||||
attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
|
||||
// If this request is not sent by me or sent to me, we should ignore it in timeline
|
||||
val myUserId = session.myUserId
|
||||
val roomId = roomSummaryHolder.roomSummary?.roomId
|
||||
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
|
||||
return null
|
||||
}
|
||||
|
||||
val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
|
||||
val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName
|
||||
else informationData.memberName
|
||||
val otherUserName = if (informationData.sentByMe) {
|
||||
session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName
|
||||
} else {
|
||||
informationData.memberName
|
||||
}
|
||||
return VerificationRequestItem_()
|
||||
.attributes(
|
||||
VerificationRequestItem.Attributes(
|
||||
@ -393,7 +402,7 @@ class MessageItemFactory @Inject constructor(
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
|
||||
val formattedBody = htmlRenderer.get().render(compressed)
|
||||
val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor)
|
||||
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
|
||||
}
|
||||
|
||||
@ -528,7 +537,7 @@ class MessageItemFactory @Inject constructor(
|
||||
private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
|
||||
return matrixFormattedBody
|
||||
?.let { htmlCompressor.compress(it) }
|
||||
?.let { htmlRenderer.get().render(it) }
|
||||
?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
|
||||
?: body
|
||||
}
|
||||
|
||||
|
@ -57,6 +57,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
EventType.STATE_ROOM_JOIN_RULES,
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
EventType.STATE_ROOM_SERVER_ACL,
|
||||
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
EventType.STATE_ROOM_WIDGET_LEGACY,
|
||||
EventType.STATE_ROOM_WIDGET,
|
||||
|
@ -19,6 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.format
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.extensions.appendNl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
@ -35,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
@ -48,9 +51,12 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
|
||||
class NoticeEventFormatter @Inject constructor(
|
||||
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val sp: StringProvider) {
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val sp: StringProvider
|
||||
) {
|
||||
|
||||
private val currentUserId: String?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
|
||||
@ -72,6 +78,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY ->
|
||||
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_WIDGET,
|
||||
@ -383,6 +390,79 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? {
|
||||
val eventContent = event.getClearContent().toModel<RoomServerAclContent>() ?: return null
|
||||
val prevEventContent = event.resolvedPrevContent()?.toModel<RoomServerAclContent>()
|
||||
|
||||
return buildString {
|
||||
// Title
|
||||
append(if (prevEventContent == null) {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_server_acl_set_title_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_server_acl_set_title, senderName)
|
||||
}
|
||||
} else {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_server_acl_updated_title_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_server_acl_updated_title, senderName)
|
||||
}
|
||||
})
|
||||
if (eventContent.allowList.isEmpty()) {
|
||||
// Special case for stuck room
|
||||
appendNl(sp.getString(R.string.notice_room_server_acl_allow_is_empty))
|
||||
} else if (vectorPreferences.developerMode()) {
|
||||
// Details, only in developer mode
|
||||
appendAclDetails(eventContent, prevEventContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendAclDetails(eventContent: RoomServerAclContent, prevEventContent: RoomServerAclContent?) {
|
||||
if (prevEventContent == null) {
|
||||
eventContent.allowList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_allowed, it)) }
|
||||
eventContent.denyList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_banned, it)) }
|
||||
if (eventContent.allowIpLiterals) {
|
||||
appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_allowed))
|
||||
} else {
|
||||
appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_not_allowed))
|
||||
}
|
||||
} else {
|
||||
// Display only diff
|
||||
var hasChanged = false
|
||||
// New allowed servers
|
||||
(eventContent.allowList - prevEventContent.allowList)
|
||||
.also { hasChanged = hasChanged || it.isNotEmpty() }
|
||||
.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_allowed, it)) }
|
||||
// Removed allowed servers
|
||||
(prevEventContent.allowList - eventContent.allowList)
|
||||
.also { hasChanged = hasChanged || it.isNotEmpty() }
|
||||
.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_allowed, it)) }
|
||||
// New denied servers
|
||||
(eventContent.denyList - prevEventContent.denyList)
|
||||
.also { hasChanged = hasChanged || it.isNotEmpty() }
|
||||
.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_banned, it)) }
|
||||
// Removed denied servers
|
||||
(prevEventContent.denyList - eventContent.denyList)
|
||||
.also { hasChanged = hasChanged || it.isNotEmpty() }
|
||||
.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_banned, it)) }
|
||||
|
||||
if (prevEventContent.allowIpLiterals != eventContent.allowIpLiterals) {
|
||||
hasChanged = true
|
||||
if (eventContent.allowIpLiterals) {
|
||||
appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_allowed))
|
||||
} else {
|
||||
appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_not_allowed))
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasChanged) {
|
||||
appendNl(sp.getString(R.string.notice_room_server_acl_updated_no_change))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? {
|
||||
val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel()
|
||||
val canonicalAlias = eventContent?.canonicalAlias
|
||||
|
@ -33,6 +33,7 @@ object TimelineDisplayableEvents {
|
||||
EventType.STATE_ROOM_ALIASES,
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
EventType.STATE_ROOM_SERVER_ACL,
|
||||
EventType.STATE_ROOM_POWER_LEVELS,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
|
@ -16,11 +16,14 @@
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.text.method.MovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
@ -28,8 +31,16 @@ import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.utils.DebouncedClickListener
|
||||
import im.vector.app.core.utils.tappableMatchingText
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
|
||||
@ -37,11 +48,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
@EpoxyAttribute
|
||||
override lateinit var attributes: Attributes
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var movementMethod: MovementMethod? = null
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
bindCreationSummaryTile(holder)
|
||||
|
||||
if (attributes.isCollapsed) {
|
||||
// Take the oldest data
|
||||
val data = distinctMergeData.lastOrNull()
|
||||
@ -70,9 +86,20 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
holder.avatarView.visibility = View.GONE
|
||||
}
|
||||
|
||||
bindEncryptionTile(holder, data)
|
||||
} else {
|
||||
holder.avatarView.visibility = View.INVISIBLE
|
||||
holder.summaryView.visibility = View.GONE
|
||||
holder.encryptionTile.isGone = true
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindEncryptionTile(holder: Holder, data: Data?) {
|
||||
if (attributes.hasEncryptionEvent) {
|
||||
holder.encryptionTile.isVisible = true
|
||||
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
this.marginEnd = leftGuideline
|
||||
}
|
||||
if (attributes.isEncryptionAlgorithmSecure) {
|
||||
@ -98,13 +125,78 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
} else {
|
||||
holder.encryptionTile.isVisible = false
|
||||
}
|
||||
} else {
|
||||
holder.avatarView.visibility = View.INVISIBLE
|
||||
holder.summaryView.visibility = View.GONE
|
||||
holder.encryptionTile.isGone = true
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
|
||||
private fun bindCreationSummaryTile(holder: Holder) {
|
||||
val roomSummary = attributes.roomSummary
|
||||
val roomDisplayName = roomSummary?.displayName
|
||||
holder.roomNameText.setTextOrHide(roomDisplayName)
|
||||
val isDirect = roomSummary?.isDirect == true
|
||||
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
|
||||
|
||||
if (isDirect) {
|
||||
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName ?: "")
|
||||
} else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) {
|
||||
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
|
||||
} else {
|
||||
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
|
||||
}
|
||||
|
||||
val topic = roomSummary?.topic
|
||||
if (topic.isNullOrBlank()) {
|
||||
// do not show hint for DMs or group DMs
|
||||
if (!isDirect) {
|
||||
val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
|
||||
val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
|
||||
holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
holder.roomTopicText.setTextOrHide(
|
||||
span {
|
||||
span(holder.view.resources.getString(R.string.topic_prefix)) {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+topic.linkify(attributes.callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
holder.roomTopicText.movementMethod = movementMethod
|
||||
|
||||
val roomItem = roomSummary?.toMatrixItem()
|
||||
val shouldSetAvatar = attributes.canChangeAvatar
|
||||
&& (roomSummary?.isDirect == false || (isDirect && membersCount >= 2))
|
||||
&& roomItem?.avatarUrl.isNullOrBlank()
|
||||
|
||||
holder.roomAvatarImageView.isVisible = roomItem != null
|
||||
if (roomItem != null) {
|
||||
attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView)
|
||||
holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view ->
|
||||
if (shouldSetAvatar) {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
|
||||
} else {
|
||||
// Note: this is no op if there is no avatar on the room
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
holder.setAvatarButton.isVisible = shouldSetAvatar
|
||||
if (shouldSetAvatar) {
|
||||
holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ ->
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
|
||||
}))
|
||||
}
|
||||
|
||||
holder.addPeopleButton.isVisible = !isDirect
|
||||
if (!isDirect) {
|
||||
holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ ->
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||
@ -114,6 +206,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
|
||||
val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
|
||||
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
|
||||
|
||||
val roomNameText by bind<TextView>(R.id.roomNameTileText)
|
||||
val roomDescriptionText by bind<TextView>(R.id.roomNameDescriptionText)
|
||||
val roomTopicText by bind<TextView>(R.id.roomNameTopicText)
|
||||
val roomAvatarImageView by bind<ImageView>(R.id.creationTileRoomAvatarImageView)
|
||||
val addPeopleButton by bind<View>(R.id.creationTileAddPeopleButton)
|
||||
val setAvatarButton by bind<View>(R.id.creationTileSetAvatarButton)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -126,8 +225,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||
val callback: TimelineEventController.Callback? = null,
|
||||
val currentUserId: String,
|
||||
val hasEncryptionEvent: Boolean,
|
||||
val isEncryptionAlgorithmSecure: Boolean
|
||||
val isEncryptionAlgorithmSecure: Boolean,
|
||||
val roomSummary: RoomSummary?,
|
||||
val canChangeAvatar: Boolean = false,
|
||||
val canChangeName: Boolean = false,
|
||||
val canChangeTopic: Boolean = false
|
||||
) : BasedMergedItem.Attributes
|
||||
}
|
||||
|
@ -33,9 +33,9 @@ import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import timber.log.Timber
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
||||
@ -169,11 +169,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
||||
}
|
||||
|
||||
private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) {
|
||||
session.getRoom(action.roomId)?.setRoomNotificationState(action.notificationState, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
val room = session.getRoom(action.roomId)
|
||||
if (room != null) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
room.setRoomNotificationState(action.notificationState)
|
||||
} catch (failure: Exception) {
|
||||
_viewEvents.post(RoomListViewEvents.Failure(failure))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleToggleTag(action: RoomListAction.ToggleTag) {
|
||||
@ -185,17 +190,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
||||
action.tag.otherTag()
|
||||
?.takeIf { room.roomSummary()?.hasTag(it).orFalse() }
|
||||
?.let { tagToRemove ->
|
||||
awaitCallback<Unit> { room.deleteTag(tagToRemove, it) }
|
||||
room.deleteTag(tagToRemove)
|
||||
}
|
||||
|
||||
// Set the tag. We do not handle the order for the moment
|
||||
awaitCallback<Unit> {
|
||||
room.addTag(action.tag, 0.5, it)
|
||||
}
|
||||
room.addTag(action.tag, 0.5)
|
||||
} else {
|
||||
awaitCallback<Unit> {
|
||||
room.deleteTag(action.tag, it)
|
||||
}
|
||||
room.deleteTag(action.tag)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomListViewEvents.Failure(failure))
|
||||
|
@ -17,21 +17,23 @@
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import android.text.Spannable
|
||||
import androidx.core.text.toSpannable
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.html.TagHandlerNoOp
|
||||
import org.commonmark.node.Node
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class EventHtmlRenderer @Inject constructor(context: Context,
|
||||
htmlConfigure: MatrixHtmlPluginConfigure) {
|
||||
class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfigure,
|
||||
context: Context) {
|
||||
|
||||
interface PostProcessor {
|
||||
fun afterRender(renderedText: Spannable)
|
||||
}
|
||||
|
||||
private val markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create(htmlConfigure))
|
||||
@ -41,35 +43,47 @@ class EventHtmlRenderer @Inject constructor(context: Context,
|
||||
return markwon.parse(text)
|
||||
}
|
||||
|
||||
fun render(text: String): CharSequence {
|
||||
/**
|
||||
* @param text the text you want to render
|
||||
* @param postProcessors an optional array of post processor to add any span if needed
|
||||
*/
|
||||
fun render(text: String, vararg postProcessors: PostProcessor): CharSequence {
|
||||
return try {
|
||||
markwon.toMarkdown(text)
|
||||
val parsed = markwon.parse(text)
|
||||
renderAndProcess(parsed, postProcessors)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to render $text to html")
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
fun render(node: Node): CharSequence? {
|
||||
/**
|
||||
* @param node the node you want to render
|
||||
* @param postProcessors an optional array of post processor to add any span if needed
|
||||
*/
|
||||
fun render(node: Node, vararg postProcessors: PostProcessor): CharSequence? {
|
||||
return try {
|
||||
markwon.render(node)
|
||||
renderAndProcess(node, postProcessors)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to render $node to html")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderAndProcess(node: Node, postProcessors: Array<out PostProcessor>): CharSequence {
|
||||
val renderedText = markwon.render(node).toSpannable()
|
||||
postProcessors.forEach {
|
||||
it.afterRender(renderedText)
|
||||
}
|
||||
return renderedText
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure {
|
||||
|
||||
override fun configureHtml(plugin: HtmlPlugin) {
|
||||
plugin
|
||||
.addHandler(TagHandlerNoOp.create("a"))
|
||||
.addHandler(FontTagHandler())
|
||||
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
|
||||
.addHandler(MxReplyTagHandler())
|
||||
.addHandler(SpanHandler(colorProvider))
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.style.URLSpan
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.tag.LinkHandler
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
class MxLinkTagHandler(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val sessionHolder: ActiveSessionHolder) : LinkHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val link = tag.attributes()["href"]
|
||||
if (link != null) {
|
||||
val permalinkData = PermalinkParser.parse(link)
|
||||
val matrixItem = when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
|
||||
MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl)
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
if (permalinkData.eventId == null) {
|
||||
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
|
||||
if (permalinkData.isRoomAlias) {
|
||||
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
} else {
|
||||
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
}
|
||||
} else {
|
||||
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
||||
null
|
||||
}
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
|
||||
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (matrixItem == null) {
|
||||
super.handle(visitor, renderer, tag)
|
||||
} else {
|
||||
val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
span,
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
URLSpan(link),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
super.handle(visitor, renderer, tag)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import io.noties.markwon.core.spans.LinkSpan
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomId: String?,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val sessionHolder: ActiveSessionHolder)
|
||||
: EventHtmlRenderer.PostProcessor {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(roomId: String?): PillsPostProcessor
|
||||
}
|
||||
|
||||
override fun afterRender(renderedText: Spannable) {
|
||||
addPillSpans(renderedText, roomId)
|
||||
}
|
||||
|
||||
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
|
||||
// We let markdown handle links and then we add PillImageSpan if needed.
|
||||
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
|
||||
linkSpans.forEach { linkSpan ->
|
||||
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
||||
val startSpan = renderedText.getSpanStart(linkSpan)
|
||||
val endSpan = renderedText.getSpanEnd(linkSpan)
|
||||
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
|
||||
val permalinkData = PermalinkParser.parse(url)
|
||||
val matrixItem = when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
if (roomId == null) {
|
||||
sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem()
|
||||
} else {
|
||||
sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem()
|
||||
}
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
if (permalinkData.eventId == null) {
|
||||
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
|
||||
if (permalinkData.isRoomAlias) {
|
||||
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
} else {
|
||||
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
}
|
||||
} else {
|
||||
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
||||
null
|
||||
}
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
|
||||
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
|
||||
}
|
||||
else -> null
|
||||
} ?: return null
|
||||
return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ import im.vector.app.core.platform.ButtonStateView
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import kotlinx.android.synthetic.main.vector_invite_view.view.*
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -73,7 +73,7 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||
}
|
||||
}
|
||||
|
||||
fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
|
||||
fun render(sender: RoomMemberSummary, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
|
||||
if (mode == Mode.LARGE) {
|
||||
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
|
||||
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)
|
||||
|
@ -69,6 +69,11 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
|
||||
}
|
||||
|
||||
override fun showFailure(throwable: Throwable) {
|
||||
// Only the resumed Fragment can eventually show the error, to avoid multiple dialog display
|
||||
if (!isResumed) {
|
||||
return
|
||||
}
|
||||
|
||||
when (throwable) {
|
||||
is Failure.Cancelled ->
|
||||
/* Ignore this error, user has cancelled the action */
|
||||
|
@ -207,7 +207,6 @@ class LoginViewModel @AssistedInject constructor(
|
||||
private fun handleCheckIfEmailHasBeenValidated(action: LoginAction.CheckIfEmailHasBeenValidated) {
|
||||
// We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
|
||||
currentTask?.cancel()
|
||||
currentTask = null
|
||||
currentTask = registrationWizard?.checkIfEmailHasBeenValidated(action.delayMillis, registrationCallback)
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
@ -96,15 +97,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
|
||||
fun render(data: Data, mode: Mode, imageView: ImageView) {
|
||||
val size = processSize(data, mode)
|
||||
imageView.layoutParams.width = size.width
|
||||
imageView.layoutParams.height = size.height
|
||||
imageView.updateLayoutParams {
|
||||
width = size.width
|
||||
height = size.height
|
||||
}
|
||||
// a11y
|
||||
imageView.contentDescription = data.filename
|
||||
|
||||
createGlideRequest(data, mode, imageView, size)
|
||||
.dontAnimate()
|
||||
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
|
||||
.thumbnail(0.3f)
|
||||
// .thumbnail(0.3f)
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
@ -117,6 +120,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Attachment Viewer
|
||||
*/
|
||||
fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
|
||||
val req = if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
|
@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor(
|
||||
context.startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
|
||||
override fun openRoomProfile(context: Context, roomId: String) {
|
||||
context.startActivity(RoomProfileActivity.newIntent(context, roomId))
|
||||
override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) {
|
||||
context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
|
||||
}
|
||||
|
||||
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {
|
||||
|
@ -78,7 +78,7 @@ interface Navigator {
|
||||
|
||||
fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false)
|
||||
|
||||
fun openRoomProfile(context: Context, roomId: String)
|
||||
fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null)
|
||||
|
||||
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)
|
||||
|
||||
|
@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
||||
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
|
||||
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
||||
val roomId = event.roomId ?: return null
|
||||
val dName = event.senderId?.let { session.getUser(it)?.displayName }
|
||||
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
|
||||
if (Membership.INVITE == content.membership) {
|
||||
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
|
||||
?: stringProvider.getString(R.string.notification_new_invitation)
|
||||
|
@ -120,7 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||
null,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
session.getUser(session.myUserId)?.displayName
|
||||
session.getRoomMember(session.myUserId, room.roomId)?.displayName
|
||||
?: context?.getString(R.string.notification_sender_me),
|
||||
session.myUserId,
|
||||
message,
|
||||
|
@ -18,10 +18,9 @@ package im.vector.app.features.raw.wellknown
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
|
||||
suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? {
|
||||
return tryOrNull { awaitCallback<String> { getWellknown(userId, it) } }
|
||||
return tryOrNull { getWellknown(userId) }
|
||||
?.let { ElementWellKnownMapper.from(it) }
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
|
||||
sealed class RoomProfileAction : VectorViewModelAction {
|
||||
object EnableEncryption : RoomProfileAction()
|
||||
object LeaveRoom : RoomProfileAction()
|
||||
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
|
||||
object ShareRoomProfile : RoomProfileAction()
|
||||
|
@ -46,10 +46,16 @@ class RoomProfileActivity :
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, roomId: String): Intent {
|
||||
private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
|
||||
|
||||
const val EXTRA_DIRECT_ACCESS_ROOM_ROOT = 0
|
||||
const val EXTRA_DIRECT_ACCESS_ROOM_SETTINGS = 1
|
||||
|
||||
fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent {
|
||||
val roomProfileArgs = RoomProfileArgs(roomId)
|
||||
return Intent(context, RoomProfileActivity::class.java).apply {
|
||||
putExtra(MvRx.KEY_ARG, roomProfileArgs)
|
||||
putExtra(EXTRA_DIRECT_ACCESS, directAccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,7 +86,13 @@ class RoomProfileActivity :
|
||||
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
|
||||
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
|
||||
if (isFirstCreation()) {
|
||||
when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) {
|
||||
EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> {
|
||||
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
|
||||
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs)
|
||||
}
|
||||
else -> addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
|
||||
}
|
||||
}
|
||||
sharedActionViewModel
|
||||
.observe()
|
||||
|
@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericFooterItem
|
||||
import im.vector.app.features.home.ShortcutCreator
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomProfileController @Inject constructor(
|
||||
@ -43,6 +44,7 @@ class RoomProfileController @Inject constructor(
|
||||
|
||||
interface Callback {
|
||||
fun onLearnMoreClicked()
|
||||
fun onEnableEncryptionClicked()
|
||||
fun onMemberListClicked()
|
||||
fun onBannedMemberListClicked()
|
||||
fun onNotificationsClicked()
|
||||
@ -84,6 +86,7 @@ class RoomProfileController @Inject constructor(
|
||||
centered(false)
|
||||
text(stringProvider.getString(learnMoreSubtitle))
|
||||
}
|
||||
buildEncryptionAction(data.actionPermissions, roomSummary)
|
||||
|
||||
// More
|
||||
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
|
||||
@ -171,4 +174,29 @@ class RoomProfileController @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEncryptionAction(actionPermissions: RoomProfileViewState.ActionPermissions, roomSummary: RoomSummary) {
|
||||
if (!roomSummary.isEncrypted) {
|
||||
if (actionPermissions.canEnableEncryption) {
|
||||
buildProfileAction(
|
||||
id = "enableEncryption",
|
||||
title = stringProvider.getString(R.string.room_settings_enable_encryption),
|
||||
dividerColor = dividerColor,
|
||||
icon = R.drawable.ic_shield_black,
|
||||
divider = false,
|
||||
editable = false,
|
||||
action = { callback?.onEnableEncryptionClicked() }
|
||||
)
|
||||
} else {
|
||||
buildProfileAction(
|
||||
id = "enableEncryption",
|
||||
title = stringProvider.getString(R.string.room_settings_enable_encryption_no_permission),
|
||||
dividerColor = dividerColor,
|
||||
icon = R.drawable.ic_shield_black,
|
||||
divider = false,
|
||||
editable = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
|
||||
import im.vector.app.features.media.BigImageViewerActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
@ -87,6 +88,7 @@ class RoomProfileFragment @Inject constructor(
|
||||
it.layoutResource = R.layout.view_stub_room_profile_header
|
||||
it.inflate()
|
||||
}
|
||||
setupWaitingView()
|
||||
setupToolbar(matrixProfileToolbar)
|
||||
setupRecyclerView()
|
||||
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(
|
||||
@ -111,6 +113,11 @@ class RoomProfileFragment @Inject constructor(
|
||||
setupLongClicks()
|
||||
}
|
||||
|
||||
private fun setupWaitingView() {
|
||||
waiting_view_status_text.setText(R.string.please_wait)
|
||||
waiting_view_status_text.isVisible = true
|
||||
}
|
||||
|
||||
private fun setupLongClicks() {
|
||||
roomProfileNameView.copyOnLongClick()
|
||||
roomProfileAliasView.copyOnLongClick()
|
||||
@ -155,6 +162,8 @@ class RoomProfileFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(roomProfileViewModel) { state ->
|
||||
waiting_view.isVisible = state.isLoading
|
||||
|
||||
state.roomSummary()?.also {
|
||||
if (it.membership.isLeft()) {
|
||||
Timber.w("The room has been left")
|
||||
@ -187,6 +196,17 @@ class RoomProfileFragment @Inject constructor(
|
||||
vectorBaseActivity.notImplemented()
|
||||
}
|
||||
|
||||
override fun onEnableEncryptionClicked() {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.room_settings_enable_encryption_dialog_title)
|
||||
.setMessage(R.string.room_settings_enable_encryption_dialog_content)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ ->
|
||||
roomProfileViewModel.handle(RoomProfileAction.EnableEncryption)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onMemberListClicked() {
|
||||
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers)
|
||||
}
|
||||
|
@ -28,12 +28,15 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.ShortcutCreator
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.rx.RxRoom
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
@ -65,6 +68,7 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
val rxRoom = room.rx()
|
||||
observeRoomSummary(rxRoom)
|
||||
observeBannedRoomMembers(rxRoom)
|
||||
observePermissions()
|
||||
}
|
||||
|
||||
private fun observeRoomSummary(rxRoom: RxRoom) {
|
||||
@ -82,8 +86,22 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePermissions() {
|
||||
PowerLevelsObservableFactory(room)
|
||||
.createObservable()
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
val permissions = RoomProfileViewState.ActionPermissions(
|
||||
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
|
||||
)
|
||||
setState { copy(actionPermissions = permissions) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomProfileAction) {
|
||||
when (action) {
|
||||
is RoomProfileAction.EnableEncryption -> handleEnableEncryption()
|
||||
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
|
||||
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
||||
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
|
||||
@ -91,6 +109,24 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleEnableEncryption() {
|
||||
postLoading(true)
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = runCatching { room.enableEncryption() }
|
||||
postLoading(false)
|
||||
result.onFailure { failure ->
|
||||
_viewEvents.post(RoomProfileViewEvents.Failure(failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postLoading(isLoading: Boolean) {
|
||||
setState {
|
||||
copy(isLoading = isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreateShortcut() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withState { state ->
|
||||
@ -102,11 +138,13 @@ class RoomProfileViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) {
|
||||
room.setRoomNotificationState(action.notificationState, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
room.setRoomNotificationState(action.notificationState)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomProfileViewEvents.Failure(failure))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeaveRoom() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user