Merge branch 'develop' into feature/bma/crop_issue

This commit is contained in:
Benoit Marty 2020-11-19 17:25:36 +01:00 committed by GitHub
commit f9e4b689b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1324 additions and 612 deletions

View File

@ -5,14 +5,21 @@ Features ✨:
- -
Improvements 🙌: Improvements 🙌:
- New room creation tile with quick action (#2346)
- Open an existing DM instead of creating a new one (#2319) - 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) - Handle events of type "m.room.server_acl" (#890)
- Move "Enable Encryption" from room setting screen to room profile screen (#2394)
Bugfix 🐛: Bugfix 🐛:
- 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 restoring draft after sharing (#2287)
- Fix issue when updating the avatar of a room (new avatar vanishing) - Fix issue when updating the avatar of a room (new avatar vanishing)
- Discard change dialog displayed by mistake when avatar has been updated - Discard change dialog displayed by mistake when avatar has been updated
- Try to fix cropped image in timeline (#2126) - Try to fix cropped image in timeline (#2126)
- Registration: annoying error message scares every new user when they add an email (#2391)
Translations 🗣: Translations 🗣:
- -

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583 distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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.pushers.Pusher
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams 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.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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.sync.SyncState 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>> { fun liveUsers(): Observable<List<User>> {
return session.getUsersLive().asObservable() return session.getUsersLive().asObservable()
} }

View File

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

View File

@ -15,11 +15,9 @@
*/ */
package org.matrix.android.sdk.api.pushrules 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.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet 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.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable
interface PushRuleService { interface PushRuleService {
/** /**
@ -29,13 +27,13 @@ interface PushRuleService {
fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet 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) fun addPushRuleListener(listener: PushRuleListener)

View File

@ -16,9 +16,6 @@
package org.matrix.android.sdk.api.raw 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 * 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 * Get a URL, either from cache or from the remote server, depending on the cache strategy
*/ */
fun getUrl(url: String, suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String
rawCacheStrategy: RawCacheStrategy,
matrixCallback: MatrixCallback<String>): Cancelable
/** /**
* Specific case for the well-known file. Cache validity is 8 hours * 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 * Clear all the cache data
*/ */
fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable suspend fun clearCache()
} }

View File

@ -16,9 +16,6 @@
package org.matrix.android.sdk.api.session.group 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. * 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. * 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. * 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. * @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 * 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 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 * Get the status of the current user's threePid
* A lookup will be performed, but also pending binding state will be restored * 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 NoIdentityServerConfigured : IdentityServiceError()
object TermsNotSignedException : IdentityServiceError() object TermsNotSignedException : IdentityServiceError()
object BulkLookupSha256NotSupported : IdentityServiceError() object BulkLookupSha256NotSupported : IdentityServiceError()
object UserConsentNotProvided : IdentityServiceError()
object BindingError : IdentityServiceError() object BindingError : IdentityServiceError()
object NoCurrentBindingError : IdentityServiceError() object NoCurrentBindingError : IdentityServiceError()
} }

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.permalinks package org.matrix.android.sdk.api.session.permalinks
import android.text.Spannable import android.text.Spannable
import org.matrix.android.sdk.api.MatrixPatterns
/** /**
* MatrixLinkify take a piece of text and turns all of the * 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: * 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 * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
*/ */
/*
// sanity checks // sanity checks
if (spannable.isEmpty()) { if (spannable.isEmpty()) {
return false return false
@ -48,14 +49,21 @@ object MatrixLinkify {
val startPos = match.range.first val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') { if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 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) val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
} }
return hasMatch 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 androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback 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.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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams 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.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 * - 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? 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,9 +16,6 @@
package org.matrix.android.sdk.api.session.room.reporting 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. * This interface defines methods to report content of an event.
*/ */
@ -28,5 +25,5 @@ interface ReportingService {
* Report content * Report content
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid * 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 package org.matrix.android.sdk.api.session.room.send
import androidx.lifecycle.LiveData 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 import org.matrix.android.sdk.api.util.Optional
interface DraftService { interface DraftService {
@ -26,12 +24,12 @@ interface DraftService {
/** /**
* Save or update a draft to the room * 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 * 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 * Return the current draft or null

View File

@ -16,9 +16,6 @@
package org.matrix.android.sdk.api.session.room.tags 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. * 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 * 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 * 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 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. * 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 beforeLimit how many events before the result are returned.
* @param afterLimit how many events after 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 includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned.
* @param callback Callback to get the search result
*/ */
fun search(searchTerm: String, suspend fun search(searchTerm: String,
roomId: String, roomId: String,
nextBatch: String?, nextBatch: String?,
orderByRecent: Boolean, orderByRecent: Boolean,
limit: Int, limit: Int,
beforeLimit: Int, beforeLimit: Int,
afterLimit: Int, afterLimit: Int,
includeProfile: Boolean, includeProfile: Boolean): SearchResult
callback: MatrixCallback<SearchResult>): Cancelable
} }

View File

@ -241,9 +241,9 @@ internal class UpdateTrustWorker(context: Context,
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds") Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
// The set of “all users” depends on the type of room: // 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 // 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 } activeMemberUserIds.filter { it != myUserId }
} else { } else {
activeMemberUserIds activeMemberUserIds

View File

@ -52,5 +52,8 @@ internal class TimeOutInterceptor @Inject constructor() : Interceptor {
const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT" const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT"
const val READ_TIMEOUT = "READ_TIMEOUT" const val READ_TIMEOUT = "READ_TIMEOUT"
const val WRITE_TIMEOUT = "WRITE_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 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.RawCacheStrategy
import org.matrix.android.sdk.api.raw.RawService 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 java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
internal class DefaultRawService @Inject constructor( internal class DefaultRawService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val getUrlTask: GetUrlTask, private val getUrlTask: GetUrlTask,
private val cleanRawCacheTask: CleanRawCacheTask private val cleanRawCacheTask: CleanRawCacheTask
) : RawService { ) : RawService {
override fun getUrl(url: String, override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String {
rawCacheStrategy: RawCacheStrategy, return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy))
matrixCallback: MatrixCallback<String>): Cancelable {
return getUrlTask
.configureWith(GetUrlTask.Params(url, rawCacheStrategy)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
} }
override fun getWellknown(userId: String, override suspend fun getWellknown(userId: String): String {
matrixCallback: MatrixCallback<String>): Cancelable {
val homeServerDomain = userId.substringAfter(":") val homeServerDomain = userId.substringAfter(":")
return getUrl( return getUrl(
"https://$homeServerDomain/.well-known/matrix/client", "https://$homeServerDomain/.well-known/matrix/client",
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false), RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
matrixCallback
) )
} }
override fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable { override suspend fun clearCache() {
return cleanRawCacheTask cleanRawCacheTask.execute(Unit)
.configureWith(Unit) {
callback = matrixCallback
}
.executeBy(taskExecutor)
} }
} }

View File

@ -16,20 +16,13 @@
package org.matrix.android.sdk.internal.session.group 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.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, internal class DefaultGroup(override val groupId: String,
private val taskExecutor: TaskExecutor,
private val getGroupDataTask: GetGroupDataTask) : Group { private val getGroupDataTask: GetGroupDataTask) : Group {
override fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable { override suspend fun fetchGroupData() {
val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId))
return getGroupDataTask.configureWith(params) { getGroupDataTask.execute(params)
this.callback = callback
}.executeBy(taskExecutor)
} }
} }

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.api.session.group.Group
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject import javax.inject.Inject
internal interface GroupFactory { internal interface GroupFactory {
@ -26,14 +25,12 @@ internal interface GroupFactory {
} }
@SessionScope @SessionScope
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask, internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) :
private val taskExecutor: TaskExecutor) :
GroupFactory { GroupFactory {
override fun create(groupId: String): Group { override fun create(groupId: String): Group {
return DefaultGroup( return DefaultGroup(
groupId = groupId, groupId = groupId,
taskExecutor = taskExecutor,
getGroupDataTask = getGroupDataTask 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 org.matrix.android.sdk.internal.util.ensureProtocol
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.extensions.orFalse
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection 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 { override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
if (!getUserConsent()) {
callback.onFailure(IdentityServiceError.UserConsentNotProvided)
return NoOpCancellable
}
if (threePids.isEmpty()) { if (threePids.isEmpty()) {
callback.onSuccess(emptyList()) callback.onSuccess(emptyList())
return NoOpCancellable return NoOpCancellable
@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor(
} }
override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable { 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()) { if (threePids.isEmpty()) {
callback.onSuccess(emptyMap()) callback.onSuccess(emptyMap())
return NoOpCancellable 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 org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration
import java.io.File import java.io.File
@Module @Module
@ -59,6 +60,7 @@ internal abstract class IdentityModule {
@SessionScope @SessionScope
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
@SessionFilesDirectory directory: File, @SessionFilesDirectory directory: File,
migration: RealmIdentityStoreMigration,
@UserMd5 userMd5: String): RealmConfiguration { @UserMd5 userMd5: String): RealmConfiguration {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(directory) .directory(directory)
@ -66,6 +68,8 @@ internal abstract class IdentityModule {
.apply { .apply {
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
} }
.schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION)
.migration(migration)
.allowWritesOnUiThread(true) .allowWritesOnUiThread(true)
.modules(IdentityRealmModule()) .modules(IdentityRealmModule())
.build() .build()

View File

@ -20,5 +20,6 @@ internal data class IdentityData(
val identityServerUrl: String?, val identityServerUrl: String?,
val token: String?, val token: String?,
val hashLookupPepper: 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 setToken(token: String?)
fun setUserConsent(consent: Boolean)
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
/** /**

View File

@ -23,7 +23,8 @@ internal open class IdentityDataEntity(
var identityServerUrl: String? = null, var identityServerUrl: String? = null,
var token: String? = null, var token: String? = null,
var hashLookupPepper: String? = null, var hashLookupPepper: String? = null,
var hashLookupAlgorithm: RealmList<String> = RealmList() var hashLookupAlgorithm: RealmList<String> = RealmList(),
var userConsent: Boolean = false
) : RealmObject() { ) : RealmObject() {
companion object 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, internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm,
pepper: String, pepper: String,
algorithms: List<String>) { algorithms: List<String>) {

View File

@ -26,7 +26,8 @@ internal object IdentityMapper {
identityServerUrl = entity.identityServerUrl, identityServerUrl = entity.identityServerUrl,
token = entity.token, token = entity.token,
hashLookupPepper = entity.hashLookupPepper, 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) { override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) {
Realm.getInstance(realmConfiguration).use { Realm.getInstance(realmConfiguration).use {
it.executeTransaction { realm -> 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 package org.matrix.android.sdk.internal.session.notification
import com.zhuinden.monarchy.Monarchy 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.PushRuleService
import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleSetKey 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.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet 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.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.mapper.PushRulesMapper
import org.matrix.android.sdk.internal.database.model.PushRulesEntity import org.matrix.android.sdk.internal.database.model.PushRulesEntity
import org.matrix.android.sdk.internal.database.query.where 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 // The rules will be updated, and will come back from the next sync response
return updatePushRuleEnableStatusTask updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled))
.configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { override suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) {
return addPushRuleTask addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule))
.configureWith(AddPushRuleTask.Params(kind, pushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) {
return updatePushRuleActionsTask updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule))
.configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
return removePushRuleTask removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule))
.configureWith(RemovePushRuleTask.Params(kind, pushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {

View File

@ -17,27 +17,37 @@
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import androidx.lifecycle.LiveData 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.MatrixCallback
import org.matrix.android.sdk.api.session.room.Room 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.RoomService
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams 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.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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams 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.Cancelable
import org.matrix.android.sdk.api.util.Optional 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.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask 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.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.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask 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.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask 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.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.fetchCopied
import javax.inject.Inject import javax.inject.Inject
internal class DefaultRoomService @Inject constructor( internal class DefaultRoomService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask,
@ -118,4 +128,24 @@ internal class DefaultRoomService @Inject constructor(
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> { override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
return roomChangeMembershipStateDataSource.getLiveStates() 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.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import io.realm.Realm 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 import javax.inject.Inject
internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { 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 roomMembers = RoomMemberHelper(realm, roomId)
val members = roomMembers.queryActiveRoomMembersEvent().findAll() 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) // 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) { val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false
res = members.firstOrNull()?.avatarUrl if (isDirectRoom) {
} else if (members.size == 2) { if (members.size == 1) {
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() res = members.firstOrNull()?.avatarUrl
res = firstOtherMember?.avatarUrl } else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.avatarUrl
}
} }
return res return res
} }

View File

@ -17,6 +17,9 @@
package org.matrix.android.sdk.internal.session.room.alias package org.matrix.android.sdk.internal.session.room.alias
import com.zhuinden.monarchy.Monarchy 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.api.util.Optional
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.findByAlias 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.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> { internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
@ -50,9 +51,11 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
} else if (!params.searchOnServer) { } else if (!params.searchOnServer) {
Optional.from<String>(null) Optional.from<String>(null)
} else { } else {
roomId = executeRequest<RoomAliasDescription>(eventBus) { roomId = tryOrNull("## Failed to get roomId from alias") {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) executeRequest<RoomAliasDescription>(eventBus) {
}.roomId apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}
}?.roomId
Optional.from(roomId) Optional.from(roomId)
} }
} }

View File

@ -19,18 +19,14 @@ package org.matrix.android.sdk.internal.session.room.draft
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject 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.DraftService
import org.matrix.android.sdk.api.session.room.send.UserDraft 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.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 import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
private val draftRepository: DraftRepository, private val draftRepository: DraftRepository,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers private val coroutineDispatchers: MatrixCoroutineDispatchers
) : DraftService { ) : 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, * 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 * or even move an existing draft to the top of the list
*/ */
override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable { override suspend fun saveDraft(draft: UserDraft) {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { withContext(coroutineDispatchers.main) {
draftRepository.saveDraft(roomId, draft) draftRepository.saveDraft(roomId, draft)
} }
} }
override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable { override suspend fun deleteDraft() {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { withContext(coroutineDispatchers.main) {
draftRepository.deleteDraft(roomId) draftRepository.deleteDraft(roomId)
} }
} }

View File

@ -93,6 +93,8 @@ internal class RoomDisplayNameResolver @Inject constructor(
} }
} else if (roomEntity?.membership == Membership.JOIN) { } else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() 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) { val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { userId -> roomSummary.heroes.mapNotNull { userId ->
roomMembers.getLastRoomMember(userId)?.takeIf { roomMembers.getLastRoomMember(userId)?.takeIf {
@ -102,22 +104,49 @@ internal class RoomDisplayNameResolver @Inject constructor(
} else { } else {
activeMembers.where() activeMembers.where()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.limit(3) .limit(5)
.findAll() .findAll()
.createSnapshot() .createSnapshot()
} }
val otherMembersCount = otherMembersSubset.count() val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) { 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) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> stringProvider.getString(R.string.room_displayname_two_members, 2 -> {
resolveRoomMemberName(otherMembersSubset[0], roomMembers), stringProvider.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[1], roomMembers) 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), 3 -> {
roomMembers.getNumberOfJoinedMembers() - 1) 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 return name ?: roomId

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.session.room.membership 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.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity 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.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where 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. * This class is an helper around STATE_ROOM_MEMBER events.

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.Assisted
import com.squareup.inject.assisted.AssistedInject 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.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, internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
private val taskExecutor: TaskExecutor,
private val reportContentTask: ReportContentTask private val reportContentTask: ReportContentTask
) : ReportingService { ) : ReportingService {
@ -34,13 +29,8 @@ internal class DefaultReportingService @AssistedInject constructor(@Assisted pri
fun create(roomId: String): ReportingService 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) val params = ReportContentTask.Params(roomId, eventId, score, reason)
reportContentTask.execute(params)
return reportContentTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }

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.Assisted
import com.squareup.inject.assisted.AssistedInject 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.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( internal class DefaultTagsService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
private val taskExecutor: TaskExecutor,
private val addTagToRoomTask: AddTagToRoomTask, private val addTagToRoomTask: AddTagToRoomTask,
private val deleteTagFromRoomTask: DeleteTagFromRoomTask private val deleteTagFromRoomTask: DeleteTagFromRoomTask
) : TagsService { ) : TagsService {
@ -36,21 +31,13 @@ internal class DefaultTagsService @AssistedInject constructor(
fun create(roomId: String): TagsService 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) val params = AddTagToRoomTask.Params(roomId, tag, order)
return addTagToRoomTask addTagToRoomTask.execute(params)
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun deleteTag(tag: String, callback: MatrixCallback<Unit>): Cancelable { override suspend fun deleteTag(tag: String) {
val params = DeleteTagFromRoomTask.Params(roomId, tag) val params = DeleteTagFromRoomTask.Params(roomId, tag)
return deleteTagFromRoomTask deleteTagFromRoomTask.execute(params)
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }

View File

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

View File

@ -17,18 +17,21 @@
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync
import org.matrix.android.sdk.internal.network.NetworkConstants 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 org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers import retrofit2.http.Header
import retrofit2.http.QueryMap import retrofit2.http.QueryMap
internal interface SyncAPI { 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") @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.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.R import org.matrix.android.sdk.R
import org.matrix.android.sdk.internal.di.UserId 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.network.executeRequest
import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService
import org.matrix.android.sdk.internal.session.filter.FilterRepository 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 // Maybe refresh the home server capabilities data we know
getHomeServerCapabilitiesTask.execute(Unit) getHomeServerCapabilitiesTask.execute(Unit)
val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
val syncResponse = executeRequest<SyncResponse>(eventBus) { val syncResponse = executeRequest<SyncResponse>(eventBus) {
apiCall = syncAPI.sync(requestParams) apiCall = syncAPI.sync(
params = requestParams,
readTimeOut = readTimeOut
)
} }
syncResponseHandler.handleResponse(syncResponse, token) syncResponseHandler.handleResponse(syncResponse, token)
if (isInitialSync) { if (isInitialSync) {
@ -87,4 +93,8 @@ internal class DefaultSyncTask @Inject constructor(
} }
Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}")
} }
companion object {
private const val TIMEOUT_MARGIN: Long = 10_000
}
} }

View File

@ -175,13 +175,22 @@
<!-- The 2 parameters will be members' name --> <!-- The 2 parameters will be members' name -->
<string name="room_displayname_two_members">%1$s and %2$s</string> <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"> <plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s and 1 other</item> <item quantity="one">%1$s and 1 other</item>
<item quantity="other">%1$s and %2$d others</item> <item quantity="other">%1$s and %2$d others</item>
</plurals> </plurals>
<string name="room_displayname_empty_room">Empty room</string> <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">Initial Sync:\nImporting account…</string>
<string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string> <string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string>

View File

@ -19,6 +19,7 @@
echo "Configure Element Template..." echo "Configure Element Template..."
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi 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" ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
} && { } && {
echo "Please restart Android Studio." 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.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported
IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error
IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_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

@ -137,7 +137,7 @@ class VectorCallViewModel @AssistedInject constructor(
session.callSignalingService().getCallWithId(it)?.let { mxCall -> session.callSignalingService().getCallWithId(it)?.let { mxCall ->
this.call = mxCall this.call = mxCall
mxCall.otherUserId mxCall.otherUserId
val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem() val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()
mxCall.addListener(callStateListener) 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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent 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.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.util.toMatrixItem
import org.webrtc.AudioSource import org.webrtc.AudioSource
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator import org.webrtc.Camera1Enumerator
@ -330,8 +331,8 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCall?.mxCall currentCall?.mxCall
?.takeIf { it.state is CallState.Connected } ?.takeIf { it.state is CallState.Connected }
?.let { mxCall -> ?.let { mxCall ->
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
?: mxCall.roomId ?: mxCall.otherUserId
// Start background service with notification // Start background service with notification
CallService.onPendingCall( CallService.onPendingCall(
context = context, context = context,
@ -388,7 +389,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
val mxCall = callContext.mxCall val mxCall = callContext.mxCall
// Update service state // Update service state
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
?: mxCall.roomId ?: mxCall.roomId
CallService.onPendingCall( CallService.onPendingCall(
context = context, context = context,
@ -576,7 +577,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
?.let { mxCall -> ?.let { mxCall ->
// Start background service with notification // Start background service with notification
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
?: mxCall.otherUserId ?: mxCall.otherUserId
CallService.onOnGoingCallBackground( CallService.onOnGoingCallBackground(
context = context, context = context,
@ -650,7 +651,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
callAudioManager.startForCall(createdCall) callAudioManager.startForCall(createdCall)
currentCall = callContext currentCall = callContext
val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName() val name = currentSession?.getRoomMember(createdCall.otherUserId, createdCall.roomId)?.toMatrixItem()?.getBestName()
?: createdCall.otherUserId ?: createdCall.otherUserId
CallService.onOutgoingCallRinging( CallService.onOutgoingCallRinging(
context = context.applicationContext, context = context.applicationContext,
@ -706,7 +707,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
// Start background service with notification // Start background service with notification
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
?: mxCall.otherUserId ?: mxCall.otherUserId
CallService.onIncomingCallRinging( CallService.onIncomingCallRinging(
context = context, context = context,
@ -845,7 +846,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
val mxCall = call.mxCall val mxCall = call.mxCall
// Update service state // Update service state
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
?: mxCall.otherUserId ?: mxCall.otherUserId
CallService.onPendingCall( CallService.onPendingCall(
context = context, context = context,

View File

@ -46,7 +46,7 @@ class JitsiCallViewModel @AssistedInject constructor(
} }
init { init {
val me = session.getUser(session.myUserId)?.toMatrixItem() val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem()
val userInfo = JitsiMeetUserInfo().apply { val userInfo = JitsiMeetUserInfo().apply {
displayName = me?.getBestName() displayName = me?.getBestName()
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } 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 { sealed class ContactsBookAction : VectorViewModelAction {
data class FilterWith(val filter: String) : ContactsBookAction() data class FilterWith(val filter: String) : ContactsBookAction()
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction() data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
object UserConsentGranted : ContactsBookAction()
} }

View File

@ -52,11 +52,10 @@ class ContactsBookController @Inject constructor(
override fun buildModels() { override fun buildModels() {
val currentState = state ?: return val currentState = state ?: return
val hasSearch = currentState.searchTerm.isNotEmpty()
when (val asyncMappedContacts = currentState.mappedContacts) { when (val asyncMappedContacts = currentState.mappedContacts) {
is Uninitialized -> renderEmptyState(false) is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading() is Loading -> renderLoading()
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts) is Success -> renderSuccess(currentState)
is Fail -> renderFailure(asyncMappedContacts.error) is Fail -> renderFailure(asyncMappedContacts.error)
} }
} }
@ -75,13 +74,13 @@ class ContactsBookController @Inject constructor(
} }
} }
private fun renderSuccess(mappedContacts: List<MappedContact>, private fun renderSuccess(state: ContactsBookViewState) {
hasSearch: Boolean, val mappedContacts = state.filteredMappedContacts
onlyBoundContacts: Boolean) {
if (mappedContacts.isEmpty()) { if (mappedContacts.isEmpty()) {
renderEmptyState(hasSearch) renderEmptyState(state.searchTerm.isNotEmpty())
} else { } 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.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView() setupRecyclerView()
setupFilterView() setupFilterView()
setupConsentView()
setupOnlyBoundContactsView() setupOnlyBoundContactsView()
setupCloseView() 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() { private fun setupOnlyBoundContactsView() {
phoneBookOnlyBoundContacts.checkedChanges() phoneBookOnlyBoundContacts.checkedChanges()
.subscribe { .subscribe {
@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor(
} }
override fun invalidate() = withState(contactsBookViewModel) { state -> override fun invalidate() = withState(contactsBookViewModel) { state ->
phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
contactsBookController.setData(state) 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.MatrixCallback
import org.matrix.android.sdk.api.session.Session 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.FoundThreePid
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import timber.log.Timber import timber.log.Timber
private typealias PhoneBookSearch = String
class ContactsBookViewModel @AssistedInject constructor(@Assisted class ContactsBookViewModel @AssistedInject constructor(@Assisted
initialState: ContactsBookViewState, initialState: ContactsBookViewState,
private val contactsDataSource: ContactsDataSource, private val contactsDataSource: ContactsDataSource,
@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
private fun loadContacts() { private fun loadContacts() {
setState { setState {
copy( 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>) { private fun performLookup(data: List<MappedContact>) {
if (!session.identityService().getUserConsent()) {
return
}
viewModelScope.launch { viewModelScope.launch {
val threePids = data.flatMap { contact -> val threePids = data.flatMap { contact ->
contact.emails.map { ThreePid.Email(it.email) } + contact.emails.map { ThreePid.Email(it.email) } +
@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
} }
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> { session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Ignore
Timber.w(failure, "Unable to perform the lookup") 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>) { override fun onSuccess(data: List<FoundThreePid>) {
@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
when (action) { when (action) {
is ContactsBookAction.FilterWith -> handleFilterWith(action) is ContactsBookAction.FilterWith -> handleFilterWith(action)
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action) is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
}.exhaustive }.exhaustive
} }
private fun handleUserConsentGranted() {
session.identityService().setUserConsent(true)
setState {
copy(userConsent = true)
}
// Perform the lookup
performLookup(allContacts)
}
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) { private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
setState { setState {
copy( copy(

View File

@ -26,10 +26,14 @@ data class ContactsBookViewState(
val mappedContacts: Async<List<MappedContact>> = Loading(), val mappedContacts: Async<List<MappedContact>> = Loading(),
// Use to filter contacts by display name // Use to filter contacts by display name
val searchTerm: String = "", 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, val onlyBoundContacts: Boolean = false,
// All contacts, filtered by searchTerm and onlyBoundContacts // All contacts, filtered by searchTerm and onlyBoundContacts
val filteredMappedContacts: List<MappedContact> = emptyList(), val filteredMappedContacts: List<MappedContact> = emptyList(),
// True when the identity service has return some data // 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 ) : MvRxState

View File

@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
object DisconnectIdentityServer : DiscoverySettingsAction() object DisconnectIdentityServer : DiscoverySettingsAction()
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction()
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()

View File

@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor(
buildIdentityServerSection(data) buildIdentityServerSection(data)
val hasIdentityServer = data.identityServer().isNullOrBlank().not() val hasIdentityServer = data.identityServer().isNullOrBlank().not()
if (hasIdentityServer && !data.termsNotSigned) { if (hasIdentityServer && !data.termsNotSigned) {
buildConsentSection(data)
buildEmailsSection(data.emailList) buildEmailsSection(data.emailList)
buildMsisdnSection(data.phoneNumbersList) 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) { private fun buildIdentityServerSection(data: DiscoverySettingsState) {
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) 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 sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
fun onTapChangeIdentityServer() fun onTapChangeIdentityServer()
fun onTapDisconnectIdentityServer() fun onTapDisconnectIdentityServer()
fun onTapUpdateUserConsent(newValue: Boolean)
fun onTapRetryToRetrieveBindings() 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() { override fun onTapRetryToRetrieveBindings() {
viewModel.handle(DiscoverySettingsAction.RetrieveBinding) viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
} }

View File

@ -25,5 +25,6 @@ data class DiscoverySettingsState(
val emailList: Async<List<PidInfo>> = Uninitialized, val emailList: Async<List<PidInfo>> = Uninitialized,
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized, val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
// Can be true if terms are updated // Can be true if terms are updated
val termsNotSigned: Boolean = false val termsNotSigned: Boolean = false,
val userConsent: Boolean = false
) : MvRxState ) : MvRxState

View File

@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
val identityServerUrl = identityService.getCurrentIdentityServerUrl() val identityServerUrl = identityService.getCurrentIdentityServerUrl()
val currentIS = state.identityServer() val currentIS = state.identityServer()
setState { setState {
copy(identityServer = Success(identityServerUrl)) copy(
identityServer = Success(identityServerUrl),
userConsent = false
)
} }
if (currentIS != identityServerUrl) retrieveBinding() if (currentIS != identityServerUrl) retrieveBinding()
} }
@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
init { init {
setState { setState {
copy(identityServer = Success(identityService.getCurrentIdentityServerUrl())) copy(
identityServer = Success(identityService.getCurrentIdentityServerUrl()),
userConsent = identityService.getUserConsent()
)
} }
startListenToIdentityManager() startListenToIdentityManager()
observeThreePids() observeThreePids()
@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer()
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action)
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true)
@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
}.exhaustive }.exhaustive
} }
private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) {
identityService.setUserConsent(action.newConsent)
setState { copy(userConsent = action.newConsent) }
}
private fun disconnectIdentityServer() { private fun disconnectIdentityServer() {
setState { copy(identityServer = Loading()) } setState { copy(identityServer = Loading()) }
viewModelScope.launch { viewModelScope.launch {
try { try {
awaitCallback<Unit> { session.identityService().disconnect(it) } awaitCallback<Unit> { session.identityService().disconnect(it) }
setState { copy(identityServer = Success(null)) } setState {
copy(
identityServer = Success(null),
userConsent = false
)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { copy(identityServer = Fail(failure)) } setState { copy(identityServer = Fail(failure)) }
} }
@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
val data = awaitCallback<String?> { val data = awaitCallback<String?> {
session.identityService().setNewIdentityServer(action.url, it) session.identityService().setNewIdentityServer(action.url, it)
} }
setState { copy(identityServer = Success(data)) } setState {
copy(
identityServer = Success(data),
userConsent = false
)
}
retrieveBinding() retrieveBinding()
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { copy(identityServer = Fail(failure)) } setState { copy(identityServer = Fail(failure)) }

View File

@ -17,6 +17,7 @@
package im.vector.app.features.grouplist package im.vector.app.features.grouplist
import androidx.lifecycle.viewModelScope
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
@ -28,7 +29,7 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction 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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams 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 -> private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) { 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 // 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) } setState { copy(selectedGroup = action.groupSummary) }
} }
} }

View File

@ -16,6 +16,8 @@
package im.vector.app.features.home.room.detail package im.vector.app.features.home.room.detail
import android.net.Uri
import android.view.View
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem
sealed class RoomDetailAction : VectorViewModelAction { sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(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 com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder 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.dialogs.withColoredButton
import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup 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.extensions.trackItemsVisibilityChange
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequests 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.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider 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.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillImageSpan 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.invite.VectorInviteView
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer 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.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.reactions.EmojiReactionPickerActivity 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.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData 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 timber.log.Timber
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -221,7 +226,8 @@ class RoomDetailFragment @Inject constructor(
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider, private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
@ -229,7 +235,7 @@ class RoomDetailFragment @Inject constructor(
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback, AttachmentsHelper.Callback,
// RoomWidgetsBannerView.Callback, GalleryOrCameraDialogHelper.Listener,
ActiveCallView.Callback { ActiveCallView.Callback {
companion object { companion object {
@ -250,10 +256,15 @@ class RoomDetailFragment @Inject constructor(
private const val ircPattern = " (IRC)" private const val ircPattern = " (IRC)"
} }
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
private val roomDetailArgs: RoomDetailArgs by args() private val roomDetailArgs: RoomDetailArgs by args()
private val glideRequests by lazy { private val glideRequests by lazy {
GlideApp.with(this) GlideApp.with(this)
} }
private val pillsPostProcessor by lazy {
pillsPostProcessorFactory.create(roomDetailArgs.roomId)
}
private val autoCompleter: AutoCompleter by lazy { private val autoCompleter: AutoCompleter by lazy {
autoCompleterFactory.create(roomDetailArgs.roomId) autoCompleterFactory.create(roomDetailArgs.roomId)
@ -364,6 +375,12 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(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 }.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) { private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
navigator.openRoom(requireContext(), openRoom.roomId, null) navigator.openRoom(requireContext(), openRoom.roomId, null)
} }
@ -848,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody ?: messageContent.body) val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document) formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
} }
composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)

View File

@ -17,10 +17,12 @@
package im.vector.app.features.home.room.detail package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.command.Command import im.vector.app.features.command.Command
import org.matrix.android.sdk.api.session.widgets.model.Widget 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 org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import java.io.File import java.io.File
@ -43,6 +45,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : 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 ShowWaitingView : RoomDetailViewEvents()
object HideWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents()

View File

@ -50,6 +50,7 @@ import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
@ -99,6 +100,7 @@ import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.lang.Exception
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -275,9 +277,39 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(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 }.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) { private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId) val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId)
if (existingDmRoomId == null) { if (existingDmRoomId == null) {
@ -475,22 +507,24 @@ class RoomDetailViewModel @AssistedInject constructor(
* Convert a send mode to a draft and save the draft * Convert a send mode to a draft and save the draft
*/ */
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState {
when { viewModelScope.launch(NonCancellable) {
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { when {
setState { copy(sendMode = it.sendMode.copy(action.draft)) } it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) 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)) } it.sendMode is SendMode.REPLY -> {
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) 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)) } it.sendMode is SendMode.QUOTE -> {
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) 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)) } it.sendMode is SendMode.EDIT -> {
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
} }
} }
} }
@ -777,7 +811,9 @@ class RoomDetailViewModel @AssistedInject constructor(
} else { } else {
// Otherwise we clear the composer and remove the draft from db // Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.REGULAR("", false)) } setState { copy(sendMode = SendMode.REGULAR("", false)) }
room.deleteDraft(NoOpMatrixCallback()) viewModelScope.launch {
room.deleteDraft()
}
} }
} }
@ -1112,15 +1148,15 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleReportContent(action: RoomDetailAction.ReportContent) { private fun handleReportContent(action: RoomDetailAction.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> { viewModelScope.launch {
override fun onSuccess(data: Unit) { val event = try {
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) room.reportContent(action.eventId, -100, action.reason)
RoomDetailViewEvents.ActionSuccess(action)
} catch (failure: Exception) {
RoomDetailViewEvents.ActionFailure(action, failure)
} }
_viewEvents.post(event)
override fun onFailure(failure: Throwable) { }
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
})
} }
private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) {
@ -1301,7 +1337,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
if (summary.membership == Membership.INVITE) { if (summary.membership == Membership.INVITE) {
summary.inviterId?.let { inviterId -> summary.inviterId?.let { inviterId ->
session.getUser(inviterId) session.getRoomMember(inviterId, summary.roomId)
}?.also { }?.also {
setState { copy(asyncInviter = Success(it)) } 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.model.RoomSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.sync.SyncState
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
/** /**
@ -60,7 +59,7 @@ data class RoomDetailViewState(
val roomId: String, val roomId: String,
val eventId: String?, val eventId: String?,
val myRoomMember: Async<RoomMemberSummary> = Uninitialized, val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
val asyncInviter: Async<User> = Uninitialized, val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val activeRoomWidgets: Async<List<Widget>> = Uninitialized, val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
val typingMessage: String? = null, val typingMessage: String? = null,

View File

@ -123,7 +123,7 @@ class SearchResultController @Inject constructor(
.formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)) .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
.spannable(spannable) .spannable(spannable)
.sender(eventAndSender.sender .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) } .listener { listener?.onItemClicked(eventAndSender.event) }
.let { result.add(it) } .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.core.resources.StringProvider
import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.app.features.html.EventHtmlRenderer 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.html.VectorHtmlCompressor
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.reactions.data.EmojiDataSource 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. * Information related to an event and used to display preview in contextual bottom sheet.
*/ */
class MessageActionsViewModel @AssistedInject constructor(@Assisted class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState, private val initialState: MessageActionState,
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>, private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor, private val htmlCompressor: VectorHtmlCompressor,
private val session: Session, private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter, private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val informationData = initialState.informationData private val informationData = initialState.informationData
private val room = session.getRoom(initialState.roomId) private val room = session.getRoom(initialState.roomId)
private val pillsPostProcessor by lazy {
pillsPostProcessorFactory.create(initialState.roomId)
}
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -164,7 +169,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
return when (timelineEvent.root.getClearType()) { return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE, EventType.MESSAGE,
EventType.STICKER -> { EventType.STICKER -> {
val messageContent: MessageContent? = timelineEvent.getLastMessageContent() val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
val html = messageContent.formattedBody val html = messageContent.formattedBody
@ -172,7 +177,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
?.let { htmlCompressor.compress(it) } ?.let { htmlCompressor.compress(it) }
?: messageContent.body ?: messageContent.body
eventHtmlRenderer.get().render(html) eventHtmlRenderer.get().render(html, pillsPostProcessor)
} else if (messageContent is MessageVerificationRequestContent) { } else if (messageContent is MessageVerificationRequestContent) {
stringProvider.getString(R.string.verification_request) stringProvider.getString(R.string.verification_request)
} else { } else {

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.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.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.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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
collapsedEventIds.removeAll(mergedEventIds) collapsedEventIds.removeAll(mergedEventIds)
} }
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } 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( val attributes = MergedRoomCreationItem.Attributes(
isCollapsed = isCollapsed, isCollapsed = isCollapsed,
mergeData = mergedData, mergeData = mergedData,
@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
hasEncryptionEvent = hasEncryption, hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback, 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_() MergedRoomCreationItem_()
.id(mergeId) .id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted) .highlighted(isCollapsed && highlighted)
.attributes(attributes) .attributes(attributes)
.movementMethod(createLinkMovementMethod(callback))
.also { .also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) 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.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.CodeVisitor
import im.vector.app.features.html.EventHtmlRenderer 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.html.VectorHtmlCompressor
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
@ -106,15 +107,19 @@ class MessageItemFactory @Inject constructor(
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val session: Session) { private val session: Session) {
private val pillsPostProcessor by lazy {
pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId)
}
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, nextEvent)
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
@ -139,16 +144,16 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
} }
@ -159,7 +164,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
return when (messageContent.optionType) { return when (messageContent.optionType) {
OPTION_TYPE_POLL -> { OPTION_TYPE_POLL -> {
MessagePollItem_() MessagePollItem_()
.attributes(attributes) .attributes(attributes)
.callback(callback) .callback(callback)
@ -217,13 +222,17 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): VerificationRequestItem? { attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
// If this request is not sent by me or sent to me, we should ignore it in timeline // If this request is not sent by me or sent to me, we should ignore it in timeline
val myUserId = session.myUserId val myUserId = session.myUserId
val roomId = roomSummaryHolder.roomSummary?.roomId
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
return null return null
} }
val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName val otherUserName = if (informationData.sentByMe) {
else informationData.memberName session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName
} else {
informationData.memberName
}
return VerificationRequestItem_() return VerificationRequestItem_()
.attributes( .attributes(
VerificationRequestItem.Attributes( VerificationRequestItem.Attributes(
@ -362,7 +371,7 @@ class MessageItemFactory @Inject constructor(
val codeVisitor = CodeVisitor() val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody) codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) { when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> { CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
if (codeFormattedBlock == null) { if (codeFormattedBlock == null) {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
@ -378,7 +387,7 @@ class MessageItemFactory @Inject constructor(
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
} }
} }
CodeVisitor.Kind.NONE -> { CodeVisitor.Kind.NONE -> {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
} }
} }
@ -393,7 +402,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes): MessageTextItem? {
val compressed = htmlCompressor.compress(messageContent.formattedBody!!) 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) return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
} }
@ -528,7 +537,7 @@ class MessageItemFactory @Inject constructor(
private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence { private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
return matrixFormattedBody return matrixFormattedBody
?.let { htmlCompressor.compress(it) } ?.let { htmlCompressor.compress(it) }
?.let { htmlRenderer.get().render(it) } ?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
?: body ?: body
} }

View File

@ -16,11 +16,14 @@
package im.vector.app.features.home.room.detail.timeline.item 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -28,8 +31,16 @@ import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R 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.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.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) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() { abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
@ -37,11 +48,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
@EpoxyAttribute @EpoxyAttribute
override lateinit var attributes: Attributes override lateinit var attributes: Attributes
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var movementMethod: MovementMethod? = null
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
bindCreationSummaryTile(holder)
if (attributes.isCollapsed) { if (attributes.isCollapsed) {
// Take the oldest data // Take the oldest data
val data = distinctMergeData.lastOrNull() val data = distinctMergeData.lastOrNull()
@ -70,34 +86,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.avatarView.visibility = View.GONE holder.avatarView.visibility = View.GONE
} }
if (attributes.hasEncryptionEvent) { bindEncryptionTile(holder, data)
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
}
} else { } else {
holder.avatarView.visibility = View.INVISIBLE holder.avatarView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE holder.summaryView.visibility = View.GONE
@ -107,6 +96,109 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.readReceiptsView.isVisible = false 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) { class Holder : BasedMergedItem.Holder(STUB_ID) {
val summaryView by bind<TextView>(R.id.itemNoticeTextView) val summaryView by bind<TextView>(R.id.itemNoticeTextView)
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView) 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 e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView) 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 { companion object {
@ -126,8 +225,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit, override val onCollapsedStateChanged: (Boolean) -> Unit,
val callback: TimelineEventController.Callback? = null,
val currentUserId: String, val currentUserId: String,
val hasEncryptionEvent: Boolean, 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 ) : BasedMergedItem.Attributes
} }

View File

@ -33,7 +33,6 @@ 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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
import java.lang.Exception import java.lang.Exception
@ -191,17 +190,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
action.tag.otherTag() action.tag.otherTag()
?.takeIf { room.roomSummary()?.hasTag(it).orFalse() } ?.takeIf { room.roomSummary()?.hasTag(it).orFalse() }
?.let { tagToRemove -> ?.let { tagToRemove ->
awaitCallback<Unit> { room.deleteTag(tagToRemove, it) } room.deleteTag(tagToRemove)
} }
// Set the tag. We do not handle the order for the moment // Set the tag. We do not handle the order for the moment
awaitCallback<Unit> { room.addTag(action.tag, 0.5)
room.addTag(action.tag, 0.5, it)
}
} else { } else {
awaitCallback<Unit> { room.deleteTag(action.tag)
room.deleteTag(action.tag, it)
}
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(RoomListViewEvents.Failure(failure)) _viewEvents.post(RoomListViewEvents.Failure(failure))

View File

@ -17,21 +17,23 @@
package im.vector.app.features.html package im.vector.app.features.html
import android.content.Context import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder import android.text.Spannable
import im.vector.app.core.glide.GlideApp import androidx.core.text.toSpannable
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import org.commonmark.node.Node import org.commonmark.node.Node
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class EventHtmlRenderer @Inject constructor(context: Context, class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfigure,
htmlConfigure: MatrixHtmlPluginConfigure) { context: Context) {
interface PostProcessor {
fun afterRender(renderedText: Spannable)
}
private val markwon = Markwon.builder(context) private val markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create(htmlConfigure)) .usePlugin(HtmlPlugin.create(htmlConfigure))
@ -41,35 +43,47 @@ class EventHtmlRenderer @Inject constructor(context: Context,
return markwon.parse(text) 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 { return try {
markwon.toMarkdown(text) val parsed = markwon.parse(text)
renderAndProcess(parsed, postProcessors)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.v("Fail to render $text to html") Timber.v("Fail to render $text to html")
text 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 { return try {
markwon.render(node) renderAndProcess(node, postProcessors)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.v("Fail to render $node to html") Timber.v("Fail to render $node to html")
return null 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, class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure {
private val colorProvider: ColorProvider,
private val avatarRenderer: AvatarRenderer,
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
override fun configureHtml(plugin: HtmlPlugin) { override fun configureHtml(plugin: HtmlPlugin) {
plugin plugin
.addHandler(TagHandlerNoOp.create("a"))
.addHandler(FontTagHandler()) .addHandler(FontTagHandler())
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
.addHandler(MxReplyTagHandler()) .addHandler(MxReplyTagHandler())
.addHandler(SpanHandler(colorProvider)) .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 im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.vector_invite_view.view.* 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.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 org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject 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) { if (mode == Mode.LARGE) {
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT } updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView) avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)

View File

@ -69,6 +69,11 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
} }
override fun showFailure(throwable: Throwable) { override fun showFailure(throwable: Throwable) {
// Only the resumed Fragment can eventually show the error, to avoid multiple dialog display
if (!isResumed) {
return
}
when (throwable) { when (throwable) {
is Failure.Cancelled -> is Failure.Cancelled ->
/* Ignore this error, user has cancelled the action */ /* Ignore this error, user has cancelled the action */

View File

@ -207,7 +207,6 @@ class LoginViewModel @AssistedInject constructor(
private fun handleCheckIfEmailHasBeenValidated(action: LoginAction.CheckIfEmailHasBeenValidated) { 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 // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
currentTask?.cancel() currentTask?.cancel()
currentTask = null
currentTask = registrationWizard?.checkIfEmailHasBeenValidated(action.delayMillis, registrationCallback) currentTask = registrationWizard?.checkIfEmailHasBeenValidated(action.delayMillis, registrationCallback)
} }

View File

@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor(
context.startActivity(KeysBackupManageActivity.intent(context)) context.startActivity(KeysBackupManageActivity.intent(context))
} }
override fun openRoomProfile(context: Context, roomId: String) { override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) {
context.startActivity(RoomProfileActivity.newIntent(context, roomId)) context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
} }
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) { 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 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) 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? { private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
val content = event.content?.toModel<RoomMemberContent>() ?: return null val content = event.content?.toModel<RoomMemberContent>() ?: return null
val roomId = event.roomId ?: 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) { if (Membership.INVITE == content.membership) {
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId)) val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
?: stringProvider.getString(R.string.notification_new_invitation) ?: stringProvider.getString(R.string.notification_new_invitation)

View File

@ -120,7 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
null, null,
false, false,
System.currentTimeMillis(), System.currentTimeMillis(),
session.getUser(session.myUserId)?.displayName session.getRoomMember(session.myUserId, room.roomId)?.displayName
?: context?.getString(R.string.notification_sender_me), ?: context?.getString(R.string.notification_sender_me),
session.myUserId, session.myUserId,
message, 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.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.util.awaitCallback
suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? { suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? {
return tryOrNull { awaitCallback<String> { getWellknown(userId, it) } } return tryOrNull { getWellknown(userId) }
?.let { ElementWellKnownMapper.from(it) } ?.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 import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
sealed class RoomProfileAction : VectorViewModelAction { sealed class RoomProfileAction : VectorViewModelAction {
object EnableEncryption : RoomProfileAction()
object LeaveRoom : RoomProfileAction() object LeaveRoom : RoomProfileAction()
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
object ShareRoomProfile : RoomProfileAction() object ShareRoomProfile : RoomProfileAction()

View File

@ -46,10 +46,16 @@ class RoomProfileActivity :
companion object { 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) val roomProfileArgs = RoomProfileArgs(roomId)
return Intent(context, RoomProfileActivity::class.java).apply { return Intent(context, RoomProfileActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, roomProfileArgs) putExtra(MvRx.KEY_ARG, roomProfileArgs)
putExtra(EXTRA_DIRECT_ACCESS, directAccess)
} }
} }
} }
@ -80,7 +86,13 @@ class RoomProfileActivity :
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
if (isFirstCreation()) { 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 sharedActionViewModel
.observe() .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.home.ShortcutCreator
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject import javax.inject.Inject
class RoomProfileController @Inject constructor( class RoomProfileController @Inject constructor(
@ -43,6 +44,7 @@ class RoomProfileController @Inject constructor(
interface Callback { interface Callback {
fun onLearnMoreClicked() fun onLearnMoreClicked()
fun onEnableEncryptionClicked()
fun onMemberListClicked() fun onMemberListClicked()
fun onBannedMemberListClicked() fun onBannedMemberListClicked()
fun onNotificationsClicked() fun onNotificationsClicked()
@ -84,6 +86,7 @@ class RoomProfileController @Inject constructor(
centered(false) centered(false)
text(stringProvider.getString(learnMoreSubtitle)) text(stringProvider.getString(learnMoreSubtitle))
} }
buildEncryptionAction(data.actionPermissions, roomSummary)
// More // More
buildProfileSection(stringProvider.getString(R.string.room_profile_section_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 im.vector.app.features.media.BigImageViewerActivity
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.* 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 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.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.util.MatrixItem 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.layoutResource = R.layout.view_stub_room_profile_header
it.inflate() it.inflate()
} }
setupWaitingView()
setupToolbar(matrixProfileToolbar) setupToolbar(matrixProfileToolbar)
setupRecyclerView() setupRecyclerView()
appBarStateChangeListener = MatrixItemAppBarStateChangeListener( appBarStateChangeListener = MatrixItemAppBarStateChangeListener(
@ -111,6 +113,11 @@ class RoomProfileFragment @Inject constructor(
setupLongClicks() setupLongClicks()
} }
private fun setupWaitingView() {
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
}
private fun setupLongClicks() { private fun setupLongClicks() {
roomProfileNameView.copyOnLongClick() roomProfileNameView.copyOnLongClick()
roomProfileAliasView.copyOnLongClick() roomProfileAliasView.copyOnLongClick()
@ -155,6 +162,8 @@ class RoomProfileFragment @Inject constructor(
} }
override fun invalidate() = withState(roomProfileViewModel) { state -> override fun invalidate() = withState(roomProfileViewModel) { state ->
waiting_view.isVisible = state.isLoading
state.roomSummary()?.also { state.roomSummary()?.also {
if (it.membership.isLeft()) { if (it.membership.isLeft()) {
Timber.w("The room has been left") Timber.w("The room has been left")
@ -187,6 +196,17 @@ class RoomProfileFragment @Inject constructor(
vectorBaseActivity.notImplemented() 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() { override fun onMemberListClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers) 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.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session 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.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership 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.RxRoom
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
@ -65,6 +68,7 @@ class RoomProfileViewModel @AssistedInject constructor(
val rxRoom = room.rx() val rxRoom = room.rx()
observeRoomSummary(rxRoom) observeRoomSummary(rxRoom)
observeBannedRoomMembers(rxRoom) observeBannedRoomMembers(rxRoom)
observePermissions()
} }
private fun observeRoomSummary(rxRoom: RxRoom) { 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) { override fun handle(action: RoomProfileAction) {
when (action) { when (action) {
is RoomProfileAction.EnableEncryption -> handleEnableEncryption()
RoomProfileAction.LeaveRoom -> handleLeaveRoom() RoomProfileAction.LeaveRoom -> handleLeaveRoom()
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
@ -91,6 +109,24 @@ class RoomProfileViewModel @AssistedInject constructor(
}.exhaustive }.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() { private fun handleCreateShortcut() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
withState { state -> withState { state ->

View File

@ -26,8 +26,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomProfileViewState( data class RoomProfileViewState(
val roomId: String, val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized,
val actionPermissions: ActionPermissions = ActionPermissions(),
val isLoading: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
data class ActionPermissions(
val canEnableEncryption: Boolean = false
)
} }

View File

@ -25,7 +25,6 @@ sealed class RoomSettingsAction : VectorViewModelAction {
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()
data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction() data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction()
object EnableEncryption : RoomSettingsAction()
object Save : RoomSettingsAction() object Save : RoomSettingsAction()
object Cancel : RoomSettingsAction() object Cancel : RoomSettingsAction()
} }

View File

@ -29,7 +29,6 @@ import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibi
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -44,7 +43,6 @@ class RoomSettingsController @Inject constructor(
// Delete the avatar, or cancel an avatar change // Delete the avatar, or cancel an avatar change
fun onAvatarDelete() fun onAvatarDelete()
fun onAvatarChange() fun onAvatarChange()
fun onEnableEncryptionClicked()
fun onNameChanged(name: String) fun onNameChanged(name: String)
fun onTopicChanged(topic: String) fun onTopicChanged(topic: String)
fun onHistoryVisibilityClicked() fun onHistoryVisibilityClicked()
@ -130,33 +128,6 @@ class RoomSettingsController @Inject constructor(
editable = data.actionPermissions.canChangeHistoryReadability, editable = data.actionPermissions.canChangeHistoryReadability,
action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() } action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() }
) )
buildEncryptionAction(data.actionPermissions, roomSummary)
}
private fun buildEncryptionAction(actionPermissions: RoomSettingsViewState.ActionPermissions, roomSummary: RoomSummary) {
if (!actionPermissions.canEnableEncryption) {
return
}
if (roomSummary.isEncrypted) {
buildProfileAction(
id = "encryption",
title = stringProvider.getString(R.string.room_settings_addresses_e2e_enabled),
dividerColor = dividerColor,
divider = false,
editable = false
)
} else {
buildProfileAction(
id = "encryption",
title = stringProvider.getString(R.string.room_settings_enable_encryption),
subtitle = stringProvider.getString(R.string.room_settings_enable_encryption_warning),
dividerColor = dividerColor,
divider = false,
editable = true,
action = { callback?.onEnableEncryptionClicked() }
)
}
} }
private fun formatRoomHistoryVisibilityEvent(event: Event): String? { private fun formatRoomHistoryVisibilityEvent(event: Event): String? {

View File

@ -127,17 +127,6 @@ class RoomSettingsFragment @Inject constructor(
invalidateOptionsMenu() invalidateOptionsMenu()
} }
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) { _, _ ->
viewModel.handle(RoomSettingsAction.EnableEncryption)
}
.show()
}
override fun onNameChanged(name: String) { override fun onNameChanged(name: String) {
viewModel.handle(RoomSettingsAction.SetRoomName(name)) viewModel.handle(RoomSettingsAction.SetRoomName(name))
} }

View File

@ -17,7 +17,6 @@
package im.vector.app.features.roomprofile.settings package im.vector.app.features.roomprofile.settings
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
@ -28,7 +27,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Observable import io.reactivex.Observable
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -118,8 +116,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_CANONICAL_ALIAS), EventType.STATE_ROOM_CANONICAL_ALIAS),
canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_HISTORY_VISIBILITY), EventType.STATE_ROOM_HISTORY_VISIBILITY)
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
) )
setState { copy(actionPermissions = permissions) } setState { copy(actionPermissions = permissions) }
} }
@ -142,7 +139,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomSettingsAction) { override fun handle(action: RoomSettingsAction) {
when (action) { when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
is RoomSettingsAction.SetAvatarAction -> handleSetAvatarAction(action) is RoomSettingsAction.SetAvatarAction -> handleSetAvatarAction(action)
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) }
@ -226,18 +222,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
) )
} }
private fun handleEnableEncryption() {
postLoading(true)
viewModelScope.launch {
val result = runCatching { room.enableEncryption() }
postLoading(false)
result.onFailure { failure ->
_viewEvents.post(RoomSettingsViewEvents.Failure(failure))
}
}
}
private fun postLoading(isLoading: Boolean) { private fun postLoading(isLoading: Boolean) {
setState { setState {
copy(isLoading = isLoading) copy(isLoading = isLoading)

View File

@ -47,8 +47,7 @@ data class RoomSettingsViewState(
val canChangeName: Boolean = false, val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false, val canChangeTopic: Boolean = false,
val canChangeCanonicalAlias: Boolean = false, val canChangeCanonicalAlias: Boolean = false,
val canChangeHistoryReadability: Boolean = false, val canChangeHistoryReadability: Boolean = false
val canEnableEncryption: Boolean = false
) )
sealed class AvatarAction { sealed class AvatarAction {

View File

@ -15,12 +15,13 @@
*/ */
package im.vector.app.features.settings package im.vector.app.features.settings
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.preference.PushRulePreference import im.vector.app.core.preference.PushRulePreference
import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import org.matrix.android.sdk.api.MatrixCallback import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind
import javax.inject.Inject import javax.inject.Inject
@ -50,29 +51,25 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor()
if (newRule != null) { if (newRule != null) {
displayLoadingView() displayLoadingView()
session.updatePushRuleActions( lifecycleScope.launch {
ruleAndKind.kind, val result = runCatching {
preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule, session.updatePushRuleActions(ruleAndKind.kind,
newRule, preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule,
object : MatrixCallback<Unit> { newRule)
override fun onSuccess(data: Unit) { }
if (!isAdded) { if (!isAdded) {
return return@launch
} }
preference.setPushRule(ruleAndKind.copy(pushRule = newRule)) hideLoadingView()
hideLoadingView() result.onSuccess {
} preference.setPushRule(ruleAndKind.copy(pushRule = newRule))
}
override fun onFailure(failure: Throwable) { result.onFailure { failure ->
if (!isAdded) { // Restore the previous value
return refreshDisplay()
} activity?.toast(errorFormatter.toHumanReadable(failure))
hideLoadingView() }
// Restore the previous value }
refreshDisplay()
activity?.toast(errorFormatter.toHumanReadable(failure))
}
})
} }
false false
} }

View File

@ -23,6 +23,7 @@ import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import im.vector.app.R import im.vector.app.R
@ -37,6 +38,7 @@ import im.vector.app.core.utils.isIgnoringBatteryOptimizations
import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleIds
@ -318,24 +320,22 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
.find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL } .find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL }
?.let { ?.let {
// Trick, we must enable this room to disable notifications // Trick, we must enable this room to disable notifications
pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, lifecycleScope.launch {
it, try {
!switchPref.isChecked, pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE,
object : MatrixCallback<Unit> { it,
override fun onSuccess(data: Unit) { !switchPref.isChecked)
// Push rules will be updated from the sync // Push rules will be updated from the sync
} } catch (failure: Throwable) {
if (!isAdded) {
return@launch
}
override fun onFailure(failure: Throwable) { // revert the check box
if (!isAdded) { switchPref.isChecked = !switchPref.isChecked
return Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show()
} }
}
// revert the check box
switchPref.isChecked = !switchPref.isChecked
Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show()
}
})
} }
} }
} }

View File

@ -20,7 +20,8 @@ import androidx.activity.result.ActivityResultLauncher
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.MatrixCallback import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleKind
import javax.inject.Inject import javax.inject.Inject
@ -48,16 +49,12 @@ class TestAccountSettings @Inject constructor(private val stringProvider: String
override fun doFix() { override fun doFix() {
if (manager?.diagStatus == TestStatus.RUNNING) return // wait before all is finished if (manager?.diagStatus == TestStatus.RUNNING) return // wait before all is finished
session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled, GlobalScope.launch {
object : MatrixCallback<Unit> { runCatching {
override fun onSuccess(data: Unit) { session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled)
manager?.retry(activityResultLauncher) }
} manager?.retry(activityResultLauncher)
}
override fun onFailure(failure: Throwable) {
manager?.retry(activityResultLauncher)
}
})
} }
} }
status = TestStatus.FAILED status = TestStatus.FAILED

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19.1001,9C18.7779,9 18.5168,8.7388 18.5168,8.4167V6.0833H16.1834C15.8613,6.0833 15.6001,5.8222 15.6001,5.5C15.6001,5.1778 15.8613,4.9167 16.1834,4.9167H18.5168V2.5833C18.5168,2.2612 18.7779,2 19.1001,2C19.4223,2 19.6834,2.2612 19.6834,2.5833V4.9167H22.0168C22.3389,4.9167 22.6001,5.1778 22.6001,5.5C22.6001,5.8222 22.3389,6.0833 22.0168,6.0833H19.6834V8.4167C19.6834,8.7388 19.4223,9 19.1001,9ZM19.6001,11C20.0669,11 20.5212,10.9467 20.9574,10.8458C21.1161,11.5383 21.2,12.2594 21.2,13C21.2,16.1409 19.6917,18.9294 17.3598,20.6808V20.6807C16.0014,21.7011 14.3635,22.3695 12.5815,22.5505C12.2588,22.5832 11.9314,22.6 11.6,22.6C6.2981,22.6 2,18.302 2,13C2,7.6981 6.2981,3.4 11.6,3.4C12.3407,3.4 13.0618,3.4839 13.7543,3.6427C13.6534,4.0788 13.6001,4.5332 13.6001,5C13.6001,8.3137 16.2864,11 19.6001,11ZM11.5999,20.68C13.6754,20.68 15.5585,19.8567 16.9407,18.5189C16.0859,16.4086 14.0167,14.92 11.5998,14.92C9.183,14.92 7.1138,16.4086 6.259,18.5189C7.6411,19.8567 9.5244,20.68 11.5999,20.68ZM11.7426,7.4117C10.3168,7.5417 9.2,8.7404 9.2,10.2C9.2,11.7464 10.4536,13 12,13C13.0308,13 13.9315,12.443 14.4176,11.6135C13.0673,10.6058 12.0929,9.1225 11.7426,7.4117Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -93,6 +93,27 @@
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
tools:visibility="visible" /> tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/phoneBookSearchForMatrixContacts"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:text="@string/phone_book_perform_lookup"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/phoneBookBottomBarrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="bottom"
app:constraint_referenced_ids="phoneBookSearchForMatrixContacts,phoneBookOnlyBoundContacts" />
<View <View
android:id="@+id/phoneBookFilterDivider" android:id="@+id/phoneBookFilterDivider"
android:layout_width="0dp" android:layout_width="0dp"
@ -101,7 +122,7 @@
android:background="?attr/vctr_list_divider_color" android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" /> app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/phoneBookRecyclerView" android:id="@+id/phoneBookRecyclerView"

View File

@ -95,7 +95,6 @@
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@ -105,4 +104,6 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_profile_action" /> tools:listitem="@layout/item_profile_action" />
<include layout="@layout/merge_overlay_waiting_view" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,31 +1,149 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:orientation="vertical">
<FrameLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/creationEncryptionTile" android:id="@+id/creationTile"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_alignParentTop="true"
android:layout_gravity="center"
android:layout_marginTop="2dp"
android:layout_marginEnd="52dp"
android:layout_marginBottom="2dp"
android:background="@drawable/rounded_rect_shape_8"
android:padding="8dp">
<include layout="@layout/item_timeline_event_status_tile_stub" /> <FrameLayout
android:id="@+id/creationEncryptionTile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="52dp"
android:layout_marginBottom="2dp"
android:background="@drawable/rounded_rect_shape_8"
android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</FrameLayout> <include layout="@layout/item_timeline_event_status_tile_stub" />
</FrameLayout>
<ImageView
android:id="@+id/creationTileRoomAvatarImageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/creationEncryptionTile"
tools:srcCompat="@tools:sample/avatars" />
<com.google.android.material.button.MaterialButton
android:id="@+id/creationTileSetAvatarButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:backgroundTint="?riotx_bottom_nav_background_color"
android:contentDescription="@string/room_settings_set_avatar"
android:elevation="2dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_camera"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="?riot_primary_text_color"
app:layout_constraintCircle="@+id/creationTileRoomAvatarImageView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="34dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/roomNameTileText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/creationTileRoomAvatarImageView"
tools:text="@sample/matrix.json/data/roomName" />
<TextView
android:id="@+id/roomNameDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNameTileText"
tools:text="@string/this_is_the_beginning_of_room_no_name" />
<TextView
android:id="@+id/roomNameTopicText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:textColor="?riotx_text_secondary"
android:textColorLink="@color/riotx_accent"
android:textSize="15sp"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNameDescriptionText"
tools:text="@string/room_created_summary_no_topic_creation_text" />
<LinearLayout
android:id="@+id/creationTileAddPeopleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/add_people"
android:focusable="true"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNameTopicText">
<ImageView
android:id="@+id/addPeopleButtonBg"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/circle"
android:backgroundTint="@color/riotx_accent"
android:importantForAccessibility="no"
android:scaleType="center"
android:src="@drawable/ic_add_people" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:importantForAccessibility="no"
android:text="@string/add_people"
android:textColor="@color/riotx_accent" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/mergedSumContainer" android:id="@+id/mergedSumContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/creationEncryptionTile"> android:layout_below="@+id/creationTile"
android:layout_marginTop="8dp">
<ImageView <ImageView
android:id="@+id/itemNoticeAvatarView" android:id="@+id/itemNoticeAvatarView"

View File

@ -1793,6 +1793,14 @@
<string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string> <string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string>
<string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string> <string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string>
<string name="settings_discovery_mail_pending">Pending</string> <string name="settings_discovery_mail_pending">Pending</string>
<string name="settings_discovery_consent_title">Send emails and phone numbers</string>
<string name="settings_discovery_consent_notice_on">You have given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
<string name="settings_discovery_consent_notice_off">You have not given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
<string name="settings_discovery_consent_action_revoke">Revoke my consent</string>
<string name="settings_discovery_consent_action_give_consent">Give consent</string>
<string name="identity_server_consent_dialog_title">Send emails and phone numbers</string>
<string name="identity_server_consent_dialog_content">In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured Identity Server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent.</string>
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string> <string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string> <string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
@ -2192,7 +2200,8 @@
<!-- Title for category in the settings which affect the behavior of the message editor (ex: enable Markdown, send typing notification, etc.) --> <!-- Title for category in the settings which affect the behavior of the message editor (ex: enable Markdown, send typing notification, etc.) -->
<string name="settings_category_composer">Message editor</string> <string name="settings_category_composer">Message editor</string>
<string name="room_settings_enable_encryption">Enable end-to-end encryption</string> <string name="room_settings_enable_encryption">Enable end-to-end encryption…</string>
<string name="room_settings_enable_encryption_no_permission">You don\'t have permission to enable encryption in this room.</string>
<string name="room_settings_enable_encryption_warning">Once enabled, encryption cannot be disabled.</string> <string name="room_settings_enable_encryption_warning">Once enabled, encryption cannot be disabled.</string>
<string name="room_settings_enable_encryption_dialog_title">Enable encryption?</string> <string name="room_settings_enable_encryption_dialog_title">Enable encryption?</string>
@ -2395,6 +2404,13 @@
<string name="room_created_summary_item_by_you">You created and configured the room.</string> <string name="room_created_summary_item_by_you">You created and configured the room.</string>
<string name="direct_room_created_summary_item">%s joined.</string> <string name="direct_room_created_summary_item">%s joined.</string>
<string name="direct_room_created_summary_item_by_you">You joined.</string> <string name="direct_room_created_summary_item_by_you">You joined.</string>
<string name="this_is_the_beginning_of_room">This is the beginning of %s.</string>
<string name="this_is_the_beginning_of_room_no_name">This is the beginning of this conversation.</string>
<string name="this_is_the_beginning_of_dm">This is the beginning of your direct message history with %s.</string>
<!-- First param will be replaced by the value of add_a_topic_link_text, that will be clickable-->
<string name="room_created_summary_no_topic_creation_text">%s to let people know what this room is about.</string>
<string name="add_a_topic_link_text">Add a topic</string>
<string name="topic_prefix">"Topic: "</string>
<string name="qr_code_scanned_self_verif_notice">Almost there! Is the other device showing the same shield?</string> <string name="qr_code_scanned_self_verif_notice">Almost there! Is the other device showing the same shield?</string>
<string name="qr_code_scanned_verif_waiting_notice">Almost there! Waiting for confirmation…</string> <string name="qr_code_scanned_verif_waiting_notice">Almost there! Waiting for confirmation…</string>
@ -2503,6 +2519,7 @@
<string name="create_room_dm_failure">"We couldn't create your DM. Please check the users you want to invite and try again."</string> <string name="create_room_dm_failure">"We couldn't create your DM. Please check the users you want to invite and try again."</string>
<string name="add_members_to_room">Add members</string> <string name="add_members_to_room">Add members</string>
<string name="add_people">Add people</string>
<string name="invite_users_to_room_action_invite">INVITE</string> <string name="invite_users_to_room_action_invite">INVITE</string>
<string name="inviting_users_to_room">Inviting users…</string> <string name="inviting_users_to_room">Inviting users…</string>
<string name="invite_users_to_room_title">Invite Users</string> <string name="invite_users_to_room_title">Invite Users</string>
@ -2527,6 +2544,7 @@
<string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string> <string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string>
<string name="identity_server_error_binding_error">The association has failed.</string> <string name="identity_server_error_binding_error">The association has failed.</string>
<string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string> <string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string>
<string name="identity_server_user_consent_not_provided">The user consent has not been provided.</string>
<string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string> <string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string>
<string name="identity_server_set_default_submit">Use %1$s</string> <string name="identity_server_set_default_submit">Use %1$s</string>
@ -2566,6 +2584,8 @@
<string name="room_settings_name_hint">Room Name</string> <string name="room_settings_name_hint">Room Name</string>
<string name="room_settings_topic_hint">Topic</string> <string name="room_settings_topic_hint">Topic</string>
<string name="room_settings_save_success">You changed room settings successfully</string> <string name="room_settings_save_success">You changed room settings successfully</string>
<string name="room_settings_set_avatar">Set avatar</string>
<string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string> <string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string>
<string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string> <string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string>
@ -2593,6 +2613,7 @@
<string name="loading_contact_book">Retrieving your contacts…</string> <string name="loading_contact_book">Retrieving your contacts…</string>
<string name="empty_contact_book">Your contact book is empty</string> <string name="empty_contact_book">Your contact book is empty</string>
<string name="contacts_book_title">Contacts book</string> <string name="contacts_book_title">Contacts book</string>
<string name="phone_book_perform_lookup">Search for contacts on Matrix</string>
<string name="three_pid_revoke_invite_dialog_title">Revoke invite</string> <string name="three_pid_revoke_invite_dialog_title">Revoke invite</string>
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string> <string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>