Merge pull request #1658 from vector-im/feature/3pid_invite
3pid invite
This commit is contained in:
commit
eedf545409
|
@ -12,6 +12,8 @@ Improvements 🙌:
|
||||||
- Setup server recovery banner (#1648)
|
- Setup server recovery banner (#1648)
|
||||||
- Set up SSSS from security settings (#1567)
|
- Set up SSSS from security settings (#1567)
|
||||||
- New lab setting to add 'unread notifications' tab to main screen
|
- New lab setting to add 'unread notifications' tab to main screen
|
||||||
|
- Render third party invite event (#548)
|
||||||
|
- Display three pid invites in the room members list (#548)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
|
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
|
||||||
|
@ -27,7 +29,7 @@ Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
-
|
- CreateRoomParams has been updated
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
- Upgrade some dependencies
|
- Upgrade some dependencies
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.rx
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import im.vector.matrix.android.api.query.QueryStringValue
|
import im.vector.matrix.android.api.query.QueryStringValue
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
@ -71,6 +72,13 @@ class RxRoom(private val room: Room) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveStateEvents(eventTypes: Set<String>): Observable<List<Event>> {
|
||||||
|
return room.getStateEventsLive(eventTypes).asObservable()
|
||||||
|
.startWithCallable {
|
||||||
|
room.getStateEvents(eventTypes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun liveReadMarker(): Observable<Optional<String>> {
|
fun liveReadMarker(): Observable<Optional<String>> {
|
||||||
return room.getReadMarkerLive().asObservable()
|
return room.getReadMarkerLive().asObservable()
|
||||||
}
|
}
|
||||||
|
@ -104,6 +112,10 @@ class RxRoom(private val room: Room) {
|
||||||
room.invite(userId, reason, it)
|
room.invite(userId, reason, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invite3pid(threePid: ThreePid): Completable = completableBuilder<Unit> {
|
||||||
|
room.invite3pid(threePid, it)
|
||||||
|
}
|
||||||
|
|
||||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||||
room.updateTopic(topic, it)
|
room.updateTopic(topic, it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||||
|
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encryptedRoom) {
|
if (encryptedRoom) {
|
||||||
|
@ -175,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
samSession.joinRoom(room.roomId, null, it)
|
samSession.joinRoom(room.roomId, null, emptyList(), it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return samSession
|
return samSession
|
||||||
|
@ -286,9 +286,11 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
fun createDM(alice: Session, bob: Session): String {
|
fun createDM(alice: Session, bob: Session): String {
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
alice.createRoom(
|
alice.createRoom(
|
||||||
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
|
CreateRoomParams().apply {
|
||||||
.setDirectMessage()
|
invitedUserIds.add(bob.myUserId)
|
||||||
.enableEncryptionIfInvitedUsersSupportIt(),
|
setDirectMessage()
|
||||||
|
enableEncryptionIfInvitedUsersSupportIt = true
|
||||||
|
},
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
|
||||||
// Create an encrypted room and add a message
|
// Create an encrypted room and add a message
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
aliceSession.createRoom(
|
aliceSession.createRoom(
|
||||||
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
CreateRoomParams().apply {
|
||||||
|
visibility = RoomDirectoryVisibility.PRIVATE
|
||||||
|
enableEncryption()
|
||||||
|
},
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -285,7 +288,7 @@ class KeyShareTests : InstrumentedTest {
|
||||||
mTestHelper.waitWithLatch(60_000) { latch ->
|
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.api.extensions
|
||||||
|
|
||||||
|
fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
|
||||||
|
return when {
|
||||||
|
startsWith(prefix) -> this
|
||||||
|
else -> "$prefix$this"
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,5 +113,5 @@ interface RoomService {
|
||||||
*/
|
*/
|
||||||
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
||||||
|
|
||||||
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
|
@ -63,6 +64,12 @@ interface MembershipService {
|
||||||
reason: String? = null,
|
reason: String? = null,
|
||||||
callback: MatrixCallback<Unit>): Cancelable
|
callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invite a user with email or phone number in the room
|
||||||
|
*/
|
||||||
|
fun invite3pid(threePid: ThreePid,
|
||||||
|
callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ban a user from the room
|
* Ban a user from the room
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.api.session.room.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing the EventType.STATE_ROOM_THIRD_PARTY_INVITE state event content
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-third-party-invite
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RoomThirdPartyInviteContent(
|
||||||
|
/**
|
||||||
|
* Required. A user-readable string which represents the user who has been invited.
|
||||||
|
* This should not contain the user's third party ID, as otherwise when the invite
|
||||||
|
* is accepted it would leak the association between the matrix ID and the third party ID.
|
||||||
|
*/
|
||||||
|
@Json(name = "display_name") val displayName: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A URL which can be fetched, with querystring public_key=public_key, to validate
|
||||||
|
* whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'.
|
||||||
|
*/
|
||||||
|
@Json(name = "key_validity_url") val keyValidityUrl: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in
|
||||||
|
* public_keys is also sufficient). This exists for backwards compatibility.
|
||||||
|
*/
|
||||||
|
@Json(name = "public_key") val publicKey: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys with which the token may be signed.
|
||||||
|
*/
|
||||||
|
@Json(name = "public_keys") val publicKeys: List<PublicKeys> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PublicKeys(
|
||||||
|
/**
|
||||||
|
* An optional URL which can be fetched, with querystring public_key=public_key, to validate whether the key
|
||||||
|
* has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. If this URL
|
||||||
|
* is absent, the key must be considered valid indefinitely.
|
||||||
|
*/
|
||||||
|
@Json(name = "key_validity_url") val keyValidityUrl: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A base-64 encoded ed25519 key with which token may be signed.
|
||||||
|
*/
|
||||||
|
@Json(name = "public_key") val publicKey: String
|
||||||
|
)
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,253 +16,102 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.create
|
package im.vector.matrix.android.api.session.room.model.create
|
||||||
|
|
||||||
import android.util.Patterns
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
import androidx.annotation.CheckResult
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import im.vector.matrix.android.api.MatrixPatterns.isUserId
|
|
||||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
// TODO Give a way to include other initial states
|
||||||
* Parameter to create a room, with facilities functions to configure it
|
class CreateRoomParams {
|
||||||
*/
|
/**
|
||||||
@JsonClass(generateAdapter = true)
|
* A public visibility indicates that the room will be shown in the published room list.
|
||||||
data class CreateRoomParams(
|
* A private visibility will hide the room from the published room list.
|
||||||
/**
|
* Rooms default to private visibility if this key is not included.
|
||||||
* A public visibility indicates that the room will be shown in the published room list.
|
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||||
* A private visibility will hide the room from the published room list.
|
*/
|
||||||
* Rooms default to private visibility if this key is not included.
|
var visibility: RoomDirectoryVisibility? = null
|
||||||
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
|
||||||
*/
|
|
||||||
@Json(name = "visibility")
|
|
||||||
val visibility: RoomDirectoryVisibility? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
|
||||||
* The alias will belong on the same homeserver which created the room.
|
|
||||||
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
|
||||||
*/
|
|
||||||
@Json(name = "room_alias_name")
|
|
||||||
val roomAliasName: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
|
|
||||||
* See Room Events for more information on m.room.name.
|
|
||||||
*/
|
|
||||||
@Json(name = "name")
|
|
||||||
val name: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
|
||||||
* See Room Events for more information on m.room.topic.
|
|
||||||
*/
|
|
||||||
@Json(name = "topic")
|
|
||||||
val topic: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of user IDs to invite to the room.
|
|
||||||
* This will tell the server to invite everyone in the list to the newly created room.
|
|
||||||
*/
|
|
||||||
@Json(name = "invite")
|
|
||||||
val invitedUserIds: List<String>? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of objects representing third party IDs to invite into the room.
|
|
||||||
*/
|
|
||||||
@Json(name = "invite_3pid")
|
|
||||||
val invite3pids: List<Invite3Pid>? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra keys to be added to the content of the m.room.create.
|
|
||||||
* The server will clobber the following keys: creator.
|
|
||||||
* Future versions of the specification may allow the server to clobber other keys.
|
|
||||||
*/
|
|
||||||
@Json(name = "creation_content")
|
|
||||||
val creationContent: Any? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of state events to set in the new room.
|
|
||||||
* This allows the user to override the default state events set in the new room.
|
|
||||||
* The expected format of the state events are an object with type, state_key and content keys set.
|
|
||||||
* Takes precedence over events set by presets, but gets overridden by name and topic keys.
|
|
||||||
*/
|
|
||||||
@Json(name = "initial_state")
|
|
||||||
val initialStates: List<Event>? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
|
||||||
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
|
||||||
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
|
||||||
* room creator.
|
|
||||||
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
|
||||||
*/
|
|
||||||
@Json(name = "preset")
|
|
||||||
val preset: CreateRoomPreset? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
|
||||||
* See Direct Messaging for more information.
|
|
||||||
*/
|
|
||||||
@Json(name = "is_direct")
|
|
||||||
val isDirect: Boolean? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The power level content to override in the default power level event
|
|
||||||
*/
|
|
||||||
@Json(name = "power_level_content_override")
|
|
||||||
val powerLevelContentOverride: PowerLevelsContent? = null
|
|
||||||
) {
|
|
||||||
@Transient
|
|
||||||
internal var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After calling this method, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||||
|
* The alias will belong on the same homeserver which created the room.
|
||||||
|
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||||
|
*/
|
||||||
|
var roomAliasName: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is not null, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||||
|
* See Room Events for more information on m.room.name.
|
||||||
|
*/
|
||||||
|
var name: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is not null, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||||
|
* See Room Events for more information on m.room.topic.
|
||||||
|
*/
|
||||||
|
var topic: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of user IDs to invite to the room.
|
||||||
|
* This will tell the server to invite everyone in the list to the newly created room.
|
||||||
|
*/
|
||||||
|
val invitedUserIds = mutableListOf<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of objects representing third party IDs to invite into the room.
|
||||||
|
*/
|
||||||
|
val invite3pids = mutableListOf<ThreePid>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
||||||
* the encryption will be enabled on the created room
|
* the encryption will be enabled on the created room
|
||||||
* @param value true to activate this behavior.
|
|
||||||
* @return this, to allow chaining methods
|
|
||||||
*/
|
*/
|
||||||
fun enableEncryptionIfInvitedUsersSupportIt(value: Boolean = true): CreateRoomParams {
|
var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
||||||
enableEncryptionIfInvitedUsersSupportIt = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the crypto algorithm to the room creation parameters.
|
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||||
*
|
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||||
* @param enable true to enable encryption.
|
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||||
* @param algorithm the algorithm, default to [MXCRYPTO_ALGORITHM_MEGOLM], which is actually the only supported algorithm for the moment
|
* room creator.
|
||||||
* @return a modified copy of the CreateRoomParams object, or this if there is no modification
|
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
var preset: CreateRoomPreset? = null
|
||||||
fun enableEncryptionWithAlgorithm(enable: Boolean = true,
|
|
||||||
algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams {
|
|
||||||
// Remove the existing value if any.
|
|
||||||
val newInitialStates = initialStates
|
|
||||||
?.filter { it.type != EventType.STATE_ROOM_ENCRYPTION }
|
|
||||||
|
|
||||||
return if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
|
|
||||||
if (enable) {
|
|
||||||
val contentMap = mapOf("algorithm" to algorithm)
|
|
||||||
|
|
||||||
val algoEvent = Event(
|
|
||||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
|
||||||
stateKey = "",
|
|
||||||
content = contentMap.toContent()
|
|
||||||
)
|
|
||||||
|
|
||||||
copy(
|
|
||||||
initialStates = newInitialStates.orEmpty() + algoEvent
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return copy(
|
|
||||||
initialStates = newInitialStates
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.e("Unsupported algorithm: $algorithm")
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the history visibility in the room creation parameters.
|
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||||
*
|
* See Direct Messaging for more information.
|
||||||
* @param historyVisibility the expected history visibility, set null to remove any existing value.
|
|
||||||
* @return a modified copy of the CreateRoomParams object
|
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
var isDirect: Boolean? = null
|
||||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams {
|
|
||||||
// Remove the existing value if any.
|
|
||||||
val newInitialStates = initialStates
|
|
||||||
?.filter { it.type != EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
|
||||||
|
|
||||||
if (historyVisibility != null) {
|
/**
|
||||||
val contentMap = mapOf("history_visibility" to historyVisibility)
|
* Extra keys to be added to the content of the m.room.create.
|
||||||
|
* The server will clobber the following keys: creator.
|
||||||
|
* Future versions of the specification may allow the server to clobber other keys.
|
||||||
|
*/
|
||||||
|
var creationContent: Any? = null
|
||||||
|
|
||||||
val historyVisibilityEvent = Event(
|
/**
|
||||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
* The power level content to override in the default power level event
|
||||||
stateKey = "",
|
*/
|
||||||
content = contentMap.toContent())
|
var powerLevelContentOverride: PowerLevelsContent? = null
|
||||||
|
|
||||||
return copy(
|
|
||||||
initialStates = newInitialStates.orEmpty() + historyVisibilityEvent
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return copy(
|
|
||||||
initialStates = newInitialStates
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark as a direct message room.
|
* Mark as a direct message room.
|
||||||
* @return a modified copy of the CreateRoomParams object
|
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
fun setDirectMessage() {
|
||||||
fun setDirectMessage(): CreateRoomParams {
|
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||||
return copy(
|
isDirect = true
|
||||||
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
|
|
||||||
isDirect = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if the created room can be a direct chat one.
|
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
*
|
|
||||||
* @return true if it is a direct chat
|
|
||||||
*/
|
*/
|
||||||
fun isDirect(): Boolean {
|
var algorithm: String? = null
|
||||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
private set
|
||||||
&& isDirect == true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
var historyVisibility: RoomHistoryVisibility? = null
|
||||||
* @return the first invited user id
|
|
||||||
*/
|
|
||||||
fun getFirstInvitedUserId(): String? {
|
|
||||||
return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
fun enableEncryption() {
|
||||||
* Add some ids to the room creation
|
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
* ids might be a matrix id or an email address.
|
|
||||||
*
|
|
||||||
* @param ids the participant ids to add.
|
|
||||||
* @return a modified copy of the CreateRoomParams object
|
|
||||||
*/
|
|
||||||
@CheckResult
|
|
||||||
fun addParticipantIds(hsConfig: HomeServerConnectionConfig,
|
|
||||||
userId: String,
|
|
||||||
ids: List<String>): CreateRoomParams {
|
|
||||||
return copy(
|
|
||||||
invite3pids = (invite3pids.orEmpty() + ids
|
|
||||||
.takeIf { hsConfig.identityServerUri != null }
|
|
||||||
?.filter { id -> Patterns.EMAIL_ADDRESS.matcher(id).matches() }
|
|
||||||
?.map { id ->
|
|
||||||
Invite3Pid(
|
|
||||||
idServer = hsConfig.identityServerUri!!.host!!,
|
|
||||||
medium = ThreePidMedium.EMAIL,
|
|
||||||
address = id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.orEmpty())
|
|
||||||
.distinct(),
|
|
||||||
invitedUserIds = (invitedUserIds.orEmpty() + ids
|
|
||||||
.filter { id -> isUserId(id) }
|
|
||||||
// do not invite oneself
|
|
||||||
.filter { id -> id != userId })
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
// TODO add phonenumbers when it will be available
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +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.matrix.android.api.session.room.model.create
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class Invite3Pid(
|
|
||||||
/**
|
|
||||||
* Required.
|
|
||||||
* The hostname+port of the identity server which should be used for third party identifier lookups.
|
|
||||||
*/
|
|
||||||
@Json(name = "id_server")
|
|
||||||
val idServer: String,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required.
|
|
||||||
* The kind of address being passed in the address field, for example email.
|
|
||||||
*/
|
|
||||||
val medium: String,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required.
|
|
||||||
* The invitee's third party identifier.
|
|
||||||
*/
|
|
||||||
val address: String
|
|
||||||
)
|
|
|
@ -62,6 +62,7 @@ import javax.net.ssl.HttpsURLConnection
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class DefaultIdentityService @Inject constructor(
|
internal class DefaultIdentityService @Inject constructor(
|
||||||
private val identityStore: IdentityStore,
|
private val identityStore: IdentityStore,
|
||||||
|
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
|
||||||
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
||||||
private val identityBulkLookupTask: IdentityBulkLookupTask,
|
private val identityBulkLookupTask: IdentityBulkLookupTask,
|
||||||
private val identityRegisterTask: IdentityRegisterTask,
|
private val identityRegisterTask: IdentityRegisterTask,
|
||||||
|
@ -278,7 +279,7 @@ internal class DefaultIdentityService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
|
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
|
||||||
ensureToken()
|
ensureIdentityTokenTask.execute(Unit)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids))
|
identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids))
|
||||||
|
@ -295,17 +296,6 @@ internal class DefaultIdentityService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ensureToken() {
|
|
||||||
val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
|
||||||
val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
|
|
||||||
|
|
||||||
if (identityData.token == null) {
|
|
||||||
// Try to get a token
|
|
||||||
val token = getNewIdentityServerToken(url)
|
|
||||||
identityStore.setToken(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getNewIdentityServerToken(url: String): String {
|
private suspend fun getNewIdentityServerToken(url: String): String {
|
||||||
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
|
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.internal.session.identity
|
||||||
|
|
||||||
|
import dagger.Lazy
|
||||||
|
import im.vector.matrix.android.api.session.identity.IdentityServiceError
|
||||||
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||||
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
|
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
|
||||||
|
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface EnsureIdentityTokenTask : Task<Unit, Unit>
|
||||||
|
|
||||||
|
internal class DefaultEnsureIdentityTokenTask @Inject constructor(
|
||||||
|
private val identityStore: IdentityStore,
|
||||||
|
private val retrofitFactory: RetrofitFactory,
|
||||||
|
@UnauthenticatedWithCertificate
|
||||||
|
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
|
||||||
|
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
||||||
|
private val identityRegisterTask: IdentityRegisterTask
|
||||||
|
) : EnsureIdentityTokenTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: Unit) {
|
||||||
|
val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
|
||||||
|
if (identityData.token == null) {
|
||||||
|
// Try to get a token
|
||||||
|
val token = getNewIdentityServerToken(url)
|
||||||
|
identityStore.setToken(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getNewIdentityServerToken(url: String): String {
|
||||||
|
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
|
||||||
|
|
||||||
|
val openIdToken = getOpenIdTokenTask.execute(Unit)
|
||||||
|
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
|
||||||
|
|
||||||
|
return token.token
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,9 @@ internal abstract class IdentityModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
|
abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask
|
abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,6 @@ package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
|
||||||
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
|
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||||
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
|
@ -28,9 +25,13 @@ import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
|
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
|
||||||
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
||||||
|
import im.vector.matrix.android.internal.session.room.create.CreateRoomBody
|
||||||
|
import im.vector.matrix.android.internal.session.room.create.CreateRoomResponse
|
||||||
|
import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||||
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
||||||
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
|
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||||
|
@ -79,7 +80,7 @@ internal interface RoomAPI {
|
||||||
*/
|
*/
|
||||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom")
|
||||||
fun createRoom(@Body param: CreateRoomParams): Call<CreateRoomResponse>
|
fun createRoom(@Body param: CreateRoomBody): Call<CreateRoomResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of messages starting from a reference.
|
* Get a list of messages starting from a reference.
|
||||||
|
@ -170,6 +171,14 @@ internal interface RoomAPI {
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
||||||
fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit>
|
fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invite a user to a room, using a ThreePid
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101
|
||||||
|
* @param roomId Required. The room identifier (not alias) to which to invite the user.
|
||||||
|
*/
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
||||||
|
fun invite3pid(@Path("roomId") roomId: String, @Body body: ThreePidInviteBody): Call<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a generic state events
|
* Send a generic state events
|
||||||
*
|
*
|
||||||
|
|
|
@ -44,6 +44,8 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.threepid.DefaultInviteThreePidTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
|
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
||||||
|
@ -139,6 +141,9 @@ internal abstract class RoomModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.internal.session.room.create
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||||
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameter to create a room
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class CreateRoomBody(
|
||||||
|
/**
|
||||||
|
* A public visibility indicates that the room will be shown in the published room list.
|
||||||
|
* A private visibility will hide the room from the published room list.
|
||||||
|
* Rooms default to private visibility if this key is not included.
|
||||||
|
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||||
|
*/
|
||||||
|
@Json(name = "visibility")
|
||||||
|
val visibility: RoomDirectoryVisibility?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||||
|
* The alias will belong on the same homeserver which created the room.
|
||||||
|
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||||
|
*/
|
||||||
|
@Json(name = "room_alias_name")
|
||||||
|
val roomAliasName: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||||
|
* See Room Events for more information on m.room.name.
|
||||||
|
*/
|
||||||
|
@Json(name = "name")
|
||||||
|
val name: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||||
|
* See Room Events for more information on m.room.topic.
|
||||||
|
*/
|
||||||
|
@Json(name = "topic")
|
||||||
|
val topic: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of user IDs to invite to the room.
|
||||||
|
* This will tell the server to invite everyone in the list to the newly created room.
|
||||||
|
*/
|
||||||
|
@Json(name = "invite")
|
||||||
|
val invitedUserIds: List<String>?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of objects representing third party IDs to invite into the room.
|
||||||
|
*/
|
||||||
|
@Json(name = "invite_3pid")
|
||||||
|
val invite3pids: List<ThreePidInviteBody>?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra keys to be added to the content of the m.room.create.
|
||||||
|
* The server will clobber the following keys: creator.
|
||||||
|
* Future versions of the specification may allow the server to clobber other keys.
|
||||||
|
*/
|
||||||
|
@Json(name = "creation_content")
|
||||||
|
val creationContent: Any?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of state events to set in the new room.
|
||||||
|
* This allows the user to override the default state events set in the new room.
|
||||||
|
* The expected format of the state events are an object with type, state_key and content keys set.
|
||||||
|
* Takes precedence over events set by presets, but gets overridden by name and topic keys.
|
||||||
|
*/
|
||||||
|
@Json(name = "initial_state")
|
||||||
|
val initialStates: List<Event>?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||||
|
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||||
|
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||||
|
* room creator.
|
||||||
|
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||||
|
*/
|
||||||
|
@Json(name = "preset")
|
||||||
|
val preset: CreateRoomPreset?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||||
|
* See Direct Messaging for more information.
|
||||||
|
*/
|
||||||
|
@Json(name = "is_direct")
|
||||||
|
val isDirect: Boolean?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The power level content to override in the default power level event
|
||||||
|
*/
|
||||||
|
@Json(name = "power_level_content_override")
|
||||||
|
val powerLevelContentOverride: PowerLevelsContent?
|
||||||
|
)
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.internal.session.room.create
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.api.session.identity.IdentityServiceError
|
||||||
|
import im.vector.matrix.android.api.session.identity.toMedium
|
||||||
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
|
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
||||||
|
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||||
|
import im.vector.matrix.android.internal.session.identity.EnsureIdentityTokenTask
|
||||||
|
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
|
||||||
|
import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
|
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
|
||||||
|
private val crossSigningService: CrossSigningService,
|
||||||
|
private val deviceListManager: DeviceListManager,
|
||||||
|
private val identityStore: IdentityStore,
|
||||||
|
@AuthenticatedIdentity
|
||||||
|
private val accessTokenProvider: AccessTokenProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun build(params: CreateRoomParams): CreateRoomBody {
|
||||||
|
val invite3pids = params.invite3pids
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
.let {
|
||||||
|
// This can throw Exception if Identity server is not configured
|
||||||
|
ensureIdentityTokenTask.execute(Unit)
|
||||||
|
|
||||||
|
val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol()
|
||||||
|
?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
|
||||||
|
params.invite3pids.map {
|
||||||
|
ThreePidInviteBody(
|
||||||
|
id_server = identityServerUrlWithoutProtocol,
|
||||||
|
id_access_token = identityServerAccessToken,
|
||||||
|
medium = it.toMedium(),
|
||||||
|
address = it.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val initialStates = listOfNotNull(
|
||||||
|
buildEncryptionWithAlgorithmEvent(params),
|
||||||
|
buildHistoryVisibilityEvent(params)
|
||||||
|
)
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
return CreateRoomBody(
|
||||||
|
visibility = params.visibility,
|
||||||
|
roomAliasName = params.roomAliasName,
|
||||||
|
name = params.name,
|
||||||
|
topic = params.topic,
|
||||||
|
invitedUserIds = params.invitedUserIds,
|
||||||
|
invite3pids = invite3pids,
|
||||||
|
creationContent = params.creationContent,
|
||||||
|
initialStates = initialStates,
|
||||||
|
preset = params.preset,
|
||||||
|
isDirect = params.isDirect,
|
||||||
|
powerLevelContentOverride = params.powerLevelContentOverride
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
|
||||||
|
return params.historyVisibility
|
||||||
|
?.let {
|
||||||
|
val contentMap = mapOf("history_visibility" to it)
|
||||||
|
|
||||||
|
Event(
|
||||||
|
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
|
stateKey = "",
|
||||||
|
content = contentMap.toContent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the crypto algorithm to the room creation parameters.
|
||||||
|
*/
|
||||||
|
private suspend fun buildEncryptionWithAlgorithmEvent(params: CreateRoomParams): Event? {
|
||||||
|
if (params.algorithm == null
|
||||||
|
&& canEnableEncryption(params)) {
|
||||||
|
// Enable the encryption
|
||||||
|
params.enableEncryption()
|
||||||
|
}
|
||||||
|
return params.algorithm
|
||||||
|
?.let {
|
||||||
|
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||||
|
throw InvalidParameterException("Unsupported algorithm: $it")
|
||||||
|
}
|
||||||
|
val contentMap = mapOf("algorithm" to it)
|
||||||
|
|
||||||
|
Event(
|
||||||
|
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||||
|
stateKey = "",
|
||||||
|
content = contentMap.toContent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
|
||||||
|
return (params.enableEncryptionIfInvitedUsersSupportIt
|
||||||
|
&& crossSigningService.isCrossSigningVerified()
|
||||||
|
&& params.invite3pids.isEmpty())
|
||||||
|
&& params.invitedUserIds.isNotEmpty()
|
||||||
|
&& params.invitedUserIds.let { userIds ->
|
||||||
|
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
|
||||||
|
|
||||||
|
userIds.all { userId ->
|
||||||
|
keys.map[userId].let { deviceMap ->
|
||||||
|
if (deviceMap.isNullOrEmpty()) {
|
||||||
|
// A user has no device, so do not enable encryption
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// Check that every user's device have at least one key
|
||||||
|
deviceMap.values.all { !it.keys.isNullOrEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.create
|
package im.vector.matrix.android.internal.session.room.create
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
|
@ -17,11 +17,9 @@
|
||||||
package im.vector.matrix.android.internal.session.room.create
|
package im.vector.matrix.android.internal.session.room.create
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
|
||||||
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
|
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
|
||||||
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
|
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
||||||
|
@ -51,20 +49,15 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
||||||
private val readMarkersTask: SetReadMarkersTask,
|
private val readMarkersTask: SetReadMarkersTask,
|
||||||
@SessionDatabase
|
@SessionDatabase
|
||||||
private val realmConfiguration: RealmConfiguration,
|
private val realmConfiguration: RealmConfiguration,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val eventBus: EventBus
|
private val eventBus: EventBus
|
||||||
) : CreateRoomTask {
|
) : CreateRoomTask {
|
||||||
|
|
||||||
override suspend fun execute(params: CreateRoomParams): String {
|
override suspend fun execute(params: CreateRoomParams): String {
|
||||||
val createRoomParams = if (canEnableEncryption(params)) {
|
val createRoomBody = createRoomBodyBuilder.build(params)
|
||||||
params.enableEncryptionWithAlgorithm()
|
|
||||||
} else {
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
val createRoomResponse = executeRequest<CreateRoomResponse>(eventBus) {
|
val createRoomResponse = executeRequest<CreateRoomResponse>(eventBus) {
|
||||||
apiCall = roomAPI.createRoom(createRoomParams)
|
apiCall = roomAPI.createRoom(createRoomBody)
|
||||||
}
|
}
|
||||||
val roomId = createRoomResponse.roomId
|
val roomId = createRoomResponse.roomId
|
||||||
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
|
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
|
||||||
|
@ -76,35 +69,13 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
||||||
} catch (exception: TimeoutCancellationException) {
|
} catch (exception: TimeoutCancellationException) {
|
||||||
throw CreateRoomFailure.CreatedWithTimeout
|
throw CreateRoomFailure.CreatedWithTimeout
|
||||||
}
|
}
|
||||||
if (createRoomParams.isDirect()) {
|
if (params.isDirect()) {
|
||||||
handleDirectChatCreation(createRoomParams, roomId)
|
handleDirectChatCreation(params, roomId)
|
||||||
}
|
}
|
||||||
setReadMarkers(roomId)
|
setReadMarkers(roomId)
|
||||||
return roomId
|
return roomId
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
|
|
||||||
return params.enableEncryptionIfInvitedUsersSupportIt
|
|
||||||
&& crossSigningService.isCrossSigningVerified()
|
|
||||||
&& params.invite3pids.isNullOrEmpty()
|
|
||||||
&& params.invitedUserIds?.isNotEmpty() == true
|
|
||||||
&& params.invitedUserIds.let { userIds ->
|
|
||||||
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
|
|
||||||
|
|
||||||
userIds.all { userId ->
|
|
||||||
keys.map[userId].let { deviceMap ->
|
|
||||||
if (deviceMap.isNullOrEmpty()) {
|
|
||||||
// A user has no device, so do not enable encryption
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
// Check that every user's device have at least one key
|
|
||||||
deviceMap.values.all { !it.keys.isNullOrEmpty() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) {
|
private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) {
|
||||||
val otherUserId = params.getFirstInvitedUserId()
|
val otherUserId = params.getFirstInvitedUserId()
|
||||||
?: throw IllegalStateException("You can't create a direct room without an invitedUser")
|
?: throw IllegalStateException("You can't create a direct room without an invitedUser")
|
||||||
|
@ -123,4 +94,21 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
||||||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
|
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
|
||||||
return readMarkersTask.execute(setReadMarkerParams)
|
return readMarkersTask.execute(setReadMarkerParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if the created room can be a direct chat one.
|
||||||
|
*
|
||||||
|
* @return true if it is a direct chat
|
||||||
|
*/
|
||||||
|
private fun CreateRoomParams.isDirect(): Boolean {
|
||||||
|
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||||
|
&& isDirect == true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the first invited user id
|
||||||
|
*/
|
||||||
|
private fun CreateRoomParams.getFirstInvitedUserId(): String? {
|
||||||
|
return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
|
@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.session.room.membership.admin.Membershi
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.fetchCopied
|
import im.vector.matrix.android.internal.util.fetchCopied
|
||||||
|
@ -48,6 +50,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
private val inviteTask: InviteTask,
|
private val inviteTask: InviteTask,
|
||||||
|
private val inviteThreePidTask: InviteThreePidTask,
|
||||||
private val joinTask: JoinRoomTask,
|
private val joinTask: JoinRoomTask,
|
||||||
private val leaveRoomTask: LeaveRoomTask,
|
private val leaveRoomTask: LeaveRoomTask,
|
||||||
private val membershipAdminTask: MembershipAdminTask,
|
private val membershipAdminTask: MembershipAdminTask,
|
||||||
|
@ -152,6 +155,15 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invite3pid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
val params = InviteThreePidTask.Params(roomId, threePid)
|
||||||
|
return inviteThreePidTask
|
||||||
|
.configureWith(params) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
val params = JoinRoomTask.Params(roomId, reason, viaServers)
|
val params = JoinRoomTask.Params(roomId, reason, viaServers)
|
||||||
return joinTask
|
return joinTask
|
||||||
|
|
|
@ -18,13 +18,13 @@ package im.vector.matrix.android.internal.session.room.membership.joining
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
|
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
|
||||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||||
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
|
|
||||||
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
|
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.internal.session.room.membership.threepid
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.identity.IdentityServiceError
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.matrix.android.api.session.identity.toMedium
|
||||||
|
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||||
|
import im.vector.matrix.android.internal.session.identity.EnsureIdentityTokenTask
|
||||||
|
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
|
||||||
|
import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface InviteThreePidTask : Task<InviteThreePidTask.Params, Unit> {
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val threePid: ThreePid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultInviteThreePidTask @Inject constructor(
|
||||||
|
private val roomAPI: RoomAPI,
|
||||||
|
private val eventBus: EventBus,
|
||||||
|
private val identityStore: IdentityStore,
|
||||||
|
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
|
||||||
|
@AuthenticatedIdentity
|
||||||
|
private val accessTokenProvider: AccessTokenProvider
|
||||||
|
) : InviteThreePidTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: InviteThreePidTask.Params) {
|
||||||
|
ensureIdentityTokenTask.execute(Unit)
|
||||||
|
|
||||||
|
val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||||
|
|
||||||
|
return executeRequest(eventBus) {
|
||||||
|
val body = ThreePidInviteBody(
|
||||||
|
id_server = identityServerUrlWithoutProtocol,
|
||||||
|
id_access_token = identityServerAccessToken,
|
||||||
|
medium = params.threePid.toMedium(),
|
||||||
|
address = params.threePid.value
|
||||||
|
)
|
||||||
|
apiCall = roomAPI.invite3pid(params.roomId, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.android.internal.session.room.membership.threepid
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class ThreePidInviteBody(
|
||||||
|
/**
|
||||||
|
* Required. The hostname+port of the identity server which should be used for third party identifier lookups.
|
||||||
|
*/
|
||||||
|
@Json(name = "id_server") val id_server: String,
|
||||||
|
/**
|
||||||
|
* Required. An access token previously registered with the identity server. Servers can treat this as optional
|
||||||
|
* to distinguish between r0.5-compatible clients and this specification version.
|
||||||
|
*/
|
||||||
|
@Json(name = "id_access_token") val id_access_token: String,
|
||||||
|
/**
|
||||||
|
* Required. The kind of address being passed in the address field, for example email.
|
||||||
|
*/
|
||||||
|
@Json(name = "medium") val medium: String,
|
||||||
|
/**
|
||||||
|
* Required. The invitee's third party identifier.
|
||||||
|
*/
|
||||||
|
@Json(name = "address") val address: String
|
||||||
|
)
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.core.contacts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
class ContactsDataSource @Inject constructor(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will return a list of contact from the contacts book of the device, with at least one email or phone.
|
||||||
|
* If both param are false, you will get en empty list.
|
||||||
|
* Note: The return list does not contain any matrixId.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun getContacts(
|
||||||
|
withEmails: Boolean,
|
||||||
|
withMsisdn: Boolean
|
||||||
|
): List<MappedContact> {
|
||||||
|
val map = mutableMapOf<Long, MappedContactBuilder>()
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
measureTimeMillis {
|
||||||
|
contentResolver.query(
|
||||||
|
ContactsContract.Contacts.CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
ContactsContract.Contacts._ID,
|
||||||
|
ContactsContract.Data.DISPLAY_NAME,
|
||||||
|
ContactsContract.Data.PHOTO_URI
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
// Sort by Display name
|
||||||
|
ContactsContract.Data.DISPLAY_NAME
|
||||||
|
)
|
||||||
|
?.use { cursor ->
|
||||||
|
if (cursor.count > 0) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
|
||||||
|
val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
|
||||||
|
|
||||||
|
val mappedContactBuilder = MappedContactBuilder(
|
||||||
|
id = id,
|
||||||
|
displayName = displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.getString(ContactsContract.Data.PHOTO_URI)
|
||||||
|
?.let { Uri.parse(it) }
|
||||||
|
?.let { mappedContactBuilder.photoURI = it }
|
||||||
|
|
||||||
|
map[id] = mappedContactBuilder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the phone numbers
|
||||||
|
if (withMsisdn) {
|
||||||
|
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
|
||||||
|
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)
|
||||||
|
?.use { innerCursor ->
|
||||||
|
while (innerCursor.moveToNext()) {
|
||||||
|
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
|
||||||
|
?.let { map[it] }
|
||||||
|
?: continue
|
||||||
|
innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||||
|
?.let {
|
||||||
|
mappedContactBuilder.msisdns.add(
|
||||||
|
MappedMsisdn(
|
||||||
|
phoneNumber = it,
|
||||||
|
matrixId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Emails
|
||||||
|
if (withEmails) {
|
||||||
|
contentResolver.query(
|
||||||
|
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
ContactsContract.CommonDataKinds.Email.CONTACT_ID,
|
||||||
|
ContactsContract.CommonDataKinds.Email.DATA
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)
|
||||||
|
?.use { innerCursor ->
|
||||||
|
while (innerCursor.moveToNext()) {
|
||||||
|
// This would allow you get several email addresses
|
||||||
|
// if the email addresses were stored in an array
|
||||||
|
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
|
||||||
|
?.let { map[it] }
|
||||||
|
?: continue
|
||||||
|
innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
|
||||||
|
?.let {
|
||||||
|
mappedContactBuilder.emails.add(
|
||||||
|
MappedEmail(
|
||||||
|
email = it,
|
||||||
|
matrixId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also { Timber.d("Took ${it}ms to fetch ${map.size} contact(s)") }
|
||||||
|
|
||||||
|
return map
|
||||||
|
.values
|
||||||
|
.filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
|
||||||
|
.map { it.build() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getString(column: String): String? {
|
||||||
|
return getColumnIndex(column)
|
||||||
|
.takeIf { it != -1 }
|
||||||
|
?.let { getString(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getLong(column: String): Long? {
|
||||||
|
return getColumnIndex(column)
|
||||||
|
.takeIf { it != -1 }
|
||||||
|
?.let { getLong(it) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.core.contacts
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
class MappedContactBuilder(
|
||||||
|
val id: Long,
|
||||||
|
val displayName: String
|
||||||
|
) {
|
||||||
|
var photoURI: Uri? = null
|
||||||
|
val msisdns = mutableListOf<MappedMsisdn>()
|
||||||
|
val emails = mutableListOf<MappedEmail>()
|
||||||
|
|
||||||
|
fun build(): MappedContact {
|
||||||
|
return MappedContact(
|
||||||
|
id = id,
|
||||||
|
displayName = displayName,
|
||||||
|
photoURI = photoURI,
|
||||||
|
msisdns = msisdns,
|
||||||
|
emails = emails
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MappedContact(
|
||||||
|
val id: Long,
|
||||||
|
val displayName: String,
|
||||||
|
val photoURI: Uri? = null,
|
||||||
|
val msisdns: List<MappedMsisdn> = emptyList(),
|
||||||
|
val emails: List<MappedEmail> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MappedEmail(
|
||||||
|
val email: String,
|
||||||
|
val matrixId: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MappedMsisdn(
|
||||||
|
val phoneNumber: String,
|
||||||
|
val matrixId: String?
|
||||||
|
)
|
|
@ -23,6 +23,7 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
|
||||||
|
import im.vector.riotx.features.contactsbook.ContactsBookFragment
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||||
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
|
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
|
||||||
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
||||||
|
@ -528,4 +529,9 @@ interface FragmentModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(WidgetFragment::class)
|
@FragmentKey(WidgetFragment::class)
|
||||||
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
|
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(ContactsBookFragment::class)
|
||||||
|
fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package im.vector.riotx.core.epoxy.profiles
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||||
|
@ -36,16 +37,21 @@ abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>()
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||||
|
@EpoxyAttribute var editable: Boolean = true
|
||||||
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||||
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
val bestName = matrixItem.getBestName()
|
val bestName = matrixItem.getBestName()
|
||||||
val matrixId = matrixItem.id.takeIf { it != bestName }
|
val matrixId = matrixItem.id
|
||||||
holder.view.setOnClickListener(clickListener)
|
.takeIf { it != bestName }
|
||||||
|
// Special case for ThreePid fake matrix item
|
||||||
|
.takeIf { it != "@" }
|
||||||
|
holder.view.setOnClickListener(clickListener?.takeIf { editable })
|
||||||
holder.titleView.text = bestName
|
holder.titleView.text = bestName
|
||||||
holder.subtitleView.setTextOrHide(matrixId)
|
holder.subtitleView.setTextOrHide(matrixId)
|
||||||
|
holder.editableView.isVisible = editable
|
||||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||||
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
|
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
|
||||||
}
|
}
|
||||||
|
@ -55,5 +61,6 @@ abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>()
|
||||||
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
|
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
|
||||||
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)
|
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)
|
||||||
val avatarDecorationImageView by bind<ImageView>(R.id.matrixItemAvatarDecoration)
|
val avatarDecorationImageView by bind<ImageView>(R.id.matrixItemAvatarDecoration)
|
||||||
|
val editableView by bind<View>(R.id.matrixItemEditable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ package im.vector.riotx.core.extensions
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.i18n.phonenumbers.NumberParseException
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||||
|
import im.vector.matrix.android.api.extensions.ensurePrefix
|
||||||
|
|
||||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||||
|
|
||||||
|
@ -33,3 +36,15 @@ fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bu
|
||||||
* Check if a CharSequence is an email
|
* Check if a CharSequence is an email
|
||||||
*/
|
*/
|
||||||
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a CharSequence is a phone number
|
||||||
|
*/
|
||||||
|
fun CharSequence.isMsisdn(): Boolean {
|
||||||
|
return try {
|
||||||
|
PhoneNumberUtil.getInstance().parse(ensurePrefix("+"), null)
|
||||||
|
true
|
||||||
|
} catch (e: NumberParseException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,13 +38,13 @@ inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T?
|
||||||
/**
|
/**
|
||||||
* Call each for each item, and between between each items
|
* Call each for each item, and between between each items
|
||||||
*/
|
*/
|
||||||
inline fun <T> Collection<T>.join(each: (T) -> Unit, between: (T) -> Unit) {
|
inline fun <T> Collection<T>.join(each: (Int, T) -> Unit, between: (Int, T) -> Unit) {
|
||||||
val lastIndex = size - 1
|
val lastIndex = size - 1
|
||||||
forEachIndexed { idx, t ->
|
forEachIndexed { idx, t ->
|
||||||
each(t)
|
each(idx, t)
|
||||||
|
|
||||||
if (idx != lastIndex) {
|
if (idx != lastIndex) {
|
||||||
between(t)
|
between(idx, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
||||||
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||||
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
|
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
|
||||||
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
|
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
|
||||||
|
const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the used permissions statuses.
|
* Log the used permissions statuses.
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
package im.vector.riotx.features.command
|
package im.vector.riotx.features.command
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixPatterns
|
import im.vector.matrix.android.api.MatrixPatterns
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.riotx.core.extensions.isEmail
|
||||||
|
import im.vector.riotx.core.extensions.isMsisdn
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object CommandParser {
|
object CommandParser {
|
||||||
|
@ -139,15 +142,24 @@ object CommandParser {
|
||||||
if (messageParts.size >= 2) {
|
if (messageParts.size >= 2) {
|
||||||
val userId = messageParts[1]
|
val userId = messageParts[1]
|
||||||
|
|
||||||
if (MatrixPatterns.isUserId(userId)) {
|
when {
|
||||||
ParsedCommand.Invite(
|
MatrixPatterns.isUserId(userId) -> {
|
||||||
userId,
|
ParsedCommand.Invite(
|
||||||
textMessage.substring(Command.INVITE.length + userId.length)
|
userId,
|
||||||
.trim()
|
textMessage.substring(Command.INVITE.length + userId.length)
|
||||||
.takeIf { it.isNotBlank() }
|
.trim()
|
||||||
)
|
.takeIf { it.isNotBlank() }
|
||||||
} else {
|
)
|
||||||
ParsedCommand.ErrorSyntax(Command.INVITE)
|
}
|
||||||
|
userId.isEmail() -> {
|
||||||
|
ParsedCommand.Invite3Pid(ThreePid.Email(userId))
|
||||||
|
}
|
||||||
|
userId.isMsisdn() -> {
|
||||||
|
ParsedCommand.Invite3Pid(ThreePid.Msisdn(userId))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.INVITE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ParsedCommand.ErrorSyntax(Command.INVITE)
|
ParsedCommand.ErrorSyntax(Command.INVITE)
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.command
|
package im.vector.riotx.features.command
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represent a parsed command
|
* Represent a parsed command
|
||||||
*/
|
*/
|
||||||
|
@ -41,6 +43,7 @@ sealed class ParsedCommand {
|
||||||
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
|
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
|
||||||
class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand()
|
class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand()
|
||||||
class Invite(val userId: String, val reason: String?) : ParsedCommand()
|
class Invite(val userId: String, val reason: String?) : ParsedCommand()
|
||||||
|
class Invite3Pid(val threePid: ThreePid) : ParsedCommand()
|
||||||
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
|
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
|
||||||
class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
|
class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
|
||||||
class ChangeTopic(val topic: String) : ParsedCommand()
|
class ChangeTopic(val topic: String) : ParsedCommand()
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.ClickListener
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.core.epoxy.onClick
|
||||||
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_contact_detail)
|
||||||
|
abstract class ContactDetailItem : VectorEpoxyModel<ContactDetailItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute lateinit var threePid: String
|
||||||
|
@EpoxyAttribute var matrixId: String? = null
|
||||||
|
@EpoxyAttribute var clickListener: ClickListener? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.view.onClick(clickListener)
|
||||||
|
holder.nameView.text = threePid
|
||||||
|
holder.matrixIdView.setTextOrHide(matrixId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val nameView by bind<TextView>(R.id.contactDetailName)
|
||||||
|
val matrixIdView by bind<TextView>(R.id.contactDetailMatrixId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.contacts.MappedContact
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_contact_main)
|
||||||
|
abstract class ContactItem : VectorEpoxyModel<ContactItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
@EpoxyAttribute lateinit var mappedContact: MappedContact
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
// If name is empty, use userId as name and force it being centered
|
||||||
|
holder.nameView.text = mappedContact.displayName
|
||||||
|
avatarRenderer.render(mappedContact, holder.avatarImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val nameView by bind<TextView>(R.id.contactDisplayName)
|
||||||
|
val avatarImageView by bind<ImageView>(R.id.contactAvatar)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class ContactsBookAction : VectorViewModelAction {
|
||||||
|
data class FilterWith(val filter: String) : ContactsBookAction()
|
||||||
|
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.contacts.MappedContact
|
||||||
|
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
||||||
|
import im.vector.riotx.core.epoxy.loadingItem
|
||||||
|
import im.vector.riotx.core.epoxy.noResultItem
|
||||||
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ContactsBookController @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val errorFormatter: ErrorFormatter) : EpoxyController() {
|
||||||
|
|
||||||
|
private var state: ContactsBookViewState? = null
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setData(state: ContactsBookViewState) {
|
||||||
|
this.state = state
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
val currentState = state ?: return
|
||||||
|
val hasSearch = currentState.searchTerm.isNotEmpty()
|
||||||
|
when (val asyncMappedContacts = currentState.mappedContacts) {
|
||||||
|
is Uninitialized -> renderEmptyState(false)
|
||||||
|
is Loading -> renderLoading()
|
||||||
|
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
|
||||||
|
is Fail -> renderFailure(asyncMappedContacts.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLoading() {
|
||||||
|
loadingItem {
|
||||||
|
id("loading")
|
||||||
|
loadingText(stringProvider.getString(R.string.loading_contact_book))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderFailure(failure: Throwable) {
|
||||||
|
errorWithRetryItem {
|
||||||
|
id("error")
|
||||||
|
text(errorFormatter.toHumanReadable(failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSuccess(mappedContacts: List<MappedContact>,
|
||||||
|
hasSearch: Boolean,
|
||||||
|
onlyBoundContacts: Boolean) {
|
||||||
|
if (mappedContacts.isEmpty()) {
|
||||||
|
renderEmptyState(hasSearch)
|
||||||
|
} else {
|
||||||
|
renderContacts(mappedContacts, onlyBoundContacts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderContacts(mappedContacts: List<MappedContact>, onlyBoundContacts: Boolean) {
|
||||||
|
for (mappedContact in mappedContacts) {
|
||||||
|
contactItem {
|
||||||
|
id(mappedContact.id)
|
||||||
|
mappedContact(mappedContact)
|
||||||
|
avatarRenderer(avatarRenderer)
|
||||||
|
}
|
||||||
|
mappedContact.emails
|
||||||
|
.forEachIndexed { index, it ->
|
||||||
|
if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
|
||||||
|
|
||||||
|
contactDetailItem {
|
||||||
|
id("${mappedContact.id}-e-$index-${it.email}")
|
||||||
|
threePid(it.email)
|
||||||
|
matrixId(it.matrixId)
|
||||||
|
clickListener {
|
||||||
|
if (it.matrixId != null) {
|
||||||
|
callback?.onMatrixIdClick(it.matrixId)
|
||||||
|
} else {
|
||||||
|
callback?.onThreePidClick(ThreePid.Email(it.email))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mappedContact.msisdns
|
||||||
|
.forEachIndexed { index, it ->
|
||||||
|
if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
|
||||||
|
|
||||||
|
contactDetailItem {
|
||||||
|
id("${mappedContact.id}-m-$index-${it.phoneNumber}")
|
||||||
|
threePid(it.phoneNumber)
|
||||||
|
matrixId(it.matrixId)
|
||||||
|
clickListener {
|
||||||
|
if (it.matrixId != null) {
|
||||||
|
callback?.onMatrixIdClick(it.matrixId)
|
||||||
|
} else {
|
||||||
|
callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderEmptyState(hasSearch: Boolean) {
|
||||||
|
val noResultRes = if (hasSearch) {
|
||||||
|
R.string.no_result_placeholder
|
||||||
|
} else {
|
||||||
|
R.string.empty_contact_book
|
||||||
|
}
|
||||||
|
noResultItem {
|
||||||
|
id("noResult")
|
||||||
|
text(stringProvider.getString(noResultRes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onMatrixIdClick(matrixId: String)
|
||||||
|
fun onThreePidClick(threePid: ThreePid)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import com.jakewharton.rxbinding3.widget.checkedChanges
|
||||||
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.features.userdirectory.PendingInvitee
|
||||||
|
import im.vector.riotx.features.userdirectory.UserDirectoryAction
|
||||||
|
import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
|
||||||
|
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
||||||
|
import im.vector.riotx.features.userdirectory.UserDirectoryViewModel
|
||||||
|
import kotlinx.android.synthetic.main.fragment_contacts_book.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ContactsBookFragment @Inject constructor(
|
||||||
|
val contactsBookViewModelFactory: ContactsBookViewModel.Factory,
|
||||||
|
private val contactsBookController: ContactsBookController
|
||||||
|
) : VectorBaseFragment(), ContactsBookController.Callback {
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_contacts_book
|
||||||
|
private val viewModel: UserDirectoryViewModel by activityViewModel()
|
||||||
|
|
||||||
|
// Use activityViewModel to avoid loading several times the data
|
||||||
|
private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()
|
||||||
|
|
||||||
|
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
|
||||||
|
setupRecyclerView()
|
||||||
|
setupFilterView()
|
||||||
|
setupOnlyBoundContactsView()
|
||||||
|
setupCloseView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupOnlyBoundContactsView() {
|
||||||
|
phoneBookOnlyBoundContacts.checkedChanges()
|
||||||
|
.subscribe {
|
||||||
|
contactsBookViewModel.handle(ContactsBookAction.OnlyBoundContacts(it))
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFilterView() {
|
||||||
|
phoneBookFilter
|
||||||
|
.textChanges()
|
||||||
|
.skipInitialValue()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.subscribe {
|
||||||
|
contactsBookViewModel.handle(ContactsBookAction.FilterWith(it.toString()))
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
phoneBookRecyclerView.cleanup()
|
||||||
|
contactsBookController.callback = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
contactsBookController.callback = this
|
||||||
|
phoneBookRecyclerView.configureWith(contactsBookController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCloseView() {
|
||||||
|
phoneBookClose.debouncedClicks {
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(contactsBookViewModel) { state ->
|
||||||
|
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
|
||||||
|
contactsBookController.setData(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMatrixIdClick(matrixId: String) {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onThreePidClick(threePid: ThreePid) {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.identity.FoundThreePid
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.riotx.core.contacts.ContactsDataSource
|
||||||
|
import im.vector.riotx.core.contacts.MappedContact
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
|
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
|
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private typealias PhoneBookSearch = String
|
||||||
|
|
||||||
|
class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||||
|
initialState: ContactsBookViewState,
|
||||||
|
private val contactsDataSource: ContactsDataSource,
|
||||||
|
private val session: Session)
|
||||||
|
: VectorViewModel<ContactsBookViewState, ContactsBookAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: ContactsBookViewState): ContactsBookViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<ContactsBookViewModel, ContactsBookViewState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? {
|
||||||
|
return when (viewModelContext) {
|
||||||
|
is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state)
|
||||||
|
is ActivityViewModelContext -> {
|
||||||
|
when (viewModelContext.activity<FragmentActivity>()) {
|
||||||
|
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().contactsBookViewModelFactory.create(state)
|
||||||
|
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().contactsBookViewModelFactory.create(state)
|
||||||
|
else -> error("Wrong activity or fragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> error("Wrong activity or fragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var allContacts: List<MappedContact> = emptyList()
|
||||||
|
private var mappedContacts: List<MappedContact> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadContacts()
|
||||||
|
|
||||||
|
selectSubscribe(ContactsBookViewState::searchTerm, ContactsBookViewState::onlyBoundContacts) { _, _ ->
|
||||||
|
updateFilteredMappedContacts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadContacts() {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
mappedContacts = Loading()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
allContacts = contactsDataSource.getContacts(
|
||||||
|
withEmails = true,
|
||||||
|
// Do not handle phone numbers for the moment
|
||||||
|
withMsisdn = false
|
||||||
|
)
|
||||||
|
mappedContacts = allContacts
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
mappedContacts = Success(allContacts)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
performLookup(allContacts)
|
||||||
|
updateFilteredMappedContacts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performLookup(data: List<MappedContact>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val threePids = data.flatMap { contact ->
|
||||||
|
contact.emails.map { ThreePid.Email(it.email) } +
|
||||||
|
contact.msisdns.map { ThreePid.Msisdn(it.phoneNumber) }
|
||||||
|
}
|
||||||
|
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
// Ignore
|
||||||
|
Timber.w(failure, "Unable to perform the lookup")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: List<FoundThreePid>) {
|
||||||
|
mappedContacts = allContacts.map { contactModel ->
|
||||||
|
contactModel.copy(
|
||||||
|
emails = contactModel.emails.map { email ->
|
||||||
|
email.copy(
|
||||||
|
matrixId = data
|
||||||
|
.firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email }
|
||||||
|
?.matrixId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
msisdns = contactModel.msisdns.map { msisdn ->
|
||||||
|
msisdn.copy(
|
||||||
|
matrixId = data
|
||||||
|
.firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber }
|
||||||
|
?.matrixId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isBoundRetrieved = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilteredMappedContacts()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFilteredMappedContacts() = withState { state ->
|
||||||
|
val filteredMappedContacts = mappedContacts
|
||||||
|
.filter { it.displayName.contains(state.searchTerm, true) }
|
||||||
|
.filter { contactModel ->
|
||||||
|
!state.onlyBoundContacts
|
||||||
|
|| contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
filteredMappedContacts = filteredMappedContacts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: ContactsBookAction) {
|
||||||
|
when (action) {
|
||||||
|
is ContactsBookAction.FilterWith -> handleFilterWith(action)
|
||||||
|
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
onlyBoundContacts = action.onlyBoundContacts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFilterWith(action: ContactsBookAction.FilterWith) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
searchTerm = action.filter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.contactsbook
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.riotx.core.contacts.MappedContact
|
||||||
|
|
||||||
|
data class ContactsBookViewState(
|
||||||
|
// All the contacts on the phone
|
||||||
|
val mappedContacts: Async<List<MappedContact>> = Loading(),
|
||||||
|
// Use to filter contacts by display name
|
||||||
|
val searchTerm: String = "",
|
||||||
|
// Tru to display only bound contacts with their bound 2pid
|
||||||
|
val onlyBoundContacts: Boolean = false,
|
||||||
|
// All contacts, filtered by searchTerm and onlyBoundContacts
|
||||||
|
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||||
|
// True when the identity service has return some data
|
||||||
|
val isBoundRetrieved: Boolean = false
|
||||||
|
) : MvRxState
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.createdirect
|
package im.vector.riotx.features.createdirect
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
import im.vector.riotx.features.userdirectory.PendingInvitee
|
||||||
|
|
||||||
sealed class CreateDirectRoomAction : VectorViewModelAction {
|
sealed class CreateDirectRoomAction : VectorViewModelAction {
|
||||||
data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set<User>) : CreateDirectRoomAction()
|
data class CreateRoomAndInviteSelectedUsers(val invitees: Set<PendingInvitee>) : CreateDirectRoomAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,15 @@ import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.extensions.addFragment
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
|
||||||
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
|
||||||
|
import im.vector.riotx.core.utils.allGranted
|
||||||
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
|
import im.vector.riotx.features.contactsbook.ContactsBookFragment
|
||||||
|
import im.vector.riotx.features.contactsbook.ContactsBookViewModel
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
||||||
|
@ -53,6 +60,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||||
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
||||||
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
|
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
|
||||||
|
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
@ -68,12 +76,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
.observe()
|
.observe()
|
||||||
.subscribe { sharedAction ->
|
.subscribe { sharedAction ->
|
||||||
when (sharedAction) {
|
when (sharedAction) {
|
||||||
UserDirectorySharedAction.OpenUsersDirectory ->
|
UserDirectorySharedAction.OpenUsersDirectory ->
|
||||||
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
|
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
|
||||||
UserDirectorySharedAction.Close -> finish()
|
UserDirectorySharedAction.Close -> finish()
|
||||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||||
}
|
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
|
@ -91,9 +100,27 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openPhoneBook() {
|
||||||
|
// Check permission first
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_READ_CONTACTS,
|
||||||
|
0)) {
|
||||||
|
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
if (allGranted(grantResults)) {
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
|
||||||
|
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
|
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
|
||||||
if (action.itemId == R.id.action_create_direct_room) {
|
if (action.itemId == R.id.action_create_direct_room) {
|
||||||
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers))
|
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.invitees))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,10 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.features.userdirectory.PendingInvitee
|
||||||
|
|
||||||
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
||||||
initialState: CreateDirectRoomViewState,
|
initialState: CreateDirectRoomViewState,
|
||||||
|
@ -48,16 +49,22 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
||||||
|
|
||||||
override fun handle(action: CreateDirectRoomAction) {
|
override fun handle(action: CreateDirectRoomAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers)
|
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.invitees)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRoomAndInviteSelectedUsers(selectedUsers: Set<User>) {
|
private fun createRoomAndInviteSelectedUsers(invitees: Set<PendingInvitee>) {
|
||||||
val roomParams = CreateRoomParams(
|
val roomParams = CreateRoomParams()
|
||||||
invitedUserIds = selectedUsers.map { it.userId }
|
.apply {
|
||||||
)
|
invitees.forEach {
|
||||||
.setDirectMessage()
|
when (it) {
|
||||||
.enableEncryptionIfInvitedUsersSupportIt()
|
is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId)
|
||||||
|
is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
setDirectMessage()
|
||||||
|
enableEncryptionIfInvitedUsersSupportIt = true
|
||||||
|
}
|
||||||
|
|
||||||
session.rx()
|
session.rx()
|
||||||
.createRoom(roomParams)
|
.createRoom(roomParams)
|
||||||
|
|
|
@ -235,11 +235,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
pendingRequest = Loading()
|
pendingRequest = Loading()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val roomParams = CreateRoomParams(
|
val roomParams = CreateRoomParams()
|
||||||
invitedUserIds = listOf(otherUserId)
|
.apply {
|
||||||
)
|
invitedUserIds.add(otherUserId)
|
||||||
.setDirectMessage()
|
setDirectMessage()
|
||||||
.enableEncryptionIfInvitedUsersSupportIt()
|
enableEncryptionIfInvitedUsersSupportIt = true
|
||||||
|
}
|
||||||
|
|
||||||
session.createRoom(roomParams, object : MatrixCallback<String> {
|
session.createRoom(roomParams, object : MatrixCallback<String> {
|
||||||
override fun onSuccess(data: String) {
|
override fun onSuccess(data: String) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.util.MatrixItem
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.riotx.core.contacts.MappedContact
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.glide.GlideRequest
|
import im.vector.riotx.core.glide.GlideRequest
|
||||||
|
@ -63,6 +64,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
|
||||||
DrawableImageViewTarget(imageView))
|
DrawableImageViewTarget(imageView))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
fun render(mappedContact: MappedContact, imageView: ImageView) {
|
||||||
|
// Create a Fake MatrixItem, for the placeholder
|
||||||
|
val matrixItem = MatrixItem.UserItem(
|
||||||
|
// Need an id starting with @
|
||||||
|
id = "@${mappedContact.displayName}",
|
||||||
|
displayName = mappedContact.displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
val placeholder = getPlaceholderDrawable(imageView.context, matrixItem)
|
||||||
|
GlideApp.with(imageView)
|
||||||
|
.load(mappedContact.photoURI)
|
||||||
|
.apply(RequestOptions.circleCropTransform())
|
||||||
|
.placeholder(placeholder)
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun render(context: Context,
|
fun render(context: Context,
|
||||||
glideRequests: GlideRequests,
|
glideRequests: GlideRequests,
|
||||||
|
|
|
@ -960,7 +960,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
updateComposerText("")
|
updateComposerText("")
|
||||||
}
|
}
|
||||||
is RoomDetailViewEvents.SlashCommandResultError -> {
|
is RoomDetailViewEvents.SlashCommandResultError -> {
|
||||||
displayCommandError(sendMessageResult.throwable.localizedMessage ?: getString(R.string.unexpected_error))
|
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
||||||
}
|
}
|
||||||
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
|
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
|
||||||
displayCommandError(getString(R.string.not_implemented))
|
displayCommandError(getString(R.string.not_implemented))
|
||||||
|
|
|
@ -457,6 +457,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
handleInviteSlashCommand(slashCommandResult)
|
handleInviteSlashCommand(slashCommandResult)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
|
is ParsedCommand.Invite3Pid -> {
|
||||||
|
handleInvite3pidSlashCommand(slashCommandResult)
|
||||||
|
popDraft()
|
||||||
|
}
|
||||||
is ParsedCommand.SetUserPowerLevel -> {
|
is ParsedCommand.SetUserPowerLevel -> {
|
||||||
handleSetUserPowerLevel(slashCommandResult)
|
handleSetUserPowerLevel(slashCommandResult)
|
||||||
popDraft()
|
popDraft()
|
||||||
|
@ -678,6 +682,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
|
||||||
|
launchSlashCommandFlow {
|
||||||
|
room.invite3pid(invite.threePid, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
|
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
|
||||||
val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
|
val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
|
||||||
?.content
|
?.content
|
||||||
|
|
|
@ -50,6 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
EventType.STATE_ROOM_TOPIC,
|
EventType.STATE_ROOM_TOPIC,
|
||||||
EventType.STATE_ROOM_AVATAR,
|
EventType.STATE_ROOM_AVATAR,
|
||||||
EventType.STATE_ROOM_MEMBER,
|
EventType.STATE_ROOM_MEMBER,
|
||||||
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STATE_ROOM_ALIASES,
|
EventType.STATE_ROOM_ALIASES,
|
||||||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||||
EventType.STATE_ROOM_JOIN_RULES,
|
EventType.STATE_ROOM_JOIN_RULES,
|
||||||
|
@ -96,8 +97,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
verificationConclusionItemFactory.create(event, highlight, callback)
|
verificationConclusionItemFactory.create(event, highlight, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)
|
|
||||||
else -> {
|
else -> {
|
||||||
// Should only happen when shouldShowHiddenEvents() settings is ON
|
// Should only happen when shouldShowHiddenEvents() settings is ON
|
||||||
Timber.v("Type ${event.root.getClearType()} not handled")
|
Timber.v("Type ${event.root.getClearType()} not handled")
|
||||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.RoomJoinRules
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
|
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
|
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||||
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
||||||
|
@ -63,6 +64,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||||
|
@ -156,6 +158,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
|
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
|
||||||
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
|
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
|
||||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
|
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
|
||||||
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName)
|
||||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
|
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
|
@ -254,6 +257,31 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?): CharSequence? {
|
||||||
|
val content = event.getClearContent().toModel<RoomThirdPartyInviteContent>()
|
||||||
|
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
prevContent != null -> {
|
||||||
|
// Revoke case
|
||||||
|
if (event.isSentByCurrentUser()) {
|
||||||
|
sp.getString(R.string.notice_room_third_party_revoked_invite_by_you, prevContent.displayName)
|
||||||
|
} else {
|
||||||
|
sp.getString(R.string.notice_room_third_party_revoked_invite, senderName, prevContent.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content != null -> {
|
||||||
|
// Invitation case
|
||||||
|
if (event.isSentByCurrentUser()) {
|
||||||
|
sp.getString(R.string.notice_room_third_party_invite_by_you, content.displayName)
|
||||||
|
} else {
|
||||||
|
sp.getString(R.string.notice_room_third_party_invite, senderName, content.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatCallEvent(type: String, event: Event, senderName: String?): CharSequence? {
|
private fun formatCallEvent(type: String, event: Event, senderName: String?): CharSequence? {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
EventType.CALL_INVITE -> {
|
EventType.CALL_INVITE -> {
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.invite
|
package im.vector.riotx.features.invite
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
import im.vector.riotx.features.userdirectory.PendingInvitee
|
||||||
|
|
||||||
sealed class InviteUsersToRoomAction : VectorViewModelAction {
|
sealed class InviteUsersToRoomAction : VectorViewModelAction {
|
||||||
data class InviteSelectedUsers(val selectedUsers: Set<User>) : InviteUsersToRoomAction()
|
data class InviteSelectedUsers(val invitees: Set<PendingInvitee>) : InviteUsersToRoomAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,16 @@ import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.extensions.addFragment
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
|
||||||
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
|
||||||
|
import im.vector.riotx.core.utils.allGranted
|
||||||
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
import im.vector.riotx.core.utils.toast
|
import im.vector.riotx.core.utils.toast
|
||||||
|
import im.vector.riotx.features.contactsbook.ContactsBookFragment
|
||||||
|
import im.vector.riotx.features.contactsbook.ContactsBookViewModel
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
||||||
|
@ -53,6 +60,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
||||||
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||||
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
||||||
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
|
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
|
||||||
|
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
@ -74,7 +82,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
||||||
UserDirectorySharedAction.Close -> finish()
|
UserDirectorySharedAction.Close -> finish()
|
||||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||||
}
|
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
|
@ -92,9 +101,27 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
||||||
viewModel.observeViewEvents { renderInviteEvents(it) }
|
viewModel.observeViewEvents { renderInviteEvents(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openPhoneBook() {
|
||||||
|
// Check permission first
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_READ_CONTACTS,
|
||||||
|
0)) {
|
||||||
|
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
if (allGranted(grantResults)) {
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
|
||||||
|
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
|
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
|
||||||
if (action.itemId == R.id.action_invite_users_to_room_invite) {
|
if (action.itemId == R.id.action_invite_users_to_room_invite) {
|
||||||
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selectedUsers))
|
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,11 @@ import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.userdirectory.PendingInvitee
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
|
||||||
class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
|
class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
|
||||||
|
@ -53,27 +53,30 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
|
||||||
|
|
||||||
override fun handle(action: InviteUsersToRoomAction) {
|
override fun handle(action: InviteUsersToRoomAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selectedUsers)
|
is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun inviteUsersToRoom(selectedUsers: Set<User>) {
|
private fun inviteUsersToRoom(invitees: Set<PendingInvitee>) {
|
||||||
_viewEvents.post(InviteUsersToRoomViewEvents.Loading)
|
_viewEvents.post(InviteUsersToRoomViewEvents.Loading)
|
||||||
|
|
||||||
Observable.fromIterable(selectedUsers).flatMapCompletable { user ->
|
Observable.fromIterable(invitees).flatMapCompletable { user ->
|
||||||
room.rx().invite(user.userId, null)
|
when (user) {
|
||||||
|
is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null)
|
||||||
|
is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid)
|
||||||
|
}
|
||||||
}.subscribe(
|
}.subscribe(
|
||||||
{
|
{
|
||||||
val successMessage = when (selectedUsers.size) {
|
val successMessage = when (invitees.size) {
|
||||||
1 -> stringProvider.getString(R.string.invitation_sent_to_one_user,
|
1 -> stringProvider.getString(R.string.invitation_sent_to_one_user,
|
||||||
selectedUsers.first().getBestName())
|
invitees.first().getBestName())
|
||||||
2 -> stringProvider.getString(R.string.invitations_sent_to_two_users,
|
2 -> stringProvider.getString(R.string.invitations_sent_to_two_users,
|
||||||
selectedUsers.first().getBestName(),
|
invitees.first().getBestName(),
|
||||||
selectedUsers.last().getBestName())
|
invitees.last().getBestName())
|
||||||
else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
|
else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
|
||||||
selectedUsers.size - 1,
|
invitees.size - 1,
|
||||||
selectedUsers.first().getBestName(),
|
invitees.first().getBestName(),
|
||||||
selectedUsers.size - 1)
|
invitees.size - 1)
|
||||||
}
|
}
|
||||||
_viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage))
|
_viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage))
|
||||||
},
|
},
|
||||||
|
|
|
@ -84,15 +84,19 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
|
||||||
copy(asyncCreateRoomRequest = Loading())
|
copy(asyncCreateRoomRequest = Loading())
|
||||||
}
|
}
|
||||||
|
|
||||||
val createRoomParams = CreateRoomParams(
|
val createRoomParams = CreateRoomParams()
|
||||||
name = state.roomName.takeIf { it.isNotBlank() },
|
.apply {
|
||||||
// Directory visibility
|
name = state.roomName.takeIf { it.isNotBlank() }
|
||||||
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE,
|
// Directory visibility
|
||||||
// Public room
|
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE
|
||||||
preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
|
// Public room
|
||||||
)
|
preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
|
||||||
// Encryption
|
|
||||||
.enableEncryptionWithAlgorithm(state.isEncrypted)
|
// Encryption
|
||||||
|
if (state.isEncrypted) {
|
||||||
|
enableEncryption()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
session.createRoom(createRoomParams, object : MatrixCallback<String> {
|
session.createRoom(createRoomParams, object : MatrixCallback<String> {
|
||||||
override fun onSuccess(data: String) {
|
override fun onSuccess(data: String) {
|
||||||
|
|
|
@ -18,4 +18,6 @@ package im.vector.riotx.features.roomprofile.members
|
||||||
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class RoomMemberListAction : VectorViewModelAction
|
sealed class RoomMemberListAction : VectorViewModelAction {
|
||||||
|
data class RevokeThreePidInvite(val stateKey: String) : RoomMemberListAction()
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,11 @@
|
||||||
package im.vector.riotx.features.roomprofile.members
|
package im.vector.riotx.features.roomprofile.members
|
||||||
|
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.dividerItem
|
import im.vector.riotx.core.epoxy.dividerItem
|
||||||
|
@ -37,6 +41,7 @@ class RoomMemberListController @Inject constructor(
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onRoomMemberClicked(roomMember: RoomMemberSummary)
|
fun onRoomMemberClicked(roomMember: RoomMemberSummary)
|
||||||
|
fun onThreePidInvites(event: Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
|
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
|
||||||
|
@ -49,15 +54,29 @@ class RoomMemberListController @Inject constructor(
|
||||||
|
|
||||||
override fun buildModels(data: RoomMemberListViewState?) {
|
override fun buildModels(data: RoomMemberListViewState?) {
|
||||||
val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return
|
val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return
|
||||||
|
val threePidInvites = data.threePidInvites().orEmpty()
|
||||||
|
var threePidInvitesDone = threePidInvites.isEmpty()
|
||||||
|
|
||||||
for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
|
for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
|
||||||
if (roomMemberList.isEmpty()) {
|
if (roomMemberList.isEmpty()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) {
|
||||||
|
// If there is not regular invite, display threepid invite before the regular user
|
||||||
|
buildProfileSection(
|
||||||
|
stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
|
||||||
|
)
|
||||||
|
|
||||||
|
buildThreePidInvites(data)
|
||||||
|
threePidInvitesDone = true
|
||||||
|
}
|
||||||
|
|
||||||
buildProfileSection(
|
buildProfileSection(
|
||||||
stringProvider.getString(powerLevelCategory.titleRes)
|
stringProvider.getString(powerLevelCategory.titleRes)
|
||||||
)
|
)
|
||||||
roomMemberList.join(
|
roomMemberList.join(
|
||||||
each = { roomMember ->
|
each = { _, roomMember ->
|
||||||
profileMatrixItem {
|
profileMatrixItem {
|
||||||
id(roomMember.userId)
|
id(roomMember.userId)
|
||||||
matrixItem(roomMember.toMatrixItem())
|
matrixItem(roomMember.toMatrixItem())
|
||||||
|
@ -68,13 +87,62 @@ class RoomMemberListController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
between = { roomMemberBefore ->
|
between = { _, roomMemberBefore ->
|
||||||
dividerItem {
|
dividerItem {
|
||||||
id("divider_${roomMemberBefore.userId}")
|
id("divider_${roomMemberBefore.userId}")
|
||||||
color(dividerColor)
|
color(dividerColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (powerLevelCategory == RoomMemberListCategories.INVITE) {
|
||||||
|
// Display the threepid invite after the regular invite
|
||||||
|
dividerItem {
|
||||||
|
id("divider_threepidinvites")
|
||||||
|
color(dividerColor)
|
||||||
|
}
|
||||||
|
buildThreePidInvites(data)
|
||||||
|
threePidInvitesDone = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!threePidInvitesDone) {
|
||||||
|
// If there is not regular invite and no regular user, finally display threepid invite here
|
||||||
|
buildProfileSection(
|
||||||
|
stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
|
||||||
|
)
|
||||||
|
|
||||||
|
buildThreePidInvites(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildThreePidInvites(data: RoomMemberListViewState) {
|
||||||
|
data.threePidInvites()
|
||||||
|
?.filter { it.content.toModel<RoomThirdPartyInviteContent>() != null }
|
||||||
|
?.join(
|
||||||
|
each = { idx, event ->
|
||||||
|
event.content.toModel<RoomThirdPartyInviteContent>()
|
||||||
|
?.let { content ->
|
||||||
|
profileMatrixItem {
|
||||||
|
id("3pid_$idx")
|
||||||
|
matrixItem(content.toMatrixItem())
|
||||||
|
avatarRenderer(avatarRenderer)
|
||||||
|
editable(data.actionsPermissions.canRevokeThreePidInvite)
|
||||||
|
clickListener { _ ->
|
||||||
|
callback?.onThreePidInvites(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
between = { idx, _ ->
|
||||||
|
dividerItem {
|
||||||
|
id("divider3_$idx")
|
||||||
|
color(dividerColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomThirdPartyInviteContent.toMatrixItem(): MatrixItem {
|
||||||
|
return MatrixItem.UserItem("@", displayName = displayName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,14 @@ import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.cleanup
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
@ -88,6 +92,22 @@ class RoomMemberListFragment @Inject constructor(
|
||||||
navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity())
|
navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onThreePidInvites(event: Event) {
|
||||||
|
// Display a dialog to revoke invite if power level is high enough
|
||||||
|
val content = event.content.toModel<RoomThirdPartyInviteContent>() ?: return
|
||||||
|
val stateKey = event.stateKey ?: return
|
||||||
|
if (withState(viewModel) { it.actionsPermissions.canRevokeThreePidInvite }) {
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setTitle(R.string.three_pid_revoke_invite_dialog_title)
|
||||||
|
.setMessage(getString(R.string.three_pid_revoke_invite_dialog_content, content.displayName))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.revoke) { _, _ ->
|
||||||
|
viewModel.handle(RoomMemberListAction.RevokeThreePidInvite(stateKey))
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderRoomSummary(state: RoomMemberListViewState) {
|
private fun renderRoomSummary(state: RoomMemberListViewState) {
|
||||||
state.roomSummary()?.let {
|
state.roomSummary()?.let {
|
||||||
roomSettingsToolbarTitleView.text = it.displayName
|
roomSettingsToolbarTitleView.text = it.displayName
|
||||||
|
|
|
@ -16,11 +16,13 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.roomprofile.members
|
package im.vector.riotx.features.roomprofile.members
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||||
import im.vector.matrix.android.api.extensions.orFalse
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
import im.vector.matrix.android.api.query.QueryStringValue
|
import im.vector.matrix.android.api.query.QueryStringValue
|
||||||
|
@ -37,12 +39,14 @@ import im.vector.matrix.rx.asObservable
|
||||||
import im.vector.matrix.rx.mapOptional
|
import im.vector.matrix.rx.mapOptional
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.matrix.rx.unwrap
|
import im.vector.matrix.rx.unwrap
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
|
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
|
class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
|
||||||
|
@ -68,6 +72,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeRoomMemberSummaries()
|
observeRoomMemberSummaries()
|
||||||
|
observeThirdPartyInvites()
|
||||||
observeRoomSummary()
|
observeRoomSummary()
|
||||||
observePowerLevel()
|
observePowerLevel()
|
||||||
}
|
}
|
||||||
|
@ -124,7 +129,12 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
||||||
PowerLevelsObservableFactory(room).createObservable()
|
PowerLevelsObservableFactory(room).createObservable()
|
||||||
.subscribe {
|
.subscribe {
|
||||||
val permissions = ActionPermissions(
|
val permissions = ActionPermissions(
|
||||||
canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
|
canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId),
|
||||||
|
canRevokeThreePidInvite = PowerLevelsHelper(it).isUserAllowedToSend(
|
||||||
|
userId = session.myUserId,
|
||||||
|
isState = true,
|
||||||
|
eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE
|
||||||
|
)
|
||||||
)
|
)
|
||||||
setState {
|
setState {
|
||||||
copy(actionsPermissions = permissions)
|
copy(actionsPermissions = permissions)
|
||||||
|
@ -140,6 +150,13 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeThirdPartyInvites() {
|
||||||
|
room.rx().liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE))
|
||||||
|
.execute { async ->
|
||||||
|
copy(threePidInvites = async)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildRoomMemberSummaries(powerLevelsContent: PowerLevelsContent, roomMembers: List<RoomMemberSummary>): RoomMemberSummaries {
|
private fun buildRoomMemberSummaries(powerLevelsContent: PowerLevelsContent, roomMembers: List<RoomMemberSummary>): RoomMemberSummaries {
|
||||||
val admins = ArrayList<RoomMemberSummary>()
|
val admins = ArrayList<RoomMemberSummary>()
|
||||||
val moderators = ArrayList<RoomMemberSummary>()
|
val moderators = ArrayList<RoomMemberSummary>()
|
||||||
|
@ -169,5 +186,19 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: RoomMemberListAction) {
|
override fun handle(action: RoomMemberListAction) {
|
||||||
|
when (action) {
|
||||||
|
is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRevokeThreePidInvite(action: RoomMemberListAction.RevokeThreePidInvite) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
room.sendStateEvent(
|
||||||
|
eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
stateKey = action.stateKey,
|
||||||
|
body = emptyMap(),
|
||||||
|
callback = NoOpMatrixCallback()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
@ -30,6 +31,7 @@ data class RoomMemberListViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
|
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
|
||||||
|
val threePidInvites: Async<List<Event>> = Uninitialized,
|
||||||
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
|
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
|
||||||
val actionsPermissions: ActionPermissions = ActionPermissions()
|
val actionsPermissions: ActionPermissions = ActionPermissions()
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
@ -38,7 +40,8 @@ data class RoomMemberListViewState(
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ActionPermissions(
|
data class ActionPermissions(
|
||||||
val canInvite: Boolean = false
|
val canInvite: Boolean = false,
|
||||||
|
val canRevokeThreePidInvite: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
typealias RoomMemberSummaries = List<Pair<RoomMemberListCategories, List<RoomMemberSummary>>>
|
typealias RoomMemberSummaries = List<Pair<RoomMemberListCategories, List<RoomMemberSummary>>>
|
||||||
|
|
|
@ -60,7 +60,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
|
||||||
is Loading -> renderLoading()
|
is Loading -> renderLoading()
|
||||||
is Success -> renderSuccess(
|
is Success -> renderSuccess(
|
||||||
computeUsersList(asyncUsers(), currentState.directorySearchTerm),
|
computeUsersList(asyncUsers(), currentState.directorySearchTerm),
|
||||||
currentState.selectedUsers.map { it.userId },
|
currentState.getSelectedMatrixId(),
|
||||||
hasSearch
|
hasSearch
|
||||||
)
|
)
|
||||||
is Fail -> renderFailure(asyncUsers.error)
|
is Fail -> renderFailure(asyncUsers.error)
|
||||||
|
|
|
@ -51,7 +51,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
|
||||||
|
|
||||||
fun setData(state: UserDirectoryViewState) {
|
fun setData(state: UserDirectoryViewState) {
|
||||||
this.isFiltering = !state.filterKnownUsersValue.isEmpty()
|
this.isFiltering = !state.filterKnownUsersValue.isEmpty()
|
||||||
val newSelection = state.selectedUsers.map { it.userId }
|
val newSelection = state.getSelectedMatrixId()
|
||||||
this.users = state.knownUsers
|
this.users = state.knownUsers
|
||||||
if (newSelection != selectedUsers) {
|
if (newSelection != selectedUsers) {
|
||||||
this.selectedUsers = newSelection
|
this.selectedUsers = newSelection
|
||||||
|
|
|
@ -63,8 +63,9 @@ class KnownUsersFragment @Inject constructor(
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupFilterView()
|
setupFilterView()
|
||||||
setupAddByMatrixIdView()
|
setupAddByMatrixIdView()
|
||||||
|
setupAddFromPhoneBookView()
|
||||||
setupCloseView()
|
setupCloseView()
|
||||||
viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
|
viewModel.selectSubscribe(this, UserDirectoryViewState::pendingInvitees) {
|
||||||
renderSelectedUsers(it)
|
renderSelectedUsers(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +78,7 @@ class KnownUsersFragment @Inject constructor(
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
withState(viewModel) {
|
withState(viewModel) {
|
||||||
val showMenuItem = it.selectedUsers.isNotEmpty()
|
val showMenuItem = it.pendingInvitees.isNotEmpty()
|
||||||
menu.forEach { menuItem ->
|
menu.forEach { menuItem ->
|
||||||
menuItem.isVisible = showMenuItem
|
menuItem.isVisible = showMenuItem
|
||||||
}
|
}
|
||||||
|
@ -86,7 +87,7 @@ class KnownUsersFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
|
||||||
sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.selectedUsers))
|
sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
|
||||||
return@withState true
|
return@withState true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +97,13 @@ class KnownUsersFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupAddFromPhoneBookView() {
|
||||||
|
addFromPhoneBook.debouncedClicks {
|
||||||
|
// TODO handle Permission first
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
knownUsersController.callback = this
|
knownUsersController.callback = this
|
||||||
// Don't activate animation as we might have way to much item animation when filtering
|
// Don't activate animation as we might have way to much item animation when filtering
|
||||||
|
@ -131,14 +139,14 @@ class KnownUsersFragment @Inject constructor(
|
||||||
knownUsersController.setData(it)
|
knownUsersController.setData(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderSelectedUsers(selectedUsers: Set<User>) {
|
private fun renderSelectedUsers(invitees: Set<PendingInvitee>) {
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
val currentNumberOfChips = chipGroup.childCount
|
val currentNumberOfChips = chipGroup.childCount
|
||||||
val newNumberOfChips = selectedUsers.size
|
val newNumberOfChips = invitees.size
|
||||||
|
|
||||||
chipGroup.removeAllViews()
|
chipGroup.removeAllViews()
|
||||||
selectedUsers.forEach { addChipToGroup(it) }
|
invitees.forEach { addChipToGroup(it) }
|
||||||
|
|
||||||
// Scroll to the bottom when adding chips. When removing chips, do not scroll
|
// Scroll to the bottom when adding chips. When removing chips, do not scroll
|
||||||
if (newNumberOfChips >= currentNumberOfChips) {
|
if (newNumberOfChips >= currentNumberOfChips) {
|
||||||
|
@ -148,22 +156,22 @@ class KnownUsersFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addChipToGroup(user: User) {
|
private fun addChipToGroup(pendingInvitee: PendingInvitee) {
|
||||||
val chip = Chip(requireContext())
|
val chip = Chip(requireContext())
|
||||||
chip.setChipBackgroundColorResource(android.R.color.transparent)
|
chip.setChipBackgroundColorResource(android.R.color.transparent)
|
||||||
chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
|
chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
|
||||||
chip.text = user.getBestName()
|
chip.text = pendingInvitee.getBestName()
|
||||||
chip.isClickable = true
|
chip.isClickable = true
|
||||||
chip.isCheckable = false
|
chip.isCheckable = false
|
||||||
chip.isCloseIconVisible = true
|
chip.isCloseIconVisible = true
|
||||||
chipGroup.addView(chip)
|
chipGroup.addView(chip)
|
||||||
chip.setOnCloseIconClickListener {
|
chip.setOnCloseIconClickListener {
|
||||||
viewModel.handle(UserDirectoryAction.RemoveSelectedUser(user))
|
viewModel.handle(UserDirectoryAction.RemovePendingInvitee(pendingInvitee))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(user: User) {
|
override fun onItemClick(user: User) {
|
||||||
view?.hideKeyboard()
|
view?.hideKeyboard()
|
||||||
viewModel.handle(UserDirectoryAction.SelectUser(user))
|
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
|
||||||
|
sealed class PendingInvitee {
|
||||||
|
data class UserPendingInvitee(val user: User) : PendingInvitee()
|
||||||
|
data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee()
|
||||||
|
|
||||||
|
fun getBestName(): String {
|
||||||
|
return when (this) {
|
||||||
|
is UserPendingInvitee -> user.getBestName()
|
||||||
|
is ThreePidPendingInvitee -> threePid.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,13 +16,12 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.userdirectory
|
package im.vector.riotx.features.userdirectory
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class UserDirectoryAction : VectorViewModelAction {
|
sealed class UserDirectoryAction : VectorViewModelAction {
|
||||||
data class FilterKnownUsers(val value: String) : UserDirectoryAction()
|
data class FilterKnownUsers(val value: String) : UserDirectoryAction()
|
||||||
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
|
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
|
||||||
object ClearFilterKnownUsers : UserDirectoryAction()
|
object ClearFilterKnownUsers : UserDirectoryAction()
|
||||||
data class SelectUser(val user: User) : UserDirectoryAction()
|
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
|
||||||
data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
|
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ class UserDirectoryFragment @Inject constructor(
|
||||||
|
|
||||||
override fun onItemClick(user: User) {
|
override fun onItemClick(user: User) {
|
||||||
view?.hideKeyboard()
|
view?.hideKeyboard()
|
||||||
viewModel.handle(UserDirectoryAction.SelectUser(user))
|
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
|
||||||
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.userdirectory
|
package im.vector.riotx.features.userdirectory
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
import im.vector.riotx.core.platform.VectorSharedAction
|
import im.vector.riotx.core.platform.VectorSharedAction
|
||||||
|
|
||||||
sealed class UserDirectorySharedAction : VectorSharedAction {
|
sealed class UserDirectorySharedAction : VectorSharedAction {
|
||||||
object OpenUsersDirectory : UserDirectorySharedAction()
|
object OpenUsersDirectory : UserDirectorySharedAction()
|
||||||
|
object OpenPhoneBook : UserDirectorySharedAction()
|
||||||
object Close : UserDirectorySharedAction()
|
object Close : UserDirectorySharedAction()
|
||||||
object GoBack : UserDirectorySharedAction()
|
object GoBack : UserDirectorySharedAction()
|
||||||
data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set<User>) : UserDirectorySharedAction()
|
data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserDirectorySharedAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.extensions.toggle
|
import im.vector.riotx.core.extensions.toggle
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
|
@ -59,9 +60,9 @@ class UserDirectoryViewModel @AssistedInject constructor(@Assisted
|
||||||
is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
|
is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
|
||||||
is ActivityViewModelContext -> {
|
is ActivityViewModelContext -> {
|
||||||
when (viewModelContext.activity<FragmentActivity>()) {
|
when (viewModelContext.activity<FragmentActivity>()) {
|
||||||
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
|
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
|
||||||
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().userDirectoryViewModelFactory.create(state)
|
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().userDirectoryViewModelFactory.create(state)
|
||||||
else -> error("Wrong activity or fragment")
|
else -> error("Wrong activity or fragment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> error("Wrong activity or fragment")
|
else -> error("Wrong activity or fragment")
|
||||||
|
@ -79,21 +80,21 @@ class UserDirectoryViewModel @AssistedInject constructor(@Assisted
|
||||||
is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
|
is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
|
||||||
is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
|
is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
|
||||||
is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
|
is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
|
||||||
is UserDirectoryAction.SelectUser -> handleSelectUser(action)
|
is UserDirectoryAction.SelectPendingInvitee -> handleSelectUser(action)
|
||||||
is UserDirectoryAction.RemoveSelectedUser -> handleRemoveSelectedUser(action)
|
is UserDirectoryAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
|
||||||
}
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemoveSelectedUser) = withState { state ->
|
private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state ->
|
||||||
val selectedUsers = state.selectedUsers.minus(action.user)
|
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
|
||||||
setState { copy(selectedUsers = selectedUsers) }
|
setState { copy(pendingInvitees = selectedUsers) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSelectUser(action: UserDirectoryAction.SelectUser) = withState { state ->
|
private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state ->
|
||||||
// Reset the filter asap
|
// Reset the filter asap
|
||||||
directoryUsersSearch.accept("")
|
directoryUsersSearch.accept("")
|
||||||
val selectedUsers = state.selectedUsers.toggle(action.user)
|
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
|
||||||
setState { copy(selectedUsers = selectedUsers) }
|
setState { copy(pendingInvitees = selectedUsers) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeDirectoryUsers() = withState { state ->
|
private fun observeDirectoryUsers() = withState { state ->
|
||||||
|
|
|
@ -27,11 +27,21 @@ data class UserDirectoryViewState(
|
||||||
val excludedUserIds: Set<String>? = null,
|
val excludedUserIds: Set<String>? = null,
|
||||||
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
||||||
val directoryUsers: Async<List<User>> = Uninitialized,
|
val directoryUsers: Async<List<User>> = Uninitialized,
|
||||||
val selectedUsers: Set<User> = emptySet(),
|
val pendingInvitees: Set<PendingInvitee> = emptySet(),
|
||||||
val createAndInviteState: Async<String> = Uninitialized,
|
val createAndInviteState: Async<String> = Uninitialized,
|
||||||
val directorySearchTerm: String = "",
|
val directorySearchTerm: String = "",
|
||||||
val filterKnownUsersValue: Option<String> = Option.empty()
|
val filterKnownUsersValue: Option<String> = Option.empty()
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds)
|
constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds)
|
||||||
|
|
||||||
|
fun getSelectedMatrixId(): List<String> {
|
||||||
|
return pendingInvitees
|
||||||
|
.mapNotNull {
|
||||||
|
when (it) {
|
||||||
|
is PendingInvitee.UserPendingInvitee -> it.user.userId
|
||||||
|
is PendingInvitee.ThreePidPendingInvitee -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/phoneBookToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
android:elevation="4dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/phoneBookClose"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_x_18dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/phoneBookTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/contacts_book_title"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/phoneBookClose"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/phoneBookFilterContainer"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookToolbar">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/phoneBookFilter"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/search" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/phoneBookOnlyBoundContacts"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
android:text="@string/matrix_only_filter"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/phoneBookFilterDivider"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/vctr_list_divider_color"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/phoneBookRecyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
android:overScrollMode="always"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterDivider"
|
||||||
|
tools:listitem="@layout/item_contact_main" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -123,6 +123,23 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider" />
|
app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/addFromPhoneBook"
|
||||||
|
style="@style/VectorButtonStyleText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:minHeight="@dimen/layout_touch_size"
|
||||||
|
android:text="@string/search_in_my_contacts"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:icon="@drawable/ic_plus_circle"
|
||||||
|
app:iconPadding="13dp"
|
||||||
|
app:iconTint="@color/riotx_accent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/addByMatrixId" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -134,7 +151,7 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/addByMatrixId"
|
app:layout_constraintTop_toBottomOf="@+id/addFromPhoneBook"
|
||||||
tools:listitem="@layout/item_known_user" />
|
tools:listitem="@layout/item_known_user" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:minHeight="60dp"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/contactDetailName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/contactDetailMatrixId"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/contactDetailMatrixId"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/contactDetailName"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/contactDetailName"
|
||||||
|
tools:text="@sample/matrix.json/data/mxid"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingEnd="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/contactAvatar"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/contactDisplayName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/contactAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -2541,4 +2541,15 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
|
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
|
||||||
|
|
||||||
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
|
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
|
||||||
|
|
||||||
|
<string name="add_from_phone_book">Add from my phone book</string>
|
||||||
|
<string name="empty_phone_book">Your phone book is empty</string>
|
||||||
|
<string name="phone_book_title">Phone book</string>
|
||||||
|
<string name="search_in_my_contacts">Search in my contacts</string>
|
||||||
|
<string name="loading_contact_book">Retrieving your contacts…</string>
|
||||||
|
<string name="empty_contact_book">Your contact book is empty</string>
|
||||||
|
<string name="contacts_book_title">Contacts book</string>
|
||||||
|
|
||||||
|
<string name="three_pid_revoke_invite_dialog_title">Revoke invite</string>
|
||||||
|
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue