mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge branch 'develop' into feature/bma_create_room_form
This commit is contained in:
commit
85bc5f54aa
118 changed files with 1900 additions and 964 deletions
11
AUTHORS.md
11
AUTHORS.md
|
@ -4,7 +4,7 @@ A full developer contributors list can be found [here](https://github.com/vector
|
|||
|
||||
Even if we try to be able to work on all the functionalities, we have more knowledge about what we have developed ourselves.
|
||||
|
||||
## Benoit: Android team leader
|
||||
## [Benoit](https://github.com/bmarty): Android team leader
|
||||
|
||||
[@benoit.marty:matrix.org](https://matrix.to/#/@benoit.marty:matrix.org)
|
||||
- Android team leader and project leader, Android developer, GitHub community manager.
|
||||
|
@ -12,7 +12,7 @@ Even if we try to be able to work on all the functionalities, we have more knowl
|
|||
- Reviewing and polishing developed features, code quality manager, PRs reviewer, GitHub community manager.
|
||||
- Release manager on the Play Store
|
||||
|
||||
## François: Software architect
|
||||
## [Ganfra](https://github.com/ganfra) (aka François): Software architect
|
||||
|
||||
[@ganfra:matrix.org](https://matrix.to/#/@ganfra:matrix.org)
|
||||
- Software architect, Android developer
|
||||
|
@ -20,12 +20,17 @@ Even if we try to be able to work on all the functionalities, we have more knowl
|
|||
- Work mainly on the global architecture of the project.
|
||||
- Specialist of the timeline, and lots of other features.
|
||||
|
||||
## Valere: Product manager, Android developer
|
||||
## [Valere](https://github.com/BillCarsonFr): Product manager, Android developer
|
||||
|
||||
[@valere35:matrix.org](https://matrix.to/#/@valere35:matrix.org)
|
||||
- Product manager, Android developer
|
||||
- Specialist on the crypto implementation.
|
||||
|
||||
## [Onuray](https://github.com/onurays): Android developer
|
||||
|
||||
[@onurays:matrix.org](https://matrix.to/#/@onurays:matrix.org)
|
||||
- Android developer
|
||||
|
||||
# Other contributors
|
||||
|
||||
First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.
|
||||
|
|
13
CHANGES.md
13
CHANGES.md
|
@ -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:
|
||||
-
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
|
|||
import org.matrix.android.sdk.api.session.pushers.Pusher
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
|
@ -92,6 +93,13 @@ class RxSession(private val session: Session) {
|
|||
}
|
||||
}
|
||||
|
||||
fun liveRoomMember(userId: String, roomId: String): Observable<Optional<RoomMemberSummary>> {
|
||||
return session.getRoomMemberLive(userId, roomId).asObservable()
|
||||
.startWithCallable {
|
||||
session.getRoomMember(userId, roomId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
return session.getUsersLive().asObservable()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -15,11 +15,9 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.api.pushrules
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
interface PushRuleService {
|
||||
/**
|
||||
|
@ -29,13 +27,13 @@ interface PushRuleService {
|
|||
|
||||
fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet
|
||||
|
||||
fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean)
|
||||
|
||||
fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun addPushRule(kind: RuleKind, pushRule: PushRule)
|
||||
|
||||
fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule)
|
||||
|
||||
fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
|
||||
|
||||
fun addPushRuleListener(listener: PushRuleListener)
|
||||
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
package org.matrix.android.sdk.api.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()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.api.session.permalinks
|
||||
|
||||
import android.text.Spannable
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
|
||||
/**
|
||||
* MatrixLinkify take a piece of text and turns all of the
|
||||
|
@ -35,7 +36,7 @@ object MatrixLinkify {
|
|||
* I disable it because it mess up with pills, and even with pills, it does not work correctly:
|
||||
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
|
||||
*/
|
||||
/*
|
||||
|
||||
// sanity checks
|
||||
if (spannable.isEmpty()) {
|
||||
return false
|
||||
|
@ -48,14 +49,21 @@ object MatrixLinkify {
|
|||
val startPos = match.range.first
|
||||
if (startPos == 0 || text[startPos - 1] != '/') {
|
||||
val endPos = match.range.last + 1
|
||||
val url = text.substring(match.range)
|
||||
var url = text.substring(match.range)
|
||||
if (MatrixPatterns.isUserId(url)
|
||||
|| MatrixPatterns.isRoomAlias(url)
|
||||
|| MatrixPatterns.isRoomId(url)
|
||||
|| MatrixPatterns.isGroupId(url)
|
||||
|| MatrixPatterns.isEventId(url)) {
|
||||
url = PermalinkService.MATRIX_TO_URL_BASE + url
|
||||
}
|
||||
val span = MatrixPermalinkSpan(url, callback)
|
||||
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasMatch
|
||||
*/
|
||||
return false
|
||||
|
||||
// return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room
|
|||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
@ -141,4 +142,20 @@ interface RoomService {
|
|||
* - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
|
||||
*/
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): String?
|
||||
|
||||
/**
|
||||
* Get a room member for the tuple {userId,roomId}
|
||||
* @param userId the userId to look for.
|
||||
* @param roomId the roomId to look for.
|
||||
* @return the room member or null
|
||||
*/
|
||||
fun getRoomMember(userId: String, roomId: String): RoomMemberSummary?
|
||||
|
||||
/**
|
||||
* Observe a live room member for the tuple {userId,roomId}
|
||||
* @param userId the userId to look for.
|
||||
* @param roomId the roomId to look for.
|
||||
* @return a LiveData of the optional found room member
|
||||
*/
|
||||
fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>>
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
package org.matrix.android.sdk.api.session.room.send
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
interface DraftService {
|
||||
|
@ -26,12 +24,12 @@ interface DraftService {
|
|||
/**
|
||||
* Save or update a draft to the room
|
||||
*/
|
||||
fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun saveDraft(draft: UserDraft)
|
||||
|
||||
/**
|
||||
* Delete the last draft, basically just after sending the message
|
||||
*/
|
||||
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun deleteDraft()
|
||||
|
||||
/**
|
||||
* Return the current draft or null
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -52,5 +52,8 @@ internal class TimeOutInterceptor @Inject constructor() : Interceptor {
|
|||
const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT"
|
||||
const val READ_TIMEOUT = "READ_TIMEOUT"
|
||||
const val WRITE_TIMEOUT = "WRITE_TIMEOUT"
|
||||
|
||||
// 1 minute
|
||||
const val DEFAULT_LONG_TIMEOUT: Long = 60_000
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,20 +16,13 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.group
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.group.Group
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
|
||||
internal class DefaultGroup(override val groupId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val getGroupDataTask: GetGroupDataTask) : Group {
|
||||
|
||||
override fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable {
|
||||
override suspend fun fetchGroupData() {
|
||||
val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId))
|
||||
return getGroupDataTask.configureWith(params) {
|
||||
this.callback = callback
|
||||
}.executeBy(taskExecutor)
|
||||
getGroupDataTask.execute(params)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.group
|
|||
|
||||
import org.matrix.android.sdk.api.session.group.Group
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GroupFactory {
|
||||
|
@ -26,14 +25,12 @@ internal interface GroupFactory {
|
|||
}
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask,
|
||||
private val taskExecutor: TaskExecutor) :
|
||||
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) :
|
||||
GroupFactory {
|
||||
|
||||
override fun create(groupId: String): Group {
|
||||
return DefaultGroup(
|
||||
groupId = groupId,
|
||||
taskExecutor = taskExecutor,
|
||||
getGroupDataTask = getGroupDataTask
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -17,27 +17,37 @@
|
|||
package org.matrix.android.sdk.internal.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import org.matrix.android.sdk.internal.util.fetchCopied
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultRoomService @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val createRoomTask: CreateRoomTask,
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
||||
|
@ -118,4 +128,24 @@ internal class DefaultRoomService @Inject constructor(
|
|||
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
|
||||
return roomChangeMembershipStateDataSource.getLiveStates()
|
||||
}
|
||||
|
||||
override fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? {
|
||||
val roomMemberEntity = monarchy.fetchCopied {
|
||||
RoomMemberHelper(it, roomId).getLastRoomMember(userId)
|
||||
}
|
||||
return roomMemberEntity?.asDomain()
|
||||
}
|
||||
|
||||
override fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
{ realm ->
|
||||
RoomMemberHelper(realm, roomId).queryRoomMembersEvent()
|
||||
.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId)
|
||||
},
|
||||
{ it.asDomain() }
|
||||
)
|
||||
return Transformations.map(liveData) { results ->
|
||||
results.firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ import org.matrix.android.sdk.internal.database.query.getOrNull
|
|||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) {
|
||||
|
@ -46,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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,18 +19,14 @@ package org.matrix.android.sdk.internal.session.room.draft
|
|||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.session.room.send.DraftService
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val draftRepository: DraftRepository,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : DraftService {
|
||||
|
||||
|
@ -43,14 +39,14 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
|
|||
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
|
||||
* or even move an existing draft to the top of the list
|
||||
*/
|
||||
override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
override suspend fun saveDraft(draft: UserDraft) {
|
||||
withContext(coroutineDispatchers.main) {
|
||||
draftRepository.saveDraft(roomId, draft)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
override suspend fun deleteDraft() {
|
||||
withContext(coroutineDispatchers.main) {
|
||||
draftRepository.deleteDraft(roomId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,8 @@ internal class RoomDisplayNameResolver @Inject constructor(
|
|||
}
|
||||
} else if (roomEntity?.membership == Membership.JOIN) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
val invitedCount = roomSummary?.invitedMembersCount ?: 0
|
||||
val joinedCount = roomSummary?.joinedMembersCount ?: 0
|
||||
val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||
roomSummary.heroes.mapNotNull { userId ->
|
||||
roomMembers.getLastRoomMember(userId)?.takeIf {
|
||||
|
@ -102,22 +104,49 @@ internal class RoomDisplayNameResolver @Inject constructor(
|
|||
} else {
|
||||
activeMembers.where()
|
||||
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
|
||||
.limit(3)
|
||||
.limit(5)
|
||||
.findAll()
|
||||
.createSnapshot()
|
||||
}
|
||||
val otherMembersCount = otherMembersSubset.count()
|
||||
name = when (otherMembersCount) {
|
||||
0 -> stringProvider.getString(R.string.room_displayname_empty_room)
|
||||
0 -> {
|
||||
stringProvider.getString(R.string.room_displayname_empty_room)
|
||||
// TODO (was xx and yyy) ...
|
||||
}
|
||||
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
|
||||
2 -> stringProvider.getString(R.string.room_displayname_two_members,
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,18 +17,21 @@
|
|||
package org.matrix.android.sdk.internal.session.sync
|
||||
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import org.matrix.android.sdk.internal.network.TimeOutInterceptor
|
||||
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.QueryMap
|
||||
|
||||
internal interface SyncAPI {
|
||||
|
||||
/**
|
||||
* Set all the timeouts to 1 minute
|
||||
* Set all the timeouts to 1 minute by default
|
||||
*/
|
||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync")
|
||||
fun sync(@QueryMap params: Map<String, String>): Call<SyncResponse>
|
||||
fun sync(@QueryMap params: Map<String, String>,
|
||||
@Header(TimeOutInterceptor.CONNECT_TIMEOUT) connectTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT,
|
||||
@Header(TimeOutInterceptor.READ_TIMEOUT) readTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT,
|
||||
@Header(TimeOutInterceptor.WRITE_TIMEOUT) writeTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT
|
||||
): Call<SyncResponse>
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync
|
|||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.R
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.TimeOutInterceptor
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService
|
||||
import org.matrix.android.sdk.internal.session.filter.FilterRepository
|
||||
|
@ -78,8 +79,13 @@ internal class DefaultSyncTask @Inject constructor(
|
|||
// Maybe refresh the home server capabilities data we know
|
||||
getHomeServerCapabilitiesTask.execute(Unit)
|
||||
|
||||
val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
|
||||
|
||||
val syncResponse = executeRequest<SyncResponse>(eventBus) {
|
||||
apiCall = syncAPI.sync(requestParams)
|
||||
apiCall = syncAPI.sync(
|
||||
params = requestParams,
|
||||
readTimeOut = readTimeOut
|
||||
)
|
||||
}
|
||||
syncResponseHandler.handleResponse(syncResponse, token)
|
||||
if (isInitialSync) {
|
||||
|
@ -87,4 +93,8 @@ internal class DefaultSyncTask @Inject constructor(
|
|||
}
|
||||
Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MARGIN: Long = 10_000
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
echo "Configure Element Template..."
|
||||
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
|
||||
{
|
||||
mkdir -p "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
|
||||
ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
|
||||
} && {
|
||||
echo "Please restart Android Studio."
|
||||
|
|
24
tools/templates/unconfigure.sh
Executable file
24
tools/templates/unconfigure.sh
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Copyright 2020 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Template prevent from upgrading Android Studio, so this script de configure the template
|
||||
echo "Un-configure Element Template..."
|
||||
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
|
||||
|
||||
rm "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other/ElementFeature"
|
||||
rm -r "${ANDROID_STUDIO%/}/plugins/android/lib/templates"
|
|
@ -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")) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
90
vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt
Normal file
90
vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,12 +97,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
|||
unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog()
|
||||
viewModelFactory = screenComponent.viewModelFactory()
|
||||
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
|
||||
injectWith(injector())
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
protected open fun injectWith(injector: ScreenComponent) = Unit
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -137,7 +137,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
session.callSignalingService().getCallWithId(it)?.let { mxCall ->
|
||||
this.call = mxCall
|
||||
mxCall.otherUserId
|
||||
val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem()
|
||||
val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()
|
||||
|
||||
mxCall.addListener(callStateListener)
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
|||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.Camera1Enumerator
|
||||
|
@ -330,8 +331,8 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
currentCall?.mxCall
|
||||
?.takeIf { it.state is CallState.Connected }
|
||||
?.let { mxCall ->
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
?: mxCall.roomId
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
// Start background service with notification
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
|
@ -388,7 +389,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
val mxCall = callContext.mxCall
|
||||
// Update service state
|
||||
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.roomId
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
|
@ -576,7 +577,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
?.let { mxCall ->
|
||||
// Start background service with notification
|
||||
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
CallService.onOnGoingCallBackground(
|
||||
context = context,
|
||||
|
@ -650,7 +651,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
callAudioManager.startForCall(createdCall)
|
||||
currentCall = callContext
|
||||
|
||||
val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(createdCall.otherUserId, createdCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: createdCall.otherUserId
|
||||
CallService.onOutgoingCallRinging(
|
||||
context = context.applicationContext,
|
||||
|
@ -706,7 +707,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
}
|
||||
|
||||
// Start background service with notification
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
CallService.onIncomingCallRinging(
|
||||
context = context,
|
||||
|
@ -845,7 +846,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
|||
}
|
||||
val mxCall = call.mxCall
|
||||
// Update service state
|
||||
val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
|
||||
val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName()
|
||||
?: mxCall.otherUserId
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
|
|
|
@ -46,7 +46,7 @@ class JitsiCallViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
init {
|
||||
val me = session.getUser(session.myUserId)?.toMatrixItem()
|
||||
val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem()
|
||||
val userInfo = JitsiMeetUserInfo().apply {
|
||||
displayName = me?.getBestName()
|
||||
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -24,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
|
|||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||
|
@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
|
||||
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
|
||||
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
|
||||
object QuickActionInvitePeople : RoomDetailAction()
|
||||
object QuickActionSetAvatar : RoomDetailAction()
|
||||
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
|
||||
object QuickActionSetTopic : RoomDetailAction()
|
||||
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ import com.google.android.material.textfield.TextInputEditText
|
|||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||
import im.vector.app.core.dialogs.withColoredButton
|
||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
|
@ -82,6 +83,7 @@ import im.vector.app.core.extensions.showKeyboard
|
|||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.intent.getFilenameFromUri
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
|
@ -141,6 +143,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsB
|
|||
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillImageSpan
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.invite.VectorInviteView
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
|
@ -149,6 +152,7 @@ import im.vector.app.features.notifications.NotificationUtils
|
|||
import im.vector.app.features.permalink.NavigationInterceptor
|
||||
import im.vector.app.features.permalink.PermalinkHandler
|
||||
import im.vector.app.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.app.features.roomprofile.RoomProfileActivity
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import im.vector.app.features.share.SharedData
|
||||
|
@ -196,6 +200,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
|||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -221,7 +226,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory
|
||||
) :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
|
@ -229,7 +235,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
AttachmentsHelper.Callback,
|
||||
// RoomWidgetsBannerView.Callback,
|
||||
GalleryOrCameraDialogHelper.Listener,
|
||||
ActiveCallView.Callback {
|
||||
|
||||
companion object {
|
||||
|
@ -250,10 +256,15 @@ class RoomDetailFragment @Inject constructor(
|
|||
private const val ircPattern = " (IRC)"
|
||||
}
|
||||
|
||||
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
|
||||
|
||||
private val roomDetailArgs: RoomDetailArgs by args()
|
||||
private val glideRequests by lazy {
|
||||
GlideApp.with(this)
|
||||
}
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(roomDetailArgs.roomId)
|
||||
}
|
||||
|
||||
private val autoCompleter: AutoCompleter by lazy {
|
||||
autoCompleterFactory.create(roomDetailArgs.roomId)
|
||||
|
@ -364,6 +375,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
|
||||
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
|
||||
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
|
||||
RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId)
|
||||
RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
|
||||
RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings()
|
||||
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
|
||||
navigator.openBigImageViewer(requireActivity(), it.view, item)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -372,6 +389,24 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri?) {
|
||||
uri ?: return
|
||||
roomDetailViewModel.handle(
|
||||
RoomDetailAction.SetAvatarAction(
|
||||
newAvatarUri = uri,
|
||||
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOpenRoomSettings() {
|
||||
navigator.openRoomProfile(
|
||||
requireContext(),
|
||||
roomDetailArgs.roomId,
|
||||
RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
|
||||
navigator.openRoom(requireContext(), openRoom.roomId, null)
|
||||
}
|
||||
|
@ -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, _ ->
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
package im.vector.app.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.command.Command
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import java.io.File
|
||||
|
||||
|
@ -43,6 +45,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
|
||||
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
|
||||
|
||||
object OpenInvitePeople : RoomDetailViewEvents()
|
||||
object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
|
||||
object OpenRoomSettings : RoomDetailViewEvents()
|
||||
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents()
|
||||
|
||||
object ShowWaitingView : RoomDetailViewEvents()
|
||||
object HideWaitingView : RoomDetailViewEvents()
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import io.reactivex.functions.BiFunction
|
|||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.commonmark.parser.Parser
|
||||
|
@ -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)) }
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
|
||||
/**
|
||||
|
@ -60,7 +59,7 @@ data class RoomDetailViewState(
|
|||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
|
||||
val typingMessage: String? = null,
|
||||
|
|
|
@ -123,7 +123,7 @@ class SearchResultController @Inject constructor(
|
|||
.formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
|
||||
.spannable(spannable)
|
||||
.sender(eventAndSender.sender
|
||||
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
|
||||
?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem())
|
||||
.listener { listener?.onItemClicked(eventAndSender.event) }
|
||||
.let { result.add(it) }
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel
|
|||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.reactions.data.EmojiDataSource
|
||||
|
@ -57,18 +58,22 @@ import java.util.ArrayList
|
|||
* Information related to an event and used to display preview in contextual bottom sheet.
|
||||
*/
|
||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: MessageActionState,
|
||||
private val initialState: MessageActionState,
|
||||
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
|
||||
private val htmlCompressor: VectorHtmlCompressor,
|
||||
private val session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
private val eventId = initialState.eventId
|
||||
private val informationData = initialState.informationData
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(initialState.roomId)
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -31,10 +31,14 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
|
||||
|
@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
collapsedEventIds.removeAll(mergedEventIds)
|
||||
}
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||
val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId
|
||||
?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) }
|
||||
?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel<PowerLevelsContent>() }
|
||||
?.let { PowerLevelsHelper(it) }
|
||||
val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
|
||||
val attributes = MergedRoomCreationItem.Attributes(
|
||||
isCollapsed = isCollapsed,
|
||||
mergeData = mergedData,
|
||||
|
@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
hasEncryptionEvent = hasEncryption,
|
||||
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
readReceiptsCallback = callback,
|
||||
currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
|
||||
callback = callback,
|
||||
currentUserId = currentUserId,
|
||||
roomSummary = roomSummaryHolder.roomSummary,
|
||||
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
|
||||
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
|
||||
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
|
||||
)
|
||||
MergedRoomCreationItem_()
|
||||
.id(mergeId)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(isCollapsed && highlighted)
|
||||
.attributes(attributes)
|
||||
.movementMethod(createLinkMovementMethod(callback))
|
||||
.also {
|
||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
|
|||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import im.vector.app.features.html.CodeVisitor
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
|
@ -106,15 +107,19 @@ class MessageItemFactory @Inject constructor(
|
|||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val session: Session) {
|
||||
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId)
|
||||
}
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.isRedacted()) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -17,21 +17,23 @@
|
|||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import android.text.Spannable
|
||||
import androidx.core.text.toSpannable
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.html.TagHandlerNoOp
|
||||
import org.commonmark.node.Node
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class EventHtmlRenderer @Inject constructor(context: Context,
|
||||
htmlConfigure: MatrixHtmlPluginConfigure) {
|
||||
class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfigure,
|
||||
context: Context) {
|
||||
|
||||
interface PostProcessor {
|
||||
fun afterRender(renderedText: Spannable)
|
||||
}
|
||||
|
||||
private val markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create(htmlConfigure))
|
||||
|
@ -41,35 +43,47 @@ class EventHtmlRenderer @Inject constructor(context: Context,
|
|||
return markwon.parse(text)
|
||||
}
|
||||
|
||||
fun render(text: String): CharSequence {
|
||||
/**
|
||||
* @param text the text you want to render
|
||||
* @param postProcessors an optional array of post processor to add any span if needed
|
||||
*/
|
||||
fun render(text: String, vararg postProcessors: PostProcessor): CharSequence {
|
||||
return try {
|
||||
markwon.toMarkdown(text)
|
||||
val parsed = markwon.parse(text)
|
||||
renderAndProcess(parsed, postProcessors)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to render $text to html")
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
fun render(node: Node): CharSequence? {
|
||||
/**
|
||||
* @param node the node you want to render
|
||||
* @param postProcessors an optional array of post processor to add any span if needed
|
||||
*/
|
||||
fun render(node: Node, vararg postProcessors: PostProcessor): CharSequence? {
|
||||
return try {
|
||||
markwon.render(node)
|
||||
renderAndProcess(node, postProcessors)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to render $node to html")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderAndProcess(node: Node, postProcessors: Array<out PostProcessor>): CharSequence {
|
||||
val renderedText = markwon.render(node).toSpannable()
|
||||
postProcessors.forEach {
|
||||
it.afterRender(renderedText)
|
||||
}
|
||||
return renderedText
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure {
|
||||
|
||||
override fun configureHtml(plugin: HtmlPlugin) {
|
||||
plugin
|
||||
.addHandler(TagHandlerNoOp.create("a"))
|
||||
.addHandler(FontTagHandler())
|
||||
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
|
||||
.addHandler(MxReplyTagHandler())
|
||||
.addHandler(SpanHandler(colorProvider))
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.style.URLSpan
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.tag.LinkHandler
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
class MxLinkTagHandler(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val sessionHolder: ActiveSessionHolder) : LinkHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val link = tag.attributes()["href"]
|
||||
if (link != null) {
|
||||
val permalinkData = PermalinkParser.parse(link)
|
||||
val matrixItem = when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
|
||||
MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl)
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
if (permalinkData.eventId == null) {
|
||||
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
|
||||
if (permalinkData.isRoomAlias) {
|
||||
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
} else {
|
||||
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
}
|
||||
} else {
|
||||
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
||||
null
|
||||
}
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
|
||||
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (matrixItem == null) {
|
||||
super.handle(visitor, renderer, tag)
|
||||
} else {
|
||||
val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
span,
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
URLSpan(link),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
super.handle(visitor, renderer, tag)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import io.noties.markwon.core.spans.LinkSpan
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomId: String?,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val sessionHolder: ActiveSessionHolder)
|
||||
: EventHtmlRenderer.PostProcessor {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(roomId: String?): PillsPostProcessor
|
||||
}
|
||||
|
||||
override fun afterRender(renderedText: Spannable) {
|
||||
addPillSpans(renderedText, roomId)
|
||||
}
|
||||
|
||||
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
|
||||
// We let markdown handle links and then we add PillImageSpan if needed.
|
||||
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
|
||||
linkSpans.forEach { linkSpan ->
|
||||
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
||||
val startSpan = renderedText.getSpanStart(linkSpan)
|
||||
val endSpan = renderedText.getSpanEnd(linkSpan)
|
||||
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
|
||||
val permalinkData = PermalinkParser.parse(url)
|
||||
val matrixItem = when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
if (roomId == null) {
|
||||
sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem()
|
||||
} else {
|
||||
sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem()
|
||||
}
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
if (permalinkData.eventId == null) {
|
||||
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
|
||||
if (permalinkData.isRoomAlias) {
|
||||
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
} else {
|
||||
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||
}
|
||||
} else {
|
||||
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
||||
null
|
||||
}
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
|
||||
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
|
||||
}
|
||||
else -> null
|
||||
} ?: return null
|
||||
return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ import im.vector.app.core.platform.ButtonStateView
|
|||
import im.vector.app.features.home.AvatarRenderer
|
||||
import kotlinx.android.synthetic.main.vector_invite_view.view.*
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -73,7 +73,7 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||
}
|
||||
}
|
||||
|
||||
fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
|
||||
fun render(sender: RoomMemberSummary, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
|
||||
if (mode == Mode.LARGE) {
|
||||
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
|
||||
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor(
|
|||
context.startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
|
||||
override fun openRoomProfile(context: Context, roomId: String) {
|
||||
context.startActivity(RoomProfileActivity.newIntent(context, roomId))
|
||||
override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) {
|
||||
context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
|
||||
}
|
||||
|
||||
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {
|
||||
|
|
|
@ -78,7 +78,7 @@ interface Navigator {
|
|||
|
||||
fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false)
|
||||
|
||||
fun openRoomProfile(context: Context, roomId: String)
|
||||
fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null)
|
||||
|
||||
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)
|
||||
|
||||
|
|
|
@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
|||
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
|
||||
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
||||
val roomId = event.roomId ?: return null
|
||||
val dName = event.senderId?.let { session.getUser(it)?.displayName }
|
||||
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
|
||||
if (Membership.INVITE == content.membership) {
|
||||
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
|
||||
?: stringProvider.getString(R.string.notification_new_invitation)
|
||||
|
|
|
@ -120,7 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
null,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
session.getUser(session.myUserId)?.displayName
|
||||
session.getRoomMember(session.myUserId, room.roomId)?.displayName
|
||||
?: context?.getString(R.string.notification_sender_me),
|
||||
session.myUserId,
|
||||
message,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
|
||||
sealed class RoomProfileAction : VectorViewModelAction {
|
||||
object EnableEncryption : RoomProfileAction()
|
||||
object LeaveRoom : RoomProfileAction()
|
||||
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
|
||||
object ShareRoomProfile : RoomProfileAction()
|
||||
|
|
|
@ -46,10 +46,16 @@ class RoomProfileActivity :
|
|||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, roomId: String): Intent {
|
||||
private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
|
||||
|
||||
const val EXTRA_DIRECT_ACCESS_ROOM_ROOT = 0
|
||||
const val EXTRA_DIRECT_ACCESS_ROOM_SETTINGS = 1
|
||||
|
||||
fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent {
|
||||
val roomProfileArgs = RoomProfileArgs(roomId)
|
||||
return Intent(context, RoomProfileActivity::class.java).apply {
|
||||
putExtra(MvRx.KEY_ARG, roomProfileArgs)
|
||||
putExtra(EXTRA_DIRECT_ACCESS, directAccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +86,13 @@ class RoomProfileActivity :
|
|||
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
|
||||
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
|
||||
if (isFirstCreation()) {
|
||||
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()
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericFooterItem
|
|||
import im.vector.app.features.home.ShortcutCreator
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomProfileController @Inject constructor(
|
||||
|
@ -43,6 +44,7 @@ class RoomProfileController @Inject constructor(
|
|||
|
||||
interface Callback {
|
||||
fun onLearnMoreClicked()
|
||||
fun onEnableEncryptionClicked()
|
||||
fun onMemberListClicked()
|
||||
fun onBannedMemberListClicked()
|
||||
fun onNotificationsClicked()
|
||||
|
@ -84,6 +86,7 @@ class RoomProfileController @Inject constructor(
|
|||
centered(false)
|
||||
text(stringProvider.getString(learnMoreSubtitle))
|
||||
}
|
||||
buildEncryptionAction(data.actionPermissions, roomSummary)
|
||||
|
||||
// More
|
||||
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
|
||||
|
@ -171,4 +174,29 @@ class RoomProfileController @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEncryptionAction(actionPermissions: RoomProfileViewState.ActionPermissions, roomSummary: RoomSummary) {
|
||||
if (!roomSummary.isEncrypted) {
|
||||
if (actionPermissions.canEnableEncryption) {
|
||||
buildProfileAction(
|
||||
id = "enableEncryption",
|
||||
title = stringProvider.getString(R.string.room_settings_enable_encryption),
|
||||
dividerColor = dividerColor,
|
||||
icon = R.drawable.ic_shield_black,
|
||||
divider = false,
|
||||
editable = false,
|
||||
action = { callback?.onEnableEncryptionClicked() }
|
||||
)
|
||||
} else {
|
||||
buildProfileAction(
|
||||
id = "enableEncryption",
|
||||
title = stringProvider.getString(R.string.room_settings_enable_encryption_no_permission),
|
||||
dividerColor = dividerColor,
|
||||
icon = R.drawable.ic_shield_black,
|
||||
divider = false,
|
||||
editable = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
|
|||
import im.vector.app.features.media.BigImageViewerActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
@ -87,6 +88,7 @@ class RoomProfileFragment @Inject constructor(
|
|||
it.layoutResource = R.layout.view_stub_room_profile_header
|
||||
it.inflate()
|
||||
}
|
||||
setupWaitingView()
|
||||
setupToolbar(matrixProfileToolbar)
|
||||
setupRecyclerView()
|
||||
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(
|
||||
|
@ -111,6 +113,11 @@ class RoomProfileFragment @Inject constructor(
|
|||
setupLongClicks()
|
||||
}
|
||||
|
||||
private fun setupWaitingView() {
|
||||
waiting_view_status_text.setText(R.string.please_wait)
|
||||
waiting_view_status_text.isVisible = true
|
||||
}
|
||||
|
||||
private fun setupLongClicks() {
|
||||
roomProfileNameView.copyOnLongClick()
|
||||
roomProfileAliasView.copyOnLongClick()
|
||||
|
@ -155,6 +162,8 @@ class RoomProfileFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(roomProfileViewModel) { state ->
|
||||
waiting_view.isVisible = state.isLoading
|
||||
|
||||
state.roomSummary()?.also {
|
||||
if (it.membership.isLeft()) {
|
||||
Timber.w("The room has been left")
|
||||
|
@ -187,6 +196,17 @@ class RoomProfileFragment @Inject constructor(
|
|||
vectorBaseActivity.notImplemented()
|
||||
}
|
||||
|
||||
override fun onEnableEncryptionClicked() {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.room_settings_enable_encryption_dialog_title)
|
||||
.setMessage(R.string.room_settings_enable_encryption_dialog_content)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ ->
|
||||
roomProfileViewModel.handle(RoomProfileAction.EnableEncryption)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onMemberListClicked() {
|
||||
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers)
|
||||
}
|
||||
|
|
|
@ -28,12 +28,15 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.ShortcutCreator
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.rx.RxRoom
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
|
@ -65,6 +68,7 @@ class RoomProfileViewModel @AssistedInject constructor(
|
|||
val rxRoom = room.rx()
|
||||
observeRoomSummary(rxRoom)
|
||||
observeBannedRoomMembers(rxRoom)
|
||||
observePermissions()
|
||||
}
|
||||
|
||||
private fun observeRoomSummary(rxRoom: RxRoom) {
|
||||
|
@ -82,8 +86,22 @@ class RoomProfileViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun observePermissions() {
|
||||
PowerLevelsObservableFactory(room)
|
||||
.createObservable()
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
val permissions = RoomProfileViewState.ActionPermissions(
|
||||
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
|
||||
)
|
||||
setState { copy(actionPermissions = permissions) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomProfileAction) {
|
||||
when (action) {
|
||||
is RoomProfileAction.EnableEncryption -> handleEnableEncryption()
|
||||
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
|
||||
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
||||
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
|
||||
|
@ -91,6 +109,24 @@ class RoomProfileViewModel @AssistedInject constructor(
|
|||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleEnableEncryption() {
|
||||
postLoading(true)
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = runCatching { room.enableEncryption() }
|
||||
postLoading(false)
|
||||
result.onFailure { failure ->
|
||||
_viewEvents.post(RoomProfileViewEvents.Failure(failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postLoading(isLoading: Boolean) {
|
||||
setState {
|
||||
copy(isLoading = isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreateShortcut() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withState { state ->
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue