Merge branch 'develop' into feature/bma/fix_crash

This commit is contained in:
Benoit Marty 2020-11-19 18:44:17 +01:00 committed by GitHub
commit 675e4579ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 1543 additions and 665 deletions

View file

@ -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.

View file

@ -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 🗣:
-

View file

@ -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

View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -71,38 +71,27 @@ class SearchMessagesTest : InstrumentedTest {
commonTestHelper.await(lock)
lock = CountDownLatch(1)
aliceSession
.searchService()
.search(
searchTerm = "lore",
limit = 10,
includeProfile = true,
afterLimit = 0,
beforeLimit = 10,
orderByRecent = true,
nextBatch = null,
roomId = aliceRoomId,
callback = object : MatrixCallback<SearchResult> {
override fun onSuccess(data: SearchResult) {
super.onSuccess(data)
assertTrue(data.results?.size == 2)
assertTrue(
data.results
?.all {
(it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
}.orFalse()
)
lock.countDown()
}
override fun onFailure(failure: Throwable) {
super.onFailure(failure)
fail(failure.localizedMessage)
lock.countDown()
}
}
)
lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
val data = commonTestHelper.runBlockingTest {
aliceSession
.searchService()
.search(
searchTerm = "lore",
limit = 10,
includeProfile = true,
afterLimit = 0,
beforeLimit = 10,
orderByRecent = true,
nextBatch = null,
roomId = aliceRoomId
)
}
assertTrue(data.results?.size == 2)
assertTrue(
data.results
?.all {
(it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
}.orFalse()
)
aliceTimeline.removeAllListeners()
cryptoTestData.cleanUp(commonTestHelper)

View file

@ -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)

View file

@ -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)

View file

@ -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()
}

View file

@ -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"

View file

@ -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()
}

View file

@ -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

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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>>
}

View file

@ -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)
}

View file

@ -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 = "*"
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}

View file

@ -16,9 +16,6 @@
package org.matrix.android.sdk.api.session.search
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
/**
* This interface defines methods to search messages in rooms.
*/
@ -35,15 +32,13 @@ interface SearchService {
* @param beforeLimit how many events before the result are returned.
* @param afterLimit how many events after the result are returned.
* @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned.
* @param callback Callback to get the search result
*/
fun search(searchTerm: String,
roomId: String,
nextBatch: String?,
orderByRecent: Boolean,
limit: Int,
beforeLimit: Int,
afterLimit: Int,
includeProfile: Boolean,
callback: MatrixCallback<SearchResult>): Cancelable
suspend fun search(searchTerm: String,
roomId: String,
nextBatch: String?,
orderByRecent: Boolean,
limit: Int,
beforeLimit: Int,
afterLimit: Int,
includeProfile: Boolean): SearchResult
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
)
}

View file

@ -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

View file

@ -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()

View file

@ -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
)

View file

@ -27,6 +27,8 @@ internal interface IdentityStore {
fun setToken(token: String?)
fun setUserConsent(consent: Boolean)
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
/**

View file

@ -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

View file

@ -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>) {

View file

@ -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
)
}

View file

@ -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 ->

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -26,6 +26,8 @@ import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import io.realm.Realm
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
import javax.inject.Inject
internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) {
@ -46,11 +48,14 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId
val roomMembers = RoomMemberHelper(realm, roomId)
val members = roomMembers.queryActiveRoomMembersEvent().findAll()
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
if (members.size == 1) {
res = members.firstOrNull()?.avatarUrl
} else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.avatarUrl
val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false
if (isDirectRoom) {
if (members.size == 1) {
res = members.firstOrNull()?.avatarUrl
} else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.avatarUrl
}
}
return res
}

View file

@ -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) {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}.roomId
roomId = tryOrNull("## Failed to get roomId from alias") {
executeRequest<RoomAliasDescription>(eventBus) {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}
}?.roomId
Optional.from(roomId)
}
}

View file

@ -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)
}
}

View file

@ -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,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1)
2 -> {
stringProvider.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
}
3 -> {
stringProvider.getString(R.string.room_displayname_3_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
resolveRoomMemberName(otherMembersSubset[2], roomMembers)
)
}
4 -> {
stringProvider.getString(R.string.room_displayname_4_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
resolveRoomMemberName(otherMembersSubset[2], roomMembers),
resolveRoomMemberName(otherMembersSubset[3], roomMembers)
)
}
else -> {
val remainingCount = invitedCount + joinedCount - otherMembersCount + 1
stringProvider.getQuantityString(
R.plurals.room_displayname_four_and_more_members,
remainingCount,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
resolveRoomMemberName(otherMembersSubset[2], roomMembers),
remainingCount
)
}
}
}
return name ?: roomId

View file

@ -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.

View file

@ -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?> {

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -16,40 +16,31 @@
package org.matrix.android.sdk.internal.session.search
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.search.SearchResult
import org.matrix.android.sdk.api.session.search.SearchService
import org.matrix.android.sdk.api.util.Cancelable
import javax.inject.Inject
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
internal class DefaultSearchService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val searchTask: SearchTask
) : SearchService {
override fun search(searchTerm: String,
roomId: String,
nextBatch: String?,
orderByRecent: Boolean,
limit: Int,
beforeLimit: Int,
afterLimit: Int,
includeProfile: Boolean,
callback: MatrixCallback<SearchResult>): Cancelable {
return searchTask
.configureWith(SearchTask.Params(
searchTerm = searchTerm,
roomId = roomId,
nextBatch = nextBatch,
orderByRecent = orderByRecent,
limit = limit,
beforeLimit = beforeLimit,
afterLimit = afterLimit,
includeProfile = includeProfile
)) {
this.callback = callback
}.executeBy(taskExecutor)
override suspend fun search(searchTerm: String,
roomId: String,
nextBatch: String?,
orderByRecent: Boolean,
limit: Int,
beforeLimit: Int,
afterLimit: Int,
includeProfile: Boolean): SearchResult {
return searchTask.execute(SearchTask.Params(
searchTerm = searchTerm,
roomId = roomId,
nextBatch = nextBatch,
orderByRecent = orderByRecent,
limit = limit,
beforeLimit = beforeLimit,
afterLimit = afterLimit,
includeProfile = includeProfile
))
}
}

View file

@ -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>
}

View file

@ -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
}
}

View file

@ -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>

View file

@ -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
View 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"

View file

@ -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
})
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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) }

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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(

View file

@ -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

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)) }

View file

@ -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) }
}
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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()

View file

@ -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 {
when {
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.REPLY -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.QUOTE -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.EDIT -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
viewModelScope.launch(NonCancellable) {
when {
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
room.saveDraft(UserDraft.REGULAR(action.draft))
}
it.sendMode is SendMode.REPLY -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
it.sendMode is SendMode.QUOTE -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
it.sendMode is SendMode.EDIT -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
}
}
}
private fun getDraftIfAny() {
val currentDraft = room.getDraft() ?: return
private fun loadDraftIfAny() {
val currentDraft = room.getDraft()
setState {
copy(
// Create a sendMode from a draft and retrieve the TimelineEvent
@ -517,6 +551,7 @@ class RoomDetailViewModel @AssistedInject constructor(
SendMode.EDIT(timelineEvent, currentDraft.text)
}
}
else -> null
} ?: SendMode.REGULAR("", fromSharing = false)
)
}
@ -772,11 +807,13 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun popDraft() = withState {
if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) {
// If we were sharing, we want to get back our last value from draft
getDraftIfAny()
loadDraftIfAny()
} else {
// Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.REGULAR("", false)) }
room.deleteDraft(NoOpMatrixCallback())
viewModelScope.launch {
room.deleteDraft()
}
}
}
@ -1111,15 +1148,15 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleReportContent(action: RoomDetailAction.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<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)) }
}

View file

@ -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,

View file

@ -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) }
}

View file

@ -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 {
@ -164,7 +169,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE,
EventType.STICKER -> {
EventType.STICKER -> {
val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
val html = messageContent.formattedBody
@ -172,7 +177,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
?.let { htmlCompressor.compress(it) }
?: messageContent.body
eventHtmlRenderer.get().render(html)
eventHtmlRenderer.get().render(html, pillsPostProcessor)
} else if (messageContent is MessageVerificationRequestContent) {
stringProvider.getString(R.string.verification_request)
} else {
@ -186,6 +191,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.STATE_ROOM_ALIASES,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL,
EventType.CALL_INVITE,
EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP,

View file

@ -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))
}

View file

@ -60,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.CodeVisitor
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
@ -106,15 +107,19 @@ class MessageItemFactory @Inject constructor(
private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val session: Session) {
private val pillsPostProcessor by lazy {
pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId)
}
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
event.root.eventId ?: return null
val informationData = messageInformationDataFactory.create(event, nextEvent)
if (event.root.isRedacted()) {
@ -139,16 +144,16 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
}
@ -159,7 +164,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
return when (messageContent.optionType) {
OPTION_TYPE_POLL -> {
OPTION_TYPE_POLL -> {
MessagePollItem_()
.attributes(attributes)
.callback(callback)
@ -217,13 +222,17 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
// If this request is not sent by me or sent to me, we should ignore it in timeline
val myUserId = session.myUserId
val roomId = roomSummaryHolder.roomSummary?.roomId
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
return null
}
val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName
else informationData.memberName
val otherUserName = if (informationData.sentByMe) {
session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName
} else {
informationData.memberName
}
return VerificationRequestItem_()
.attributes(
VerificationRequestItem.Attributes(
@ -362,7 +371,7 @@ class MessageItemFactory @Inject constructor(
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> {
CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
if (codeFormattedBlock == null) {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
@ -378,7 +387,7 @@ class MessageItemFactory @Inject constructor(
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
}
CodeVisitor.Kind.NONE -> {
CodeVisitor.Kind.NONE -> {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
}
}
@ -393,7 +402,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
val formattedBody = htmlRenderer.get().render(compressed)
val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor)
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
@ -528,7 +537,7 @@ class MessageItemFactory @Inject constructor(
private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
return matrixFormattedBody
?.let { htmlCompressor.compress(it) }
?.let { htmlRenderer.get().render(it) }
?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
?: body
}

View file

@ -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,

View file

@ -19,6 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.format
import im.vector.app.ActiveSessionDataSource
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.extensions.appendNl
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
@ -35,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
@ -48,9 +51,12 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import timber.log.Timber
import javax.inject.Inject
class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val sp: StringProvider) {
class NoticeEventFormatter @Inject constructor(
private val activeSessionDataSource: ActiveSessionDataSource,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val vectorPreferences: VectorPreferences,
private val sp: StringProvider
) {
private val currentUserId: String?
get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
@ -72,6 +78,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY ->
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_WIDGET,
@ -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

View file

@ -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,

View file

@ -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,34 +86,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.avatarView.visibility = View.GONE
}
if (attributes.hasEncryptionEvent) {
holder.encryptionTile.isVisible = true
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
if (attributes.isEncryptionAlgorithmSecure) {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
} else {
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
}
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
null, null, null
)
} else {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
null, null, null
)
}
} else {
holder.encryptionTile.isVisible = false
}
bindEncryptionTile(holder, data)
} else {
holder.avatarView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
@ -107,6 +96,109 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.readReceiptsView.isVisible = false
}
private fun bindEncryptionTile(holder: Holder, data: Data?) {
if (attributes.hasEncryptionEvent) {
holder.encryptionTile.isVisible = true
holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
if (attributes.isEncryptionAlgorithmSecure) {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
} else {
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
}
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
null, null, null
)
} else {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
null, null, null
)
}
} else {
holder.encryptionTile.isVisible = false
}
}
private fun bindCreationSummaryTile(holder: Holder) {
val roomSummary = attributes.roomSummary
val roomDisplayName = roomSummary?.displayName
holder.roomNameText.setTextOrHide(roomDisplayName)
val isDirect = roomSummary?.isDirect == true
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
if (isDirect) {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName ?: "")
} else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
} else {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
}
val topic = roomSummary?.topic
if (topic.isNullOrBlank()) {
// do not show hint for DMs or group DMs
if (!isDirect) {
val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
override fun onClick(widget: View) {
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
}
}))
}
} else {
holder.roomTopicText.setTextOrHide(
span {
span(holder.view.resources.getString(R.string.topic_prefix)) {
textStyle = "bold"
}
+topic.linkify(attributes.callback)
}
)
}
holder.roomTopicText.movementMethod = movementMethod
val roomItem = roomSummary?.toMatrixItem()
val shouldSetAvatar = attributes.canChangeAvatar
&& (roomSummary?.isDirect == false || (isDirect && membersCount >= 2))
&& roomItem?.avatarUrl.isNullOrBlank()
holder.roomAvatarImageView.isVisible = roomItem != null
if (roomItem != null) {
attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView)
holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view ->
if (shouldSetAvatar) {
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
} else {
// Note: this is no op if there is no avatar on the room
attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view))
}
}))
}
holder.setAvatarButton.isVisible = shouldSetAvatar
if (shouldSetAvatar) {
holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
}))
}
holder.addPeopleButton.isVisible = !isDirect
if (!isDirect) {
holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople)
}))
}
}
class Holder : BasedMergedItem.Holder(STUB_ID) {
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
@ -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
}

View file

@ -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) {
_viewEvents.post(RoomListViewEvents.Failure(failure))
val room = session.getRoom(action.roomId)
if (room != null) {
viewModelScope.launch {
try {
room.setRoomNotificationState(action.notificationState)
} catch (failure: Exception) {
_viewEvents.post(RoomListViewEvents.Failure(failure))
}
}
})
}
}
private fun handleToggleTag(action: RoomListAction.ToggleTag) {
@ -185,17 +190,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
action.tag.otherTag()
?.takeIf { room.roomSummary()?.hasTag(it).orFalse() }
?.let { tagToRemove ->
awaitCallback<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))

View file

@ -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))
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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 */

View file

@ -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)
}

View file

@ -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

View file

@ -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) {

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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) }
}

View file

@ -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()

View file

@ -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()) {
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) {
EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> {
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs)
}
else -> addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
}
}
sharedActionViewModel
.observe()

View file

@ -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
)
}
}
}
}

View file

@ -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)
}

View file

@ -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