Merge branch 'develop' into feature/bma_create_room_form

This commit is contained in:
Benoit Marty 2020-11-20 13:57:35 +01:00 committed by GitHub
commit 85bc5f54aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
118 changed files with 1900 additions and 964 deletions

View File

@ -4,7 +4,7 @@ A full developer contributors list can be found [here](https://github.com/vector
Even if we try to be able to work on all the functionalities, we have more knowledge about what we have developed ourselves.
## Benoit: Android team leader
## [Benoit](https://github.com/bmarty): Android team leader
[@benoit.marty:matrix.org](https://matrix.to/#/@benoit.marty:matrix.org)
- Android team leader and project leader, Android developer, GitHub community manager.
@ -12,7 +12,7 @@ Even if we try to be able to work on all the functionalities, we have more knowl
- Reviewing and polishing developed features, code quality manager, PRs reviewer, GitHub community manager.
- Release manager on the Play Store
## François: Software architect
## [Ganfra](https://github.com/ganfra) (aka François): Software architect
[@ganfra:matrix.org](https://matrix.to/#/@ganfra:matrix.org)
- Software architect, Android developer
@ -20,12 +20,17 @@ Even if we try to be able to work on all the functionalities, we have more knowl
- Work mainly on the global architecture of the project.
- Specialist of the timeline, and lots of other features.
## Valere: Product manager, Android developer
## [Valere](https://github.com/BillCarsonFr): Product manager, Android developer
[@valere35:matrix.org](https://matrix.to/#/@valere35:matrix.org)
- Product manager, Android developer
- Specialist on the crypto implementation.
## [Onuray](https://github.com/onurays): Android developer
[@onurays:matrix.org](https://matrix.to/#/@onurays:matrix.org)
- Android developer
# Other contributors
First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.

View File

@ -5,15 +5,22 @@ Features ✨:
-
Improvements 🙌:
- New room creation tile with quick action (#2346)
- Open an existing DM instead of creating a new one (#2319)
- Use RoomMember instead of User in the context of a Room.
- Ask for explicit user consent to send their contact details to the identity server (#2375)
- Handle events of type "m.room.server_acl" (#890)
- Room creation form: add advanced section to disable federation (#1314)
- Move "Enable Encryption" from room setting screen to room profile screen (#2394)
Bugfix 🐛:
- Fix crash on AttachmentViewer (#2365)
- Exclude yourself when decorating rooms which are direct or don't have more than 2 users (#2370)
- F-Droid version: ensure timeout of sync request can be more than 60 seconds (#2169)
- Fix issue when restoring draft after sharing (#2287)
- Fix issue when updating the avatar of a room (new avatar vanishing)
- Discard change dialog displayed by mistake when avatar has been updated
- Try to fix cropped image in timeline (#2126)
- Registration: annoying error message scares every new user when they add an email (#2391)
Translations 🗣:
@ -25,6 +32,9 @@ SDK API changes ⚠️:
Build 🧱:
-
Test:
- Add `allScreensTest` to cover all screens of the app
Other changes:
- Upgrade Realm dependency to 10.0.0
@ -1038,5 +1048,8 @@ SDK API changes ⚠️:
Build 🧱:
-
Test:
-
Other changes:
-

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.sync.SyncState
@ -92,6 +93,13 @@ class RxSession(private val session: Session) {
}
}
fun liveRoomMember(userId: String, roomId: String): Observable<Optional<RoomMemberSummary>> {
return session.getRoomMemberLive(userId, roomId).asObservable()
.startWithCallable {
session.getRoomMember(userId, roomId).toOptional()
}
}
fun liveUsers(): Observable<List<User>> {
return session.getUsersLive().asObservable()
}

View File

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

View File

@ -15,11 +15,9 @@
*/
package org.matrix.android.sdk.api.pushrules
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable
interface PushRuleService {
/**
@ -29,13 +27,13 @@ interface PushRuleService {
fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet
fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean)
fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
suspend fun addPushRule(kind: RuleKind, pushRule: PushRule)
fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule)
fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
fun addPushRuleListener(listener: PushRuleListener)

View File

@ -16,9 +16,6 @@
package org.matrix.android.sdk.api.session.group
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
/**
* This interface defines methods to interact within a group.
*/
@ -28,8 +25,7 @@ interface Group {
/**
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
* The SDK also takes care of refreshing group data every hour.
* @param callback : the matrix callback to be notified of success or failure
* @return a Cancelable to be able to cancel requests.
*/
fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable
suspend fun fetchGroupData()
}

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.permalinks
import android.text.Spannable
import org.matrix.android.sdk.api.MatrixPatterns
/**
* MatrixLinkify take a piece of text and turns all of the
@ -35,7 +36,7 @@ object MatrixLinkify {
* I disable it because it mess up with pills, and even with pills, it does not work correctly:
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
*/
/*
// sanity checks
if (spannable.isEmpty()) {
return false
@ -48,14 +49,21 @@ object MatrixLinkify {
val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 1
val url = text.substring(match.range)
var url = text.substring(match.range)
if (MatrixPatterns.isUserId(url)
|| MatrixPatterns.isRoomAlias(url)
|| MatrixPatterns.isRoomId(url)
|| MatrixPatterns.isGroupId(url)
|| MatrixPatterns.isEventId(url)) {
url = PermalinkService.MATRIX_TO_URL_BASE + url
}
val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
return hasMatch
*/
return false
// return false
}
}

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.util.Cancelable
@ -141,4 +142,20 @@ interface RoomService {
* - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
*/
fun getExistingDirectRoomWithUser(otherUserId: String): String?
/**
* Get a room member for the tuple {userId,roomId}
* @param userId the userId to look for.
* @param roomId the roomId to look for.
* @return the room member or null
*/
fun getRoomMember(userId: String, roomId: String): RoomMemberSummary?
/**
* Observe a live room member for the tuple {userId,roomId}
* @param userId the userId to look for.
* @param roomId the roomId to look for.
* @return a LiveData of the optional found room member
*/
fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>>
}

View File

@ -17,8 +17,6 @@
package org.matrix.android.sdk.api.session.room.send
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
interface DraftService {
@ -26,12 +24,12 @@ interface DraftService {
/**
* Save or update a draft to the room
*/
fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable
suspend fun saveDraft(draft: UserDraft)
/**
* Delete the last draft, basically just after sending the message
*/
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
suspend fun deleteDraft()
/**
* Return the current draft or null

View File

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

View File

@ -241,9 +241,9 @@ internal class UpdateTrustWorker(context: Context,
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
// The set of “all users” depends on the type of room:
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (roomSummaryEntity.isDirect) {
val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) {
activeMemberUserIds.filter { it != myUserId }
} else {
activeMemberUserIds

View File

@ -1679,27 +1679,24 @@ internal class RealmCryptoStore @Inject constructor(
// Only keep one week history
realm.where<IncomingGossipingRequestEntity>()
.lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs)
.findAll().let {
Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity")
it.deleteAllFromRealm()
}
.findAll()
.also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") }
.deleteAllFromRealm()
// Clean the cancelled ones?
realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.findAll().let {
Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity")
it.deleteAllFromRealm()
}
.findAll()
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") }
.deleteAllFromRealm()
// Only keep one week history
realm.where<GossipingEventEntity>()
.lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs)
.findAll().let {
Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields")
it.deleteAllFromRealm()
}
.findAll()
.also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") }
.deleteAllFromRealm()
// Can we do something for WithHeldSessionEntity?
}

View File

@ -52,5 +52,8 @@ internal class TimeOutInterceptor @Inject constructor() : Interceptor {
const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT"
const val READ_TIMEOUT = "READ_TIMEOUT"
const val WRITE_TIMEOUT = "WRITE_TIMEOUT"
// 1 minute
const val DEFAULT_LONG_TIMEOUT: Long = 60_000
}
}

View File

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

View File

@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.group
import org.matrix.android.sdk.api.session.group.Group
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject
internal interface GroupFactory {
@ -26,14 +25,12 @@ internal interface GroupFactory {
}
@SessionScope
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask,
private val taskExecutor: TaskExecutor) :
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) :
GroupFactory {
override fun create(groupId: String): Group {
return DefaultGroup(
groupId = groupId,
taskExecutor = taskExecutor,
getGroupDataTask = getGroupDataTask
)
}

View File

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.notification
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.pushrules.PushRuleService
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleSetKey
@ -24,7 +23,6 @@ import org.matrix.android.sdk.api.pushrules.getActions
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper
import org.matrix.android.sdk.internal.database.model.PushRulesEntity
import org.matrix.android.sdk.internal.database.query.where
@ -103,37 +101,21 @@ internal class DefaultPushRuleService @Inject constructor(
)
}
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
override suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) {
// The rules will be updated, and will come back from the next sync response
return updatePushRuleEnableStatusTask
.configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) {
this.callback = callback
}
.executeBy(taskExecutor)
updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled))
}
override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
return addPushRuleTask
.configureWith(AddPushRuleTask.Params(kind, pushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) {
addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule))
}
override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
return updatePushRuleActionsTask
.configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) {
updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule))
}
override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
return removePushRuleTask
.configureWith(RemovePushRuleTask.Params(kind, pushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule))
}
override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {

View File

@ -17,27 +17,37 @@
package org.matrix.android.sdk.internal.session.room
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.fetchCopied
import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
@ -118,4 +128,24 @@ internal class DefaultRoomService @Inject constructor(
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
return roomChangeMembershipStateDataSource.getLiveStates()
}
override fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? {
val roomMemberEntity = monarchy.fetchCopied {
RoomMemberHelper(it, roomId).getLastRoomMember(userId)
}
return roomMemberEntity?.asDomain()
}
override fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm ->
RoomMemberHelper(realm, roomId).queryRoomMembersEvent()
.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId)
},
{ it.asDomain() }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
}

View File

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

View File

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

View File

@ -19,18 +19,14 @@ package org.matrix.android.sdk.internal.session.room.draft
import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import org.matrix.android.sdk.api.MatrixCallback
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.room.send.DraftService
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
private val draftRepository: DraftRepository,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : DraftService {
@ -43,14 +39,14 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
* or even move an existing draft to the top of the list
*/
override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
override suspend fun saveDraft(draft: UserDraft) {
withContext(coroutineDispatchers.main) {
draftRepository.saveDraft(roomId, draft)
}
}
override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
override suspend fun deleteDraft() {
withContext(coroutineDispatchers.main) {
draftRepository.deleteDraft(roomId)
}
}

View File

@ -93,6 +93,8 @@ internal class RoomDisplayNameResolver @Inject constructor(
}
} else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val invitedCount = roomSummary?.invitedMembersCount ?: 0
val joinedCount = roomSummary?.joinedMembersCount ?: 0
val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { userId ->
roomMembers.getLastRoomMember(userId)?.takeIf {
@ -102,22 +104,49 @@ internal class RoomDisplayNameResolver @Inject constructor(
} else {
activeMembers.where()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.limit(3)
.limit(5)
.findAll()
.createSnapshot()
}
val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) {
0 -> stringProvider.getString(R.string.room_displayname_empty_room)
0 -> {
stringProvider.getString(R.string.room_displayname_empty_room)
// TODO (was xx and yyy) ...
}
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> stringProvider.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1)
2 -> {
stringProvider.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
}
3 -> {
stringProvider.getString(R.string.room_displayname_3_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
resolveRoomMemberName(otherMembersSubset[2], roomMembers)
)
}
4 -> {
stringProvider.getString(R.string.room_displayname_4_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
resolveRoomMemberName(otherMembersSubset[2], roomMembers),
resolveRoomMemberName(otherMembersSubset[3], roomMembers)
)
}
else -> {
val remainingCount = invitedCount + joinedCount - otherMembersCount + 1
stringProvider.getQuantityString(
R.plurals.room_displayname_four_and_more_members,
remainingCount,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers),
resolveRoomMemberName(otherMembersSubset[2], roomMembers),
remainingCount
)
}
}
}
return name ?: roomId

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.session.room.membership
import io.realm.Realm
import io.realm.RealmQuery
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
@ -25,8 +27,6 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
import io.realm.Realm
import io.realm.RealmQuery
/**
* This class is an helper around STATE_ROOM_MEMBER events.

View File

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

View File

@ -17,18 +17,21 @@
package org.matrix.android.sdk.internal.session.sync
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.network.TimeOutInterceptor
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Header
import retrofit2.http.QueryMap
internal interface SyncAPI {
/**
* Set all the timeouts to 1 minute
* Set all the timeouts to 1 minute by default
*/
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync")
fun sync(@QueryMap params: Map<String, String>): Call<SyncResponse>
fun sync(@QueryMap params: Map<String, String>,
@Header(TimeOutInterceptor.CONNECT_TIMEOUT) connectTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT,
@Header(TimeOutInterceptor.READ_TIMEOUT) readTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT,
@Header(TimeOutInterceptor.WRITE_TIMEOUT) writeTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT
): Call<SyncResponse>
}

View File

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

View File

@ -175,13 +175,22 @@
<!-- The 2 parameters will be members' name -->
<string name="room_displayname_two_members">%1$s and %2$s</string>
<!-- The 3 parameters will be members' name -->
<string name="room_displayname_3_members">%1$s, %2$s and %3$s</string>
<!-- The 4 parameters will be members' name -->
<string name="room_displayname_4_members">%1$s, %2$s, %3$s and %4$s</string>
<!-- The 3 first parameters will be members' name -->
<plurals name="room_displayname_four_and_more_members">
<item quantity="one">%1$s, %2$s, %3$s and %4$d other</item>
<item quantity="other">%1$s, %2$s, %3$s and %4$d others</item>
</plurals>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s and 1 other</item>
<item quantity="other">%1$s and %2$d others</item>
</plurals>
<string name="room_displayname_empty_room">Empty room</string>
<string name="room_displayname_empty_room_was">Empty room (was %s)</string>
<string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string>
<string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string>

View File

@ -19,6 +19,7 @@
echo "Configure Element Template..."
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
{
mkdir -p "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
} && {
echo "Please restart Android Studio."

24
tools/templates/unconfigure.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
#
# Copyright 2020 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Template prevent from upgrading Android Studio, so this script de configure the template
echo "Un-configure Element Template..."
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
rm "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other/ElementFeature"
rm -r "${ANDROID_STUDIO%/}/plugins/android/lib/templates"

View File

@ -461,6 +461,10 @@ dependencies {
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
// Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// "The one who serves a great Espresso"
androidTestImplementation('com.schibsted.spain:barista:3.7.0') {
exclude group: 'org.jetbrains.kotlin'
}
}
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {

View File

@ -67,7 +67,7 @@ class RegistrationTest {
.perform(click())
// Enter local synapse
onView((withId(R.id.loginServerUrlFormHomeServerUrl)))
onView(withId(R.id.loginServerUrlFormHomeServerUrl))
.perform(typeText(homeServerUrl))
// Click on continue
@ -87,7 +87,7 @@ class RegistrationTest {
.check(matches(isDisplayed()))
// Ensure user id
onView((withId(R.id.loginField)))
onView(withId(R.id.loginField))
.perform(typeText(userId))
// Ensure login button not yet enabled
@ -95,7 +95,7 @@ class RegistrationTest {
.check(matches(not(isEnabled())))
// Ensure password
onView((withId(R.id.passwordField)))
onView(withId(R.id.passwordField))
.perform(closeSoftKeyboard(), typeText(password))
// Submit

View File

@ -79,7 +79,7 @@ class SecurityBootstrapTest : VerificationTestBase() {
fun testBasicBootstrap() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {

View File

@ -18,15 +18,11 @@ package im.vector.app
import android.net.Uri
import androidx.lifecycle.Observer
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import im.vector.app.ui.UiTestBase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
import org.junit.Assert
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixCallback
@ -43,108 +39,12 @@ abstract class VerificationTestBase {
val password = "password"
val homeServerUrl: String = "http://10.0.2.2:8080"
fun doLogin(homeServerUrl: String, userId: String, password: String) {
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
protected val uiTestBase = UiTestBase()
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
// Chose custom server
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
.perform(ViewActions.click())
// Enter local synapse
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(ViewActions.typeText(homeServerUrl))
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
// Click on the signin button
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSignIn))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
// Ensure password flow supported
Espresso.onView(ViewMatchers.withId(R.id.loginField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
.perform(ViewActions.typeText(userId))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.typeText(password))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
}
private fun createAccount(userId: String = "UiAutoTest", password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
// Chose custom server
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
.perform(ViewActions.click())
// Enter local synapse
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(ViewActions.typeText(homeServerUrl))
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
// Click on the signup button
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
// Ensure password flow supported
Espresso.onView(ViewMatchers.withId(R.id.loginField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
.perform(ViewActions.typeText(userId))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
.perform(ViewActions.typeText(password))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.homeDrawerFragmentContainer))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
fun createAccountAndSync(matrix: Matrix, userName: String,
password: String,
withInitialSync: Boolean): Session {
fun createAccountAndSync(matrix: Matrix,
userName: String,
password: String,
withInitialSync: Boolean): Session {
val hs = createHomeServerConfig()
doSync<LoginFlowResult> {
@ -174,7 +74,7 @@ abstract class VerificationTestBase {
return session
}
fun createHomeServerConfig(): HomeServerConnectionConfig {
private fun createHomeServerConfig(): HomeServerConnectionConfig {
return HomeServerConnectionConfig.Builder()
.withHomeServerUri(Uri.parse(homeServerUrl))
.build()
@ -200,7 +100,7 @@ abstract class VerificationTestBase {
return result!!
}
fun syncSession(session: Session) {
private fun syncSession(session: Session) {
val lock = CountDownLatch(1)
GlobalScope.launch(Dispatchers.Main) { session.open() }

View File

@ -78,7 +78,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
fun checkVerifyPopup() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
@ -215,10 +215,10 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
}
fun signout() {
onView((withId(R.id.groupToolbarAvatarImageView)))
onView(withId(R.id.groupToolbarAvatarImageView))
.perform(click())
onView((withId(R.id.homeDrawerHeaderSettingsView)))
onView(withId(R.id.homeDrawerHeaderSettingsView))
.perform(click())
onView(withText("General"))

View File

@ -88,7 +88,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
fun checkVerifyWithPassphrase() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
@ -137,10 +137,10 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
onView(withId(R.id.ssss__root)).check(matches(isDisplayed()))
}
onView((withId(R.id.ssss_passphrase_enter_edittext)))
onView(withId(R.id.ssss_passphrase_enter_edittext))
.perform(typeText(passphrase))
onView((withId(R.id.ssss_passphrase_submit)))
onView(withId(R.id.ssss_passphrase_submit))
.perform(click())
System.out.println("*** passphrase 1")

View File

@ -0,0 +1,46 @@
/*
* 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.espresso.tools
import android.widget.Switch
import androidx.annotation.StringRes
import androidx.preference.Preference
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.PreferenceMatchers.withKey
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import im.vector.app.R
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.instanceOf
fun clickOnPreference(@StringRes textResId: Int) {
onView(withId(R.id.recycler_view))
.perform(actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(textResId)), click()))
}
fun clickOnSwitchPreference(preferenceKey: String) {
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey(preferenceKey)))
.onChildView(withClassName(`is`(Switch::class.java.name))).perform(click())
}

View File

@ -0,0 +1,25 @@
/*
* 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.espresso.tools
import android.app.Activity
import im.vector.app.activityIdlingResource
import im.vector.app.withIdlingResource
inline fun <reified T : Activity> waitUntilActivityVisible(noinline block: (() -> Unit)) {
withIdlingResource(activityIdlingResource(T::class.java), block)
}

View File

@ -0,0 +1,460 @@
/*
* 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.ui
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.schibsted.spain.barista.assertion.BaristaListAssertions.assertListItemCount
import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickBack
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.longClickOn
import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton
import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo
import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItem
import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItemChild
import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.clickMenu
import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.openMenu
import im.vector.app.EspressoHelper
import im.vector.app.R
import im.vector.app.SleepViewAction
import im.vector.app.activityIdlingResource
import im.vector.app.espresso.tools.clickOnPreference
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.features.MainActivity
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.initialSyncIdlingResource
import im.vector.app.waitForView
import im.vector.app.withIdlingResource
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.Thread.sleep
import java.util.UUID
/**
* This test aim to open every possible screen of the application
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class UiAllScreensSanityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val uiTestBase = UiTestBase()
// Last passing: 2020-11-09
@Test
fun allScreensTest() {
// Create an account
val userId = "UiTest_" + UUID.randomUUID().toString()
uiTestBase.createAccount(userId = userId)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
assertDisplayed(R.id.roomListContainer)
closeSoftKeyboard()
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
assertDisplayed(R.id.roomListContainer)
}
assertDisplayed(R.id.bottomNavigationView)
// Settings
navigateToSettings()
// Create DM
clickOn(R.id.bottom_action_people)
createDm()
// Create Room
// First navigate to the other tab
clickOn(R.id.bottom_action_rooms)
createRoom()
assertDisplayed(R.id.bottomNavigationView)
// Long click on the room
onView(withId(R.id.roomListView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.room_displayname_empty_room)),
longClick()
)
)
pressBack()
uiTestBase.signout()
// We have sent a message in a e2e room, accept to loose it
clickOn(R.id.exitAnywayButton)
// Dark pattern
clickDialogNegativeButton()
// Login again on the same account
waitUntilActivityVisible<LoginActivity> {
assertDisplayed(R.id.loginSplashLogo)
}
uiTestBase.login(userId)
ignoreVerification()
uiTestBase.signout()
clickDialogPositiveButton()
// TODO Deactivate account instead of logout?
}
private fun ignoreVerification() {
Thread.sleep(6000)
val activity = EspressoHelper.getCurrentActivity()!!
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
assertDisplayed(R.id.bottomSheetFragmentContainer)
onView(ViewMatchers.isRoot()).perform(SleepViewAction.sleep(2000))
clickOn(R.string.skip)
assertDisplayed(R.string.are_you_sure)
clickOn(R.string.skip)
}
private fun createRoom() {
clickOn(R.id.createGroupRoomButton)
waitUntilActivityVisible<RoomDirectoryActivity> {
assertDisplayed(R.id.publicRoomsList)
}
clickOn(R.string.create_new_room)
// Create
assertListItemCount(R.id.createRoomForm, 10)
clickListItemChild(R.id.createRoomForm, 9, R.id.form_submit_button)
waitUntilActivityVisible<RoomDetailActivity> {
assertDisplayed(R.id.roomDetailContainer)
}
clickOn(R.id.attachmentButton)
clickBack()
// Send a message
writeTo(R.id.composerEditText, "Hello world!")
clickOn(R.id.sendButton)
navigateToRoomSettings()
// Long click on the message
longClickOnMessageTest()
// Menu
openMenu()
pressBack()
clickMenu(R.id.voice_call)
pressBack()
clickMenu(R.id.video_call)
pressBack()
pressBack()
}
private fun longClickOnMessageTest() {
// Test quick reaction
longClickOnMessage()
// Add quick reaction
clickOn("👍")
sleep(1000)
// Open reactions
longClickOn("👍")
pressBack()
// Test add reaction
longClickOnMessage()
clickOn(R.string.message_add_reaction)
// Filter
// TODO clickMenu(R.id.search)
clickListItem(R.id.emojiRecyclerView, 4)
// Test Edit mode
longClickOnMessage()
clickOn(R.string.edit)
// TODO Cancel action
writeTo(R.id.composerEditText, "Hello universe!")
clickOn(R.id.sendButton)
// Open edit history
longClickOnMessage("Hello universe! (edited)")
clickOn(R.string.message_view_edit_history)
pressBack()
}
private fun longClickOnMessage(text: String = "Hello world!") {
onView(withId(R.id.timelineRecyclerView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(text)),
longClick()
)
)
}
private fun navigateToRoomSettings() {
clickOn(R.id.roomToolbarTitleView)
assertDisplayed(R.id.roomProfileAvatarView)
// Room settings
clickListItem(R.id.matrixProfileRecyclerView, 3)
pressBack()
// Notifications
clickListItem(R.id.matrixProfileRecyclerView, 5)
pressBack()
assertDisplayed(R.id.roomProfileAvatarView)
// People
clickListItem(R.id.matrixProfileRecyclerView, 7)
assertDisplayed(R.id.inviteUsersButton)
navigateToRoomPeople()
// Fab
navigateToInvite()
pressBack()
pressBack()
assertDisplayed(R.id.roomProfileAvatarView)
// Uploads
clickListItem(R.id.matrixProfileRecyclerView, 9)
// File tab
clickOn(R.string.uploads_files_title)
pressBack()
assertDisplayed(R.id.roomProfileAvatarView)
// Leave
clickListItem(R.id.matrixProfileRecyclerView, 13)
clickDialogNegativeButton()
// Menu share
// clickMenu(R.id.roomProfileShareAction)
// pressBack()
pressBack()
}
private fun navigateToInvite() {
assertDisplayed(R.id.inviteUsersButton)
clickOn(R.id.inviteUsersButton)
closeSoftKeyboard()
pressBack()
}
private fun navigateToRoomPeople() {
// Open first user
clickListItem(R.id.roomSettingsRecyclerView, 1)
assertDisplayed(R.id.memberProfilePowerLevelView)
// Verification
clickListItem(R.id.matrixProfileRecyclerView, 1)
clickBack()
// Role
clickListItem(R.id.matrixProfileRecyclerView, 3)
clickDialogNegativeButton()
clickBack()
}
private fun createDm() {
clickOn(R.id.createChatRoomButton)
withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) {
assertDisplayed(R.id.addByMatrixId)
}
closeSoftKeyboard()
pressBack()
pressBack()
}
private fun navigateToSettings() {
clickOn(R.id.groupToolbarAvatarImageView)
clickOn(R.id.homeDrawerHeaderSettingsView)
clickOn(R.string.settings_general_title)
navigateToSettingsGeneral()
pressBack()
clickOn(R.string.settings_notifications)
navigateToSettingsNotifications()
pressBack()
clickOn(R.string.settings_preferences)
navigateToSettingsPreferences()
pressBack()
clickOn(R.string.preference_voice_and_video)
pressBack()
clickOn(R.string.settings_ignored_users)
pressBack()
clickOn(R.string.settings_security_and_privacy)
navigateToSettingsSecurity()
pressBack()
clickOn(R.string.room_settings_labs_pref_title)
pressBack()
clickOn(R.string.settings_advanced_settings)
navigateToSettingsAdvanced()
pressBack()
clickOn(R.string.preference_root_help_about)
navigateToSettingsHelp()
pressBack()
pressBack()
}
private fun navigateToSettingsHelp() {
/*
clickOn(R.string.settings_app_info_link_title)
Cannot go back...
pressBack()
clickOn(R.string.settings_copyright)
pressBack()
clickOn(R.string.settings_app_term_conditions)
pressBack()
clickOn(R.string.settings_privacy_policy)
pressBack()
*/
clickOn(R.string.settings_third_party_notices)
clickDialogPositiveButton()
}
private fun navigateToSettingsAdvanced() {
clickOnPreference(R.string.settings_notifications_targets)
pressBack()
clickOnPreference(R.string.settings_push_rules)
pressBack()
/* TODO P2 test developer screens
// Enable developer mode
clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
clickOnPreference(R.string.settings_account_data)
clickOn("m.push_rules")
pressBack()
pressBack()
clickOnPreference(R.string.settings_key_requests)
pressBack()
// Disable developer mode
clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
*/
}
private fun navigateToSettingsSecurity() {
clickOnPreference(R.string.settings_active_sessions_show_all)
pressBack()
clickOnPreference(R.string.encryption_message_recovery)
// TODO go deeper here
pressBack()
/* Cannot exit
clickOnPreference(R.string.encryption_export_e2e_room_keys)
pressBack()
*/
}
private fun navigateToSettingsPreferences() {
clickOn(R.string.settings_interface_language)
onView(isRoot())
.perform(waitForView(withText("Dansk (Danmark)")))
pressBack()
clickOn(R.string.settings_theme)
clickDialogNegativeButton()
clickOn(R.string.font_size)
clickDialogNegativeButton()
}
private fun navigateToSettingsNotifications() {
clickOn(R.string.settings_notification_advanced)
pressBack()
/*
clickOn(R.string.settings_noisy_notifications_preferences)
TODO Cannot go back
pressBack()
clickOn(R.string.settings_silent_notifications_preferences)
pressBack()
clickOn(R.string.settings_call_notifications_preferences)
pressBack()
*/
clickOnPreference(R.string.settings_notification_troubleshoot)
pressBack()
}
private fun navigateToSettingsGeneral() {
clickOn(R.string.settings_profile_picture)
clickDialogPositiveButton()
clickOn(R.string.settings_display_name)
clickDialogNegativeButton()
clickOn(R.string.settings_password)
clickDialogNegativeButton()
clickOn(R.string.settings_emails_and_phone_numbers_title)
pressBack()
clickOn(R.string.settings_discovery_manage)
clickOn(R.string.add_identity_server)
pressBack()
pressBack()
// Identity server
clickOnPreference(R.string.settings_identity_server)
pressBack()
// Deactivate account
clickOnPreference(R.string.settings_deactivate_my_account)
pressBack()
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.ui
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertDisabled
import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertEnabled
import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn
import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo
import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.features.home.HomeActivity
import im.vector.app.waitForView
class UiTestBase {
fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(true, userId, password, homeServerUrl)
}
fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(false, userId, password, homeServerUrl)
}
private fun initSession(createAccount: Boolean,
userId: String,
password: String,
homeServerUrl: String) {
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit)
clickOn(R.id.loginSplashSubmit)
assertDisplayed(R.id.loginServerTitle, R.string.login_server_title)
// Chose custom server
clickOn(R.id.loginServerChoiceOther)
// Enter local synapse
writeTo(R.id.loginServerUrlFormHomeServerUrl, homeServerUrl)
assertEnabled(R.id.loginServerUrlFormSubmit)
closeSoftKeyboard()
clickOn(R.id.loginServerUrlFormSubmit)
onView(isRoot()).perform(waitForView(withId(R.id.loginSignupSigninSubmit)))
if (createAccount) {
// Click on the signup button
assertDisplayed(R.id.loginSignupSigninSubmit)
clickOn(R.id.loginSignupSigninSubmit)
} else {
// Click on the signin button
assertDisplayed(R.id.loginSignupSigninSignIn)
clickOn(R.id.loginSignupSigninSignIn)
}
// Ensure password flow supported
assertDisplayed(R.id.loginField)
assertDisplayed(R.id.passwordField)
writeTo(R.id.loginField, userId)
assertDisabled(R.id.loginSubmit)
writeTo(R.id.passwordField, password)
assertEnabled(R.id.loginSubmit)
closeSoftKeyboard()
clickOn(R.id.loginSubmit)
// Wait
waitUntilActivityVisible<HomeActivity> {
assertDisplayed(R.id.homeDetailFragmentContainer)
}
}
fun signout() {
clickOn(R.id.groupToolbarAvatarImageView)
clickOn(R.id.homeDrawerHeaderSignoutView)
}
}

View File

@ -30,12 +30,12 @@ class DebugSasEmojiActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_generic_recycler)
val controller = SasEmojiController()
recyclerView.configureWith(controller)
genericRecyclerView.configureWith(controller)
controller.setData(SasState(getAllVerificationEmojis()))
}
override fun onDestroy() {
recyclerView.cleanup()
genericRecyclerView.cleanup()
super.onDestroy()
}
}

View File

@ -97,12 +97,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog()
viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector())
super.onAttach(context)
}
protected open fun injectWith(injector: ScreenComponent) = Unit
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -137,7 +137,7 @@ class VectorCallViewModel @AssistedInject constructor(
session.callSignalingService().getCallWithId(it)?.let { mxCall ->
this.call = mxCall
mxCall.otherUserId
val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem()
val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()
mxCall.addListener(callStateListener)

View File

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

View File

@ -46,7 +46,7 @@ class JitsiCallViewModel @AssistedInject constructor(
}
init {
val me = session.getUser(session.myUserId)?.toMatrixItem()
val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem()
val userInfo = JitsiMeetUserInfo().apply {
displayName = me?.getBestName()
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }

View File

@ -28,7 +28,7 @@ import im.vector.app.features.settings.VectorSettingsUrls
// Increase this value to show again the disclaimer dialog after an upgrade of the application
private const val CURRENT_DISCLAIMER_VALUE = 2
private const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE"
const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE"
fun showDisclaimerDialog(activity: Activity) {
val sharedPrefs = DefaultSharedPreferences.getInstance(activity)

View File

@ -55,7 +55,7 @@ class DiscoverySettingsFragment @Inject constructor(
sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java)
controller.listener = this
recyclerView.configureWith(controller)
genericRecyclerView.configureWith(controller)
sharedViewModel.navigateEvent.observeEvent(this) {
when (it) {
@ -74,7 +74,7 @@ class DiscoverySettingsFragment @Inject constructor(
}
override fun onDestroyView() {
recyclerView.cleanup()
genericRecyclerView.cleanup()
controller.listener = null
super.onDestroyView()
}

View File

@ -17,6 +17,7 @@
package im.vector.app.features.grouplist
import androidx.lifecycle.viewModelScope
import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
@ -28,7 +29,7 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import org.matrix.android.sdk.api.NoOpMatrixCallback
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams
@ -95,7 +96,9 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
// We take care of refreshing group data when selecting to be sure we get all the rooms and users
session.getGroup(action.groupSummary.groupId)?.fetchGroupData(NoOpMatrixCallback())
viewModelScope.launch {
session.getGroup(action.groupSummary.groupId)?.fetchGroupData()
}
setState { copy(selectedGroup = action.groupSummary) }
}
}

View File

@ -22,6 +22,6 @@ import org.matrix.android.sdk.api.util.MatrixItem
sealed class HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents()
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents()
object PromptToEnableSessionPush : HomeActivityViewEvents()
}

View File

@ -78,29 +78,30 @@ class HomeActivityViewModel @AssistedInject constructor(
}
private fun observeCrossSigningReset() {
val safeActiveSession = activeSessionHolder.getSafeActiveSession()
val crossSigningService = safeActiveSession
?.cryptoService()
?.crossSigningService()
onceTrusted = crossSigningService
?.allPrivateKeysKnown() ?: false
val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return
onceTrusted = safeActiveSession
.cryptoService()
.crossSigningService().allPrivateKeysKnown()
safeActiveSession
?.rx()
?.liveCrossSigningInfo(safeActiveSession.myUserId)
?.subscribe {
.rx()
.liveCrossSigningInfo(safeActiveSession.myUserId)
.subscribe {
val isVerified = it.getOrNull()?.isTrusted() ?: false
if (!isVerified && onceTrusted) {
// cross signing keys have been reset
// Tigger a popup to re-verify
_viewEvents.post(
HomeActivityViewEvents.OnCrossSignedInvalidated(
safeActiveSession.getUser(safeActiveSession.myUserId)?.toMatrixItem()
)
)
// Note: user can be null in case of logout
safeActiveSession.getUser(safeActiveSession.myUserId)
?.toMatrixItem()
?.let { user ->
_viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user))
}
}
onceTrusted = isVerified
}?.disposeOnClear()
}
.disposeOnClear()
}
private fun observeInitialSync() {

View File

@ -16,6 +16,8 @@
package im.vector.app.features.home.room.detail
import android.net.Uri
import android.view.View
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
@ -24,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem
sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
object QuickActionInvitePeople : RoomDetailAction()
object QuickActionSetAvatar : RoomDetailAction()
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
object QuickActionSetTopic : RoomDetailAction()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
}

View File

@ -71,6 +71,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
@ -82,6 +83,7 @@ import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.extensions.trackItemsVisibilityChange
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
@ -141,6 +143,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsB
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillImageSpan
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
@ -149,6 +152,7 @@ import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.reactions.EmojiReactionPickerActivity
import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData
@ -196,6 +200,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import timber.log.Timber
import java.io.File
import java.net.URL
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -221,7 +226,8 @@ class RoomDetailFragment @Inject constructor(
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory
) :
VectorBaseFragment(),
TimelineEventController.Callback,
@ -229,7 +235,7 @@ class RoomDetailFragment @Inject constructor(
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback,
// RoomWidgetsBannerView.Callback,
GalleryOrCameraDialogHelper.Listener,
ActiveCallView.Callback {
companion object {
@ -250,10 +256,15 @@ class RoomDetailFragment @Inject constructor(
private const val ircPattern = " (IRC)"
}
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
private val roomDetailArgs: RoomDetailArgs by args()
private val glideRequests by lazy {
GlideApp.with(this)
}
private val pillsPostProcessor by lazy {
pillsPostProcessorFactory.create(roomDetailArgs.roomId)
}
private val autoCompleter: AutoCompleter by lazy {
autoCompleterFactory.create(roomDetailArgs.roomId)
@ -364,6 +375,12 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId)
RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings()
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item)
}
}.exhaustive
}
@ -372,6 +389,24 @@ class RoomDetailFragment @Inject constructor(
}
}
override fun onImageReady(uri: Uri?) {
uri ?: return
roomDetailViewModel.handle(
RoomDetailAction.SetAvatarAction(
newAvatarUri = uri,
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()
)
)
}
private fun handleOpenRoomSettings() {
navigator.openRoomProfile(
requireContext(),
roomDetailArgs.roomId,
RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS
)
}
private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
navigator.openRoom(requireContext(), openRoom.roomId, null)
}
@ -508,7 +543,7 @@ class RoomDetailFragment @Inject constructor(
modelBuildListener = null
autoCompleter.clear()
debouncer.cancelAll()
recyclerView.cleanup()
timelineRecyclerView.cleanup()
super.onDestroyView()
}
@ -535,7 +570,7 @@ class RoomDetailFragment @Inject constructor(
jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager(
jumpToBottomView,
debouncer,
recyclerView,
timelineRecyclerView,
layoutManager
)
}
@ -558,7 +593,7 @@ class RoomDetailFragment @Inject constructor(
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(action.eventId)
} else {
recyclerView.stopScroll()
timelineRecyclerView.stopScroll()
layoutManager.scrollToPosition(scrollPosition)
}
}
@ -848,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
}
composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
@ -969,14 +1004,14 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline
recyclerView.trackItemsVisibilityChange()
timelineRecyclerView.trackItemsVisibilityChange()
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(recyclerView, layoutManager, timelineEventController)
recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null
recyclerView.setHasFixedSize(true)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(timelineRecyclerView, layoutManager, timelineEventController)
timelineRecyclerView.layoutManager = layoutManager
timelineRecyclerView.itemAnimator = null
timelineRecyclerView.setHasFixedSize(true)
modelBuildListener = OnModelBuildFinishedListener {
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
@ -985,7 +1020,7 @@ class RoomDetailFragment @Inject constructor(
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
}
timelineEventController.addModelBuildListener(modelBuildListener)
recyclerView.adapter = timelineEventController.adapter
timelineRecyclerView.adapter = timelineEventController.adapter
if (vectorPreferences.swipeToReplyIsEnabled()) {
val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
@ -1015,9 +1050,9 @@ class RoomDetailFragment @Inject constructor(
}
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler)
val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView)
touchHelper.attachToRecyclerView(timelineRecyclerView)
}
recyclerView.addGlidePreloader(
timelineRecyclerView.addGlidePreloader(
epoxyController = timelineEventController,
requestManager = GlideApp.with(this),
preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ ->

View File

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

View File

@ -50,6 +50,7 @@ import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser
@ -276,9 +277,39 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
_viewEvents.post(
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
)
}
}.exhaustive
}
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback<Unit> {
room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it)
}
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
}
}
private fun handleInvitePeople() {
_viewEvents.post(RoomDetailViewEvents.OpenInvitePeople)
}
private fun handleQuickSetAvatar() {
_viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog)
}
private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId)
if (existingDmRoomId == null) {
@ -476,22 +507,24 @@ class RoomDetailViewModel @AssistedInject constructor(
* Convert a send mode to a draft and save the draft
*/
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState {
when {
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.REPLY -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.QUOTE -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.EDIT -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
viewModelScope.launch(NonCancellable) {
when {
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
room.saveDraft(UserDraft.REGULAR(action.draft))
}
it.sendMode is SendMode.REPLY -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
it.sendMode is SendMode.QUOTE -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
it.sendMode is SendMode.EDIT -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
}
}
}
@ -778,7 +811,9 @@ class RoomDetailViewModel @AssistedInject constructor(
} else {
// Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.REGULAR("", false)) }
room.deleteDraft(NoOpMatrixCallback())
viewModelScope.launch {
room.deleteDraft()
}
}
}
@ -1302,7 +1337,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
if (summary.membership == Membership.INVITE) {
summary.inviterId?.let { inviterId ->
session.getUser(inviterId)
session.getRoomMember(inviterId, summary.roomId)
}?.also {
setState { copy(asyncInviter = Success(it)) }
}

View File

@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.session.widgets.model.Widget
/**
@ -60,7 +59,7 @@ data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
val asyncInviter: Async<User> = Uninitialized,
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
val typingMessage: String? = null,

View File

@ -123,7 +123,7 @@ class SearchResultController @Inject constructor(
.formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
.spannable(spannable)
.sender(eventAndSender.sender
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem())
.listener { listener?.onItemClicked(eventAndSender.event) }
.let { result.add(it) }
}

View File

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

View File

@ -31,10 +31,14 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId
?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) }
?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel<PowerLevelsContent>() }
?.let { PowerLevelsHelper(it) }
val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
val attributes = MergedRoomCreationItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback,
currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
callback = callback,
currentUserId = currentUserId,
roomSummary = roomSummaryHolder.roomSummary,
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
)
MergedRoomCreationItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted)
.attributes(attributes)
.movementMethod(createLinkMovementMethod(callback))
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}

View File

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

View File

@ -16,11 +16,14 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.text.SpannableString
import android.text.method.MovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -28,8 +31,16 @@ import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.core.utils.tappableMatchingText
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
@ -37,11 +48,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
@EpoxyAttribute
override lateinit var attributes: Attributes
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var movementMethod: MovementMethod? = null
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
bindCreationSummaryTile(holder)
if (attributes.isCollapsed) {
// Take the oldest data
val data = distinctMergeData.lastOrNull()
@ -70,34 +86,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.avatarView.visibility = View.GONE
}
if (attributes.hasEncryptionEvent) {
holder.encryptionTile.isVisible = true
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
if (attributes.isEncryptionAlgorithmSecure) {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
} else {
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
}
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
null, null, null
)
} else {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
null, null, null
)
}
} else {
holder.encryptionTile.isVisible = false
}
bindEncryptionTile(holder, data)
} else {
holder.avatarView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
@ -107,6 +96,109 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.readReceiptsView.isVisible = false
}
private fun bindEncryptionTile(holder: Holder, data: Data?) {
if (attributes.hasEncryptionEvent) {
holder.encryptionTile.isVisible = true
holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
if (attributes.isEncryptionAlgorithmSecure) {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
} else {
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
}
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
null, null, null
)
} else {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
null, null, null
)
}
} else {
holder.encryptionTile.isVisible = false
}
}
private fun bindCreationSummaryTile(holder: Holder) {
val roomSummary = attributes.roomSummary
val roomDisplayName = roomSummary?.displayName
holder.roomNameText.setTextOrHide(roomDisplayName)
val isDirect = roomSummary?.isDirect == true
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
if (isDirect) {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName ?: "")
} else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
} else {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
}
val topic = roomSummary?.topic
if (topic.isNullOrBlank()) {
// do not show hint for DMs or group DMs
if (!isDirect) {
val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
override fun onClick(widget: View) {
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
}
}))
}
} else {
holder.roomTopicText.setTextOrHide(
span {
span(holder.view.resources.getString(R.string.topic_prefix)) {
textStyle = "bold"
}
+topic.linkify(attributes.callback)
}
)
}
holder.roomTopicText.movementMethod = movementMethod
val roomItem = roomSummary?.toMatrixItem()
val shouldSetAvatar = attributes.canChangeAvatar
&& (roomSummary?.isDirect == false || (isDirect && membersCount >= 2))
&& roomItem?.avatarUrl.isNullOrBlank()
holder.roomAvatarImageView.isVisible = roomItem != null
if (roomItem != null) {
attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView)
holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view ->
if (shouldSetAvatar) {
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
} else {
// Note: this is no op if there is no avatar on the room
attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view))
}
}))
}
holder.setAvatarButton.isVisible = shouldSetAvatar
if (shouldSetAvatar) {
holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
}))
}
holder.addPeopleButton.isVisible = !isDirect
if (!isDirect) {
holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople)
}))
}
}
class Holder : BasedMergedItem.Holder(STUB_ID) {
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
@ -114,6 +206,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
val roomNameText by bind<TextView>(R.id.roomNameTileText)
val roomDescriptionText by bind<TextView>(R.id.roomNameDescriptionText)
val roomTopicText by bind<TextView>(R.id.roomNameTopicText)
val roomAvatarImageView by bind<ImageView>(R.id.creationTileRoomAvatarImageView)
val addPeopleButton by bind<View>(R.id.creationTileAddPeopleButton)
val setAvatarButton by bind<View>(R.id.creationTileSetAvatarButton)
}
companion object {
@ -126,8 +225,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit,
val callback: TimelineEventController.Callback? = null,
val currentUserId: String,
val hasEncryptionEvent: Boolean,
val isEncryptionAlgorithmSecure: Boolean
val isEncryptionAlgorithmSecure: Boolean,
val roomSummary: RoomSummary?,
val canChangeAvatar: Boolean = false,
val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false
) : BasedMergedItem.Attributes
}

View File

@ -17,21 +17,23 @@
package im.vector.app.features.html
import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import android.text.Spannable
import androidx.core.text.toSpannable
import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import org.commonmark.node.Node
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EventHtmlRenderer @Inject constructor(context: Context,
htmlConfigure: MatrixHtmlPluginConfigure) {
class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfigure,
context: Context) {
interface PostProcessor {
fun afterRender(renderedText: Spannable)
}
private val markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create(htmlConfigure))
@ -41,35 +43,47 @@ class EventHtmlRenderer @Inject constructor(context: Context,
return markwon.parse(text)
}
fun render(text: String): CharSequence {
/**
* @param text the text you want to render
* @param postProcessors an optional array of post processor to add any span if needed
*/
fun render(text: String, vararg postProcessors: PostProcessor): CharSequence {
return try {
markwon.toMarkdown(text)
val parsed = markwon.parse(text)
renderAndProcess(parsed, postProcessors)
} catch (failure: Throwable) {
Timber.v("Fail to render $text to html")
text
}
}
fun render(node: Node): CharSequence? {
/**
* @param node the node you want to render
* @param postProcessors an optional array of post processor to add any span if needed
*/
fun render(node: Node, vararg postProcessors: PostProcessor): CharSequence? {
return try {
markwon.render(node)
renderAndProcess(node, postProcessors)
} catch (failure: Throwable) {
Timber.v("Fail to render $node to html")
return null
}
}
private fun renderAndProcess(node: Node, postProcessors: Array<out PostProcessor>): CharSequence {
val renderedText = markwon.render(node).toSpannable()
postProcessors.forEach {
it.afterRender(renderedText)
}
return renderedText
}
}
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
private val colorProvider: ColorProvider,
private val avatarRenderer: AvatarRenderer,
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure {
override fun configureHtml(plugin: HtmlPlugin) {
plugin
.addHandler(TagHandlerNoOp.create("a"))
.addHandler(FontTagHandler())
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
.addHandler(MxReplyTagHandler())
.addHandler(SpanHandler(colorProvider))
}

View File

@ -1,89 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.html
import android.content.Context
import android.text.style.URLSpan
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.tag.LinkHandler
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
class MxLinkTagHandler(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) : LinkHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val link = tag.attributes()["href"]
if (link != null) {
val permalinkData = PermalinkParser.parse(link)
val matrixItem = when (permalinkData) {
is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl)
}
is PermalinkData.RoomLink -> {
if (permalinkData.eventId == null) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
if (permalinkData.isRoomAlias) {
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
} else {
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
}
} else {
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
null
}
}
is PermalinkData.GroupLink -> {
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
}
else -> null
}
if (matrixItem == null) {
super.handle(visitor, renderer, tag)
} else {
val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem)
SpannableBuilder.setSpans(
visitor.builder(),
span,
tag.start(),
tag.end()
)
SpannableBuilder.setSpans(
visitor.builder(),
URLSpan(link),
tag.start(),
tag.end()
)
}
} else {
super.handle(visitor, renderer, tag)
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.html
import android.content.Context
import android.text.Spannable
import android.text.Spanned
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.core.spans.LinkSpan
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomId: String?,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder)
: EventHtmlRenderer.PostProcessor {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String?): PillsPostProcessor
}
override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId)
}
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
// We let markdown handle links and then we add PillImageSpan if needed.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan)
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
val permalinkData = PermalinkParser.parse(url)
val matrixItem = when (permalinkData) {
is PermalinkData.UserLink -> {
if (roomId == null) {
sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem()
} else {
sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem()
}
}
is PermalinkData.RoomLink -> {
if (permalinkData.eventId == null) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
if (permalinkData.isRoomAlias) {
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
} else {
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
}
} else {
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
null
}
}
is PermalinkData.GroupLink -> {
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
}
else -> null
} ?: return null
return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
}
}

View File

@ -27,7 +27,7 @@ import im.vector.app.core.platform.ButtonStateView
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.vector_invite_view.view.*
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -73,7 +73,7 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
}
}
fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
fun render(sender: RoomMemberSummary, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
if (mode == Mode.LARGE) {
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)

View File

@ -0,0 +1,53 @@
/*
* 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.media
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class AttachmentProviderFactory @Inject constructor(
private val imageContentRenderer: ImageContentRenderer,
private val vectorDateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val session: Session
) {
fun createProvider(attachments: List<TimelineEvent>): RoomEventsAttachmentProvider {
return RoomEventsAttachmentProvider(
attachments,
imageContentRenderer,
vectorDateFormatter,
session.fileService(),
stringProvider
)
}
fun createProvider(attachments: List<AttachmentData>, room: Room?): DataAttachmentRoomProvider {
return DataAttachmentRoomProvider(
attachments,
room,
imageContentRenderer,
vectorDateFormatter,
session.fileService(),
stringProvider
)
}
}

View File

@ -20,17 +20,30 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.core.view.isVisible
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.transition.Transition
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.lib.attachmentviewer.AttachmentInfo
import im.vector.lib.attachmentviewer.AttachmentSourceProvider
import im.vector.lib.attachmentviewer.ImageLoaderTarget
import im.vector.lib.attachmentviewer.VideoLoaderTarget
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import java.io.File
abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider {
abstract class BaseAttachmentProvider<Type>(
private val attachments: List<Type>,
private val imageContentRenderer: ImageContentRenderer,
protected val fileService: FileService,
private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider
) : AttachmentSourceProvider {
interface InteractionListener {
fun onDismissTapped()
@ -41,9 +54,13 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend
var interactionListener: InteractionListener? = null
protected var overlayView: AttachmentOverlayView? = null
private var overlayView: AttachmentOverlayView? = null
override fun overlayViewAtPosition(context: Context, position: Int): View? {
final override fun getItemCount() = attachments.size
protected fun getItem(position: Int) = attachments[position]
final override fun overlayViewAtPosition(context: Context, position: Int): View? {
if (position == -1) return null
if (overlayView == null) {
overlayView = AttachmentOverlayView(context)
@ -60,9 +77,24 @@ abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRend
interactionListener?.videoSeekTo(percent)
}
}
val timelineEvent = getTimelineEventAtPosition(position)
if (timelineEvent != null) {
val dateString = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
overlayView?.updateWith(
counter = stringProvider.getString(R.string.attachment_viewer_item_x_of_y, position + 1, getItemCount()),
senderInfo = "${timelineEvent.senderInfo.displayName} $dateString"
)
overlayView?.videoControlsGroup?.isVisible = timelineEvent.root.isVideoMessage()
} else {
overlayView?.updateWith("", "")
}
return overlayView
}
abstract fun getTimelineEventAtPosition(position: Int): TimelineEvent?
override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) {
(info.data as? ImageContentRenderer.Data)?.let {
imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget<ImageView, Drawable>(target.contextView()) {

View File

@ -16,30 +16,26 @@
package im.vector.app.features.media
import android.content.Context
import android.view.View
import androidx.core.view.isVisible
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.lib.attachmentviewer.AttachmentInfo
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import java.io.File
class DataAttachmentRoomProvider(
private val attachments: List<AttachmentData>,
attachments: List<AttachmentData>,
private val room: Room?,
private val initialIndex: Int,
imageContentRenderer: ImageContentRenderer,
private val dateFormatter: VectorDateFormatter,
fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) {
override fun getItemCount(): Int = attachments.size
dateFormatter: VectorDateFormatter,
fileService: FileService,
stringProvider: StringProvider
) : BaseAttachmentProvider<AttachmentData>(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) {
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return attachments[position].let {
return getItem(position).let {
when (it) {
is ImageContentRenderer.Data -> {
if (it.mimeType == "image/gif") {
@ -73,22 +69,13 @@ class DataAttachmentRoomProvider(
}
}
override fun overlayViewAtPosition(context: Context, position: Int): View? {
super.overlayViewAtPosition(context, position)
val item = attachments[position]
val timeLineEvent = room?.getTimeLineEvent(item.eventId)
if (timeLineEvent != null) {
val dateString = dateFormatter.format(timeLineEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()
} else {
overlayView?.updateWith("", "")
}
return overlayView
override fun getTimelineEventAtPosition(position: Int): TimelineEvent? {
val item = getItem(position)
return room?.getTimeLineEvent(item.eventId)
}
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
val item = attachments[position]
val item = getItem(position)
fileService.downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = item.eventId,

View File

@ -21,6 +21,7 @@ import android.net.Uri
import android.os.Parcelable
import android.view.View
import android.widget.ImageView
import androidx.core.view.updateLayoutParams
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ -96,15 +97,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
fun render(data: Data, mode: Mode, imageView: ImageView) {
val size = processSize(data, mode)
imageView.layoutParams.width = size.width
imageView.layoutParams.height = size.height
imageView.updateLayoutParams {
width = size.width
height = size.height
}
// a11y
imageView.contentDescription = data.filename
createGlideRequest(data, mode, imageView, size)
.dontAnimate()
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
.thumbnail(0.3f)
// .thumbnail(0.3f)
.into(imageView)
}
@ -117,6 +120,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
}
}
/**
* Used by Attachment Viewer
*/
fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
val req = if (data.elementToDecrypt != null) {
// Encrypted image

View File

@ -16,18 +16,12 @@
package im.vector.app.features.media
import android.content.Context
import android.view.View
import androidx.core.view.isVisible
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.lib.attachmentviewer.AttachmentInfo
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@ -36,22 +30,17 @@ import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.File
import javax.inject.Inject
class RoomEventsAttachmentProvider(
private val attachments: List<TimelineEvent>,
private val initialIndex: Int,
attachments: List<TimelineEvent>,
imageContentRenderer: ImageContentRenderer,
private val dateFormatter: VectorDateFormatter,
fileService: FileService
) : BaseAttachmentProvider(imageContentRenderer, fileService) {
override fun getItemCount(): Int {
return attachments.size
}
dateFormatter: VectorDateFormatter,
fileService: FileService,
stringProvider: StringProvider
) : BaseAttachmentProvider<TimelineEvent>(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) {
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return attachments[position].let {
return getItem(position).let {
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
if (content is MessageImageContent) {
val data = ImageContentRenderer.Data(
@ -125,17 +114,12 @@ class RoomEventsAttachmentProvider(
}
}
override fun overlayViewAtPosition(context: Context, position: Int): View? {
super.overlayViewAtPosition(context, position)
val item = attachments[position]
val dateString = dateFormatter.format(item.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
return overlayView
override fun getTimelineEventAtPosition(position: Int): TimelineEvent? {
return getItem(position)
}
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
attachments[position].let { timelineEvent ->
getItem(position).let { timelineEvent ->
val messageContent = timelineEvent.root.getClearContent().toModel<MessageContent>()
as? MessageWithAttachmentContent
@ -160,18 +144,3 @@ class RoomEventsAttachmentProvider(
}
}
}
class AttachmentProviderFactory @Inject constructor(
private val imageContentRenderer: ImageContentRenderer,
private val vectorDateFormatter: VectorDateFormatter,
private val session: Session
) {
fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomEventsAttachmentProvider {
return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
}
fun createProvider(attachments: List<AttachmentData>, room: Room?, initialIndex: Int): DataAttachmentRoomProvider {
return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
}
}

View File

@ -70,7 +70,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
private var initialIndex = 0
private var isAnimatingOut = false
var currentSourceProvider: BaseAttachmentProvider? = null
private var currentSourceProvider: BaseAttachmentProvider<*>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -117,36 +117,22 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
val room = args.roomId?.let { session.getRoom(it) }
val inMemoryData = intent.getParcelableArrayListExtra<AttachmentData>(EXTRA_IN_MEMORY_DATA)
if (inMemoryData != null) {
val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex)
val index = inMemoryData.indexOfFirst { it.eventId == args.eventId }
initialIndex = index
sourceProvider.interactionListener = this
setSourceProvider(sourceProvider)
this.currentSourceProvider = sourceProvider
if (savedInstanceState == null) {
pager2.setCurrentItem(index, false)
// The page change listener is not notified of the change...
pager2.post {
onSelectedPositionChanged(index)
}
}
val sourceProvider = if (inMemoryData != null) {
initialIndex = inMemoryData.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0)
dataSourceFactory.createProvider(inMemoryData, room)
} else {
val events = room?.getAttachmentMessages()
?: emptyList()
val index = events.indexOfFirst { it.eventId == args.eventId }
initialIndex = index
val sourceProvider = dataSourceFactory.createProvider(events, index)
sourceProvider.interactionListener = this
setSourceProvider(sourceProvider)
this.currentSourceProvider = sourceProvider
if (savedInstanceState == null) {
pager2.setCurrentItem(index, false)
// The page change listener is not notified of the change...
pager2.post {
onSelectedPositionChanged(index)
}
val events = room?.getAttachmentMessages().orEmpty()
initialIndex = events.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0)
dataSourceFactory.createProvider(events)
}
sourceProvider.interactionListener = this
setSourceProvider(sourceProvider)
currentSourceProvider = sourceProvider
if (savedInstanceState == null) {
pager2.setCurrentItem(initialIndex, false)
// The page change listener is not notified of the change...
pager2.post {
onSelectedPositionChanged(initialIndex)
}
}
@ -278,7 +264,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
}
override fun onShareTapped() {
this.currentSourceProvider?.getFileForSharing(currentPosition) { data ->
currentSourceProvider?.getFileForSharing(currentPosition) { data ->
if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri()))
}

View File

@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor(
context.startActivity(KeysBackupManageActivity.intent(context))
}
override fun openRoomProfile(context: Context, roomId: String) {
context.startActivity(RoomProfileActivity.newIntent(context, roomId))
override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) {
context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
}
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {

View File

@ -78,7 +78,7 @@ interface Navigator {
fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false)
fun openRoomProfile(context: Context, roomId: String)
fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null)
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)

View File

@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
val content = event.content?.toModel<RoomMemberContent>() ?: return null
val roomId = event.roomId ?: return null
val dName = event.senderId?.let { session.getUser(it)?.displayName }
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
if (Membership.INVITE == content.membership) {
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
?: stringProvider.getString(R.string.notification_new_invitation)

View File

@ -120,7 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
null,
false,
System.currentTimeMillis(),
session.getUser(session.myUserId)?.displayName
session.getRoomMember(session.myUserId, room.roomId)?.displayName
?: context?.getString(R.string.notification_sender_me),
session.myUserId,
message,

View File

@ -46,7 +46,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
private var weakCurrentActivity: WeakReference<Activity>? = null
private var currentAlerter: VectorAlert? = null
private val alertFiFo = ArrayList<VectorAlert>()
private val alertFiFo = mutableListOf<VectorAlert>()
fun postVectorAlert(alert: VectorAlert) {
synchronized(alertFiFo) {

View File

@ -41,12 +41,12 @@ class EmojiSearchResultFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
epoxyController.listener = this
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
epoxyController.listener = null
recyclerView.cleanup()
genericRecyclerView.cleanup()
super.onDestroyView()
}

View File

@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
sealed class RoomProfileAction : VectorViewModelAction {
object EnableEncryption : RoomProfileAction()
object LeaveRoom : RoomProfileAction()
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
object ShareRoomProfile : RoomProfileAction()

View File

@ -46,10 +46,16 @@ class RoomProfileActivity :
companion object {
fun newIntent(context: Context, roomId: String): Intent {
private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
const val EXTRA_DIRECT_ACCESS_ROOM_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ROOM_SETTINGS = 1
fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent {
val roomProfileArgs = RoomProfileArgs(roomId)
return Intent(context, RoomProfileActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, roomProfileArgs)
putExtra(EXTRA_DIRECT_ACCESS, directAccess)
}
}
}
@ -80,7 +86,13 @@ class RoomProfileActivity :
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
if (isFirstCreation()) {
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) {
EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> {
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs)
}
else -> addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
}
}
sharedActionViewModel
.observe()

View File

@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class RoomProfileController @Inject constructor(
@ -43,6 +44,7 @@ class RoomProfileController @Inject constructor(
interface Callback {
fun onLearnMoreClicked()
fun onEnableEncryptionClicked()
fun onMemberListClicked()
fun onBannedMemberListClicked()
fun onNotificationsClicked()
@ -84,6 +86,7 @@ class RoomProfileController @Inject constructor(
centered(false)
text(stringProvider.getString(learnMoreSubtitle))
}
buildEncryptionAction(data.actionPermissions, roomSummary)
// More
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
@ -171,4 +174,29 @@ class RoomProfileController @Inject constructor(
)
}
}
private fun buildEncryptionAction(actionPermissions: RoomProfileViewState.ActionPermissions, roomSummary: RoomSummary) {
if (!roomSummary.isEncrypted) {
if (actionPermissions.canEnableEncryption) {
buildProfileAction(
id = "enableEncryption",
title = stringProvider.getString(R.string.room_settings_enable_encryption),
dividerColor = dividerColor,
icon = R.drawable.ic_shield_black,
divider = false,
editable = false,
action = { callback?.onEnableEncryptionClicked() }
)
} else {
buildProfileAction(
id = "enableEncryption",
title = stringProvider.getString(R.string.room_settings_enable_encryption_no_permission),
dividerColor = dividerColor,
icon = R.drawable.ic_shield_black,
divider = false,
editable = false
)
}
}
}
}

View File

@ -49,6 +49,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.media.BigImageViewerActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.util.MatrixItem
@ -87,6 +88,7 @@ class RoomProfileFragment @Inject constructor(
it.layoutResource = R.layout.view_stub_room_profile_header
it.inflate()
}
setupWaitingView()
setupToolbar(matrixProfileToolbar)
setupRecyclerView()
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(
@ -111,6 +113,11 @@ class RoomProfileFragment @Inject constructor(
setupLongClicks()
}
private fun setupWaitingView() {
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
}
private fun setupLongClicks() {
roomProfileNameView.copyOnLongClick()
roomProfileAliasView.copyOnLongClick()
@ -155,6 +162,8 @@ class RoomProfileFragment @Inject constructor(
}
override fun invalidate() = withState(roomProfileViewModel) { state ->
waiting_view.isVisible = state.isLoading
state.roomSummary()?.also {
if (it.membership.isLeft()) {
Timber.w("The room has been left")
@ -187,6 +196,17 @@ class RoomProfileFragment @Inject constructor(
vectorBaseActivity.notImplemented()
}
override fun onEnableEncryptionClicked() {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.room_settings_enable_encryption_dialog_title)
.setMessage(R.string.room_settings_enable_encryption_dialog_content)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ ->
roomProfileViewModel.handle(RoomProfileAction.EnableEncryption)
}
.show()
}
override fun onMemberListClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers)
}

View File

@ -28,12 +28,15 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.rx.RxRoom
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
@ -65,6 +68,7 @@ class RoomProfileViewModel @AssistedInject constructor(
val rxRoom = room.rx()
observeRoomSummary(rxRoom)
observeBannedRoomMembers(rxRoom)
observePermissions()
}
private fun observeRoomSummary(rxRoom: RxRoom) {
@ -82,8 +86,22 @@ class RoomProfileViewModel @AssistedInject constructor(
}
}
private fun observePermissions() {
PowerLevelsObservableFactory(room)
.createObservable()
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
val permissions = RoomProfileViewState.ActionPermissions(
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
)
setState { copy(actionPermissions = permissions) }
}
.disposeOnClear()
}
override fun handle(action: RoomProfileAction) {
when (action) {
is RoomProfileAction.EnableEncryption -> handleEnableEncryption()
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
@ -91,6 +109,24 @@ class RoomProfileViewModel @AssistedInject constructor(
}.exhaustive
}
private fun handleEnableEncryption() {
postLoading(true)
viewModelScope.launch {
val result = runCatching { room.enableEncryption() }
postLoading(false)
result.onFailure { failure ->
_viewEvents.post(RoomProfileViewEvents.Failure(failure))
}
}
}
private fun postLoading(isLoading: Boolean) {
setState {
copy(isLoading = isLoading)
}
}
private fun handleCreateShortcut() {
viewModelScope.launch(Dispatchers.IO) {
withState { state ->

View File

@ -26,8 +26,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomProfileViewState(
val roomId: String,
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 {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
data class ActionPermissions(
val canEnableEncryption: Boolean = false
)
}

View File

@ -56,7 +56,7 @@ class RoomBannedMemberListFragment @Inject constructor(
roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar)
setupSearchView()
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.observeViewEvents {
when (it) {
@ -83,7 +83,7 @@ class RoomBannedMemberListFragment @Inject constructor(
}
override fun onDestroyView() {
recyclerView.cleanup()
roomSettingsRecyclerView.cleanup()
super.onDestroyView()
}

View File

@ -57,7 +57,7 @@ class RoomMemberListFragment @Inject constructor(
setupToolbar(roomSettingsToolbar)
setupSearchView()
setupInviteUsersButton()
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true)
}
private fun setupInviteUsersButton() {
@ -65,7 +65,7 @@ class RoomMemberListFragment @Inject constructor(
navigator.openInviteUsersToRoom(requireContext(), roomProfileArgs.roomId)
}
// Hide FAB when list is scrolling
recyclerView.addOnScrollListener(
roomSettingsRecyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
@ -99,7 +99,7 @@ class RoomMemberListFragment @Inject constructor(
}
override fun onDestroyView() {
recyclerView.cleanup()
roomSettingsRecyclerView.cleanup()
super.onDestroyView()
}

View File

@ -25,7 +25,6 @@ sealed class RoomSettingsAction : VectorViewModelAction {
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()
data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction()
object EnableEncryption : RoomSettingsAction()
object Save : 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.toModel
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 javax.inject.Inject
@ -44,7 +43,6 @@ class RoomSettingsController @Inject constructor(
// Delete the avatar, or cancel an avatar change
fun onAvatarDelete()
fun onAvatarChange()
fun onEnableEncryptionClicked()
fun onNameChanged(name: String)
fun onTopicChanged(topic: String)
fun onHistoryVisibilityClicked()
@ -130,33 +128,6 @@ class RoomSettingsController @Inject constructor(
editable = data.actionPermissions.canChangeHistoryReadability,
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? {

View File

@ -72,7 +72,7 @@ class RoomSettingsFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
controller.callback = this
setupToolbar(roomSettingsToolbar)
recyclerView.configureWith(controller, hasFixedSize = true)
roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true)
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
@ -93,7 +93,8 @@ class RoomSettingsFragment @Inject constructor(
}
override fun onDestroyView() {
recyclerView.cleanup()
controller.callback = null
roomSettingsRecyclerView.cleanup()
super.onDestroyView()
}
@ -127,17 +128,6 @@ class RoomSettingsFragment @Inject constructor(
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) {
viewModel.handle(RoomSettingsAction.SetRoomName(name))
}

View File

@ -17,7 +17,6 @@
package im.vector.app.features.roomprofile.settings
import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@ -28,7 +27,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Completable
import io.reactivex.Observable
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
@ -118,8 +116,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_CANONICAL_ALIAS),
canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_HISTORY_VISIBILITY),
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
EventType.STATE_ROOM_HISTORY_VISIBILITY)
)
setState { copy(actionPermissions = permissions) }
}
@ -142,7 +139,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomSettingsAction) {
when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
is RoomSettingsAction.SetAvatarAction -> handleSetAvatarAction(action)
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
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) {
setState {
copy(isLoading = isLoading)

View File

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

View File

@ -24,6 +24,7 @@ import com.squareup.seismic.ShakeDetector
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.features.disclaimer.SHARED_PREF_KEY
import im.vector.app.features.homeserver.ServerUrlsRepository
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -248,6 +249,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
// theme
keysToKeep.add(ThemeUtils.APPLICATION_THEME_KEY)
// Disclaimer dialog
keysToKeep.add(SHARED_PREF_KEY)
// get all the existing keys
val keys = defaultPrefs.all.keys

View File

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

View File

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

@ -68,12 +68,12 @@ class CrossSigningSettingsFragment @Inject constructor(
}
private fun setupRecyclerView() {
recyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
genericRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.interactionListener = this
}
override fun onDestroyView() {
recyclerView.cleanup()
genericRecyclerView.cleanup()
controller.interactionListener = null
super.onDestroyView()
}

View File

@ -61,7 +61,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
devicesController.callback = this
recyclerView.configureWith(devicesController, showDivider = true)
genericRecyclerView.configureWith(devicesController, showDivider = true)
viewModel.observeViewEvents {
when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message)
@ -97,7 +97,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
override fun onDestroyView() {
devicesController.callback = null
recyclerView.cleanup()
genericRecyclerView.cleanup()
super.onDestroyView()
}

View File

@ -57,13 +57,13 @@ class AccountDataFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
genericRecyclerView.cleanup()
epoxyController.interactionListener = null
}

View File

@ -50,13 +50,13 @@ class GossipingEventsPaperTrailFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
genericRecyclerView.cleanup()
epoxyController.interactionListener = null
}

View File

@ -45,11 +45,11 @@ class IncomingKeyRequestListFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
genericRecyclerView.cleanup()
}
}

View File

@ -41,13 +41,13 @@ class OutgoingKeyRequestListFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
// epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
genericRecyclerView.cleanup()
// epoxyController.interactionListener = null
}
}

View File

@ -49,7 +49,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
ignoredUsersController.callback = this
recyclerView.configureWith(ignoredUsersController)
genericRecyclerView.configureWith(ignoredUsersController)
viewModel.observeViewEvents {
when (it) {
is IgnoredUsersViewEvents.Loading -> showLoading(it.message)
@ -60,7 +60,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
override fun onDestroyView() {
ignoredUsersController.callback = null
recyclerView.cleanup()
genericRecyclerView.cleanup()
super.onDestroyView()
}

View File

@ -59,11 +59,11 @@ class PushGatewaysFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
recyclerView.cleanup()
genericRecyclerView.cleanup()
super.onDestroyView()
}

View File

@ -43,11 +43,11 @@ class PushRulesFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
genericRecyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
recyclerView.cleanup()
genericRecyclerView.cleanup()
super.onDestroyView()
}

Some files were not shown because too many files have changed in this diff Show More