Merge branch 'develop' into feature/room_update

This commit is contained in:
ganfra 2019-07-30 15:42:46 +02:00
commit f4df27c2dc
92 changed files with 2558 additions and 278 deletions

View File

@ -2,7 +2,7 @@ Changes in RiotX 0.3.0 (2019-XX-XX)
===================================================

Features:
-
- Create Direct Room flow

Improvements:
- UI for pending edits (#193)

View File

@ -38,6 +38,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0"

testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.reactivex.Observable
import io.reactivex.android.MainThreadDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers

private class LiveDataObservable<T>(
private val liveData: LiveData<T>,
@ -57,5 +59,5 @@ private class LiveDataObservable<T>(
}

fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this)
return LiveDataObservable(this).observeOn(Schedulers.computation())
}

View File

@ -0,0 +1,39 @@
/*
* 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.rx

import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.CompletableEmitter
import io.reactivex.SingleEmitter

internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {

override fun onSuccess(data: T) {
completableEmitter.onComplete()
}

override fun onFailure(failure: Throwable) {
completableEmitter.onError(failure)
}
}

fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
completableEmitter.setCancellable {
this.cancel()
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.rx

import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.SingleEmitter

internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> {

override fun onSuccess(data: T) {
singleEmitter.onSuccess(data)
}

override fun onFailure(failure: Throwable) {
singleEmitter.onError(failure)
}
}

fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
singleEmitter.setCancellable {
this.cancel()
}
}

View File

@ -21,24 +21,28 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import io.reactivex.Single

class RxRoom(private val room: Room) {

fun liveRoomSummary(): Observable<RoomSummary> {
return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation())
return room.liveRoomSummary().asObservable()
}

fun liveRoomMemberIds(): Observable<List<String>> {
return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation())
return room.getRoomMemberIdsLive().asObservable()
}

fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation())
return room.getEventSummaryLive(eventId).asObservable()
}

fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation())
return room.liveTimeLineEvent(eventId).asObservable()
}

fun loadRoomMembersIfNeeded(): Single<Boolean> = Single.create {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
}

}

View File

@ -16,30 +16,51 @@

package im.vector.matrix.rx

import androidx.paging.PagedList
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import io.reactivex.Single

class RxSession(private val session: Session) {

fun liveRoomSummaries(): Observable<List<RoomSummary>> {
return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation())
return session.liveRoomSummaries().asObservable()
}

fun liveGroupSummaries(): Observable<List<GroupSummary>> {
return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation())
return session.liveGroupSummaries().asObservable()
}

fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable().observeOn(Schedulers.computation())
return session.syncState().asObservable()
}

fun livePushers(): Observable<List<Pusher>> {
return session.livePushers().asObservable().observeOn(Schedulers.computation())
return session.livePushers().asObservable()
}

fun liveUsers(): Observable<List<User>> {
return session.liveUsers().asObservable()
}

fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
return session.livePagedUsers(filter).asObservable()
}

fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
}

fun searchUsersDirectory(search: String,
limit: Int,
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
}

}

View File

@ -28,7 +28,7 @@ object MatrixPatterns {
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)

// regex pattern to find room ids in a string.
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"

View File

@ -31,7 +31,7 @@ import okhttp3.TlsVersion
@JsonClass(generateAdapter = true)
data class HomeServerConnectionConfig(
val homeServerUri: Uri,
val identityServerUri: Uri,
val identityServerUri: Uri? = null,
val antiVirusServerUri: Uri? = null,
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
val shouldPin: Boolean = false,
@ -48,7 +48,7 @@ data class HomeServerConnectionConfig(
class Builder {

private lateinit var homeServerUri: Uri
private lateinit var identityServerUri: Uri
private var identityServerUri: Uri? = null
private var antiVirusServerUri: Uri? = null
private val allowedFingerprints: MutableList<Fingerprint> = ArrayList()
private var shouldPin: Boolean = false

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable

interface PushRuleService {

@ -31,7 +32,7 @@ interface PushRuleService {

//TODO update rule

fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>)
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable

fun addPushRuleListener(listener: PushRuleListener)


View File

@ -30,15 +30,17 @@ interface RoomDirectoryService {
/**
* Get rooms from directory
*/
fun getPublicRooms(server: String?,
publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable

/**
* Join a room by id
*/
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable

/**
* Fetches the overall metadata about protocols supported by the homeserver.
* Includes both the available protocols and all fields required for queries against each protocol.
*/
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>)
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable

}

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable

/**
* This interface defines methods to get rooms. It's implemented at the session level.
@ -27,10 +28,9 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
interface RoomService {

/**
* Create a room
* Create a room asynchronously
*/
fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback<String>)
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable

/**
* Join a room by id

View File

@ -30,7 +30,7 @@ interface MembershipService {
* This methods load all room members if it was done yet.
* @return a [Cancelable]
*/
fun loadRoomMembersIfNeeded(): Cancelable
fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Boolean>): Cancelable

/**
* Return the roomMember with userId or null.
@ -52,16 +52,17 @@ interface MembershipService {
/**
* Invite a user in the room
*/
fun invite(userId: String, callback: MatrixCallback<Unit>)
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable

/**
* Join the room, or accept an invitation.
*/
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>)

fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable

/**
* Leave the room, or reject an invitation.
*/
fun leave(callback: MatrixCallback<Unit>)
fun leave(callback: MatrixCallback<Unit>): Cancelable

}

View File

@ -128,8 +128,8 @@ class CreateRoomParams {
contentMap["algorithm"] = algorithm

val algoEvent = Event(type = EventType.ENCRYPTION,
stateKey = "",
content = contentMap.toContent()
stateKey = "",
content = contentMap.toContent()
)

if (null == initialStates) {
@ -162,8 +162,8 @@ class CreateRoomParams {
contentMap["history_visibility"] = historyVisibility

val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
stateKey = "",
content = contentMap.toContent())
stateKey = "",
content = contentMap.toContent())

if (null == initialStates) {
initialStates = Arrays.asList<Event>(historyVisibilityEvent)
@ -202,8 +202,8 @@ class CreateRoomParams {
*/
fun isDirect(): Boolean {
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
&& isDirect == true
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
&& isDirect == true
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
}

/**
@ -223,14 +223,13 @@ class CreateRoomParams {
credentials: Credentials,
ids: List<String>) {
for (id in ids) {
if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) {
if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) {
if (null == invite3pids) {
invite3pids = ArrayList()
}

val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!,
medium = ThreePidMedium.EMAIL,
address = id)
medium = ThreePidMedium.EMAIL,
address = id)

invite3pids!!.add(pid)
} else if (isUserId(id)) {

View File

@ -17,7 +17,10 @@
package im.vector.matrix.android.api.session.user

import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable

/**
* This interface defines methods to get users. It's implemented at the session level.
@ -31,11 +34,34 @@ interface UserService {
*/
fun getUser(userId: String): User?

/**
* Search list of users on server directory.
* @param search the searched term
* @param limit the max number of users to return
* @param excludedUserIds the user ids to filter from the search
* @param callback the async callback
* @return Cancelable
*/
fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set<String>, callback: MatrixCallback<List<User>>): Cancelable

/**
* Observe a live user from a userId
* @param userId the userId to look for.
* @return a Livedata of user with userId
*/
fun observeUser(userId: String): LiveData<User?>
fun liveUser(userId: String): LiveData<User?>

/**
* Observe a live list of users sorted alphabetically
* @return a Livedata of users
*/
fun liveUsers(): LiveData<List<User>>

/**
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
* @param filter the filter. It will look into userId and displayName.
* @return a Livedata of users
*/
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>>

}

View File

@ -19,14 +19,17 @@ package im.vector.matrix.android.internal.database
import android.os.Handler
import android.os.HandlerThread
import io.realm.*
import timber.log.Timber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

private const val THREAD_NAME = "REALM_QUERY_LATCH"

class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {

fun await() {
@Throws(InterruptedException::class)
fun await(timeout: Long = Long.MAX_VALUE, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) {
val latch = CountDownLatch(1)
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
handlerThread.start()
@ -46,8 +49,13 @@ class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConf
})
}
handler.post(runnable)
latch.await()
handlerThread.quit()
try {
latch.await(timeout, timeUnit)
} catch (exception: InterruptedException) {
throw exception
} finally {
handlerThread.quit()
}
}



View File

@ -33,6 +33,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false,
var directUserId: String? = null,
var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.kotlin.where

internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
@ -29,3 +30,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
}
return query
}

internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.findAll()
}

internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): Boolean {
return RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.findAll()
.isNotEmpty()
}




View File

@ -65,11 +65,12 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm:

internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
roomId: String,
includesSending: Boolean,
includedTypes: List<String> = emptyList(),
excludedTypes: List<String> = emptyList()): TimelineEventEntity? {

val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val eventList = if (roomEntity.sendingTimelineEvents.isNotEmpty()) {
val eventList = if (includesSending && roomEntity.sendingTimelineEvents.isNotEmpty()) {
roomEntity.sendingTimelineEvents
} else {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents

View File

@ -19,11 +19,11 @@ package im.vector.matrix.android.internal.session
import dagger.BindsInstance
import dagger.Component
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.di.MatrixComponent
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.session.cache.CacheModule
import im.vector.matrix.android.internal.session.content.ContentModule
import im.vector.matrix.android.internal.session.content.UploadContentWorker
@ -46,20 +46,21 @@ import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.task.TaskExecutor

@Component(dependencies = [MatrixComponent::class],
modules = [
SessionModule::class,
RoomModule::class,
SyncModule::class,
SignOutModule::class,
GroupModule::class,
UserModule::class,
FilterModule::class,
GroupModule::class,
ContentModule::class,
CacheModule::class,
CryptoModule::class,
PushersModule::class
]
modules = [
SessionModule::class,
RoomModule::class,
SyncModule::class,
SignOutModule::class,
GroupModule::class,
UserModule::class,
FilterModule::class,
GroupModule::class,
ContentModule::class,
CacheModule::class,
CryptoModule::class,
PushersModule::class,
AccountDataModule::class
]
)
@SessionScope
internal interface SessionComponent {

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.query.where
@ -80,8 +81,8 @@ internal class DefaultPushRuleService @Inject constructor(
return contentRules + overrideRules + roomRules + senderRules + underrideRules
}

override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>) {
updatePushRuleEnableStatusTask
override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return updatePushRuleEnableStatusTask
.configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled))
// TODO Fetch the rules
.dispatchTo(callback)

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.task.toConfigurableTask
import javax.inject.Inject

internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
private val joinRoomTask: JoinRoomTask,
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
private val taskExecutor: TaskExecutor) : RoomDirectoryService {

@ -43,8 +44,15 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
.executeBy(taskExecutor)
}

override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>) {
getThirdPartyProtocolsTask
override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId))
.dispatchTo(callback)
.executeBy(taskExecutor)
}

override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
return getThirdPartyProtocolsTask
.toConfigurableTask()
.dispatchTo(callback)
.executeBy(taskExecutor)

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -43,8 +44,8 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService {

override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>) {
createRoomTask
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
return createRoomTask
.configureWith(createRoomParams)
.dispatchTo(callback)
.executeBy(taskExecutor)

View File

@ -62,7 +62,9 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomId: String,
membership: Membership? = null,
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null) {
unreadNotifications: RoomSyncUnreadNotifications? = null,
isDirect: Boolean? = null,
directUserId: String? = null) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)

@ -85,7 +87,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomSummaryEntity.membership = membership
}

val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()

val otherRoomMembers = RoomMembers(realm, roomId)
@ -95,6 +97,10 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
.asSequence()
.map { it.stateKey }

if (isDirect != null) {
roomSummaryEntity.isDirect = isDirect
roomSummaryEntity.directUserId = directUserId
}
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic

View File

@ -17,22 +17,33 @@
package im.vector.matrix.android.internal.session.room.create

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
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.internal.database.RealmQueryLatch
import im.vector.matrix.android.internal.di.SessionDatabase
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.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.RealmConfiguration
import java.util.concurrent.TimeUnit
import javax.inject.Inject

internal interface CreateRoomTask : Task<CreateRoomParams, String>

internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val directChatsHelper: DirectChatsHelper,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase private val realmConfiguration: RealmConfiguration) : CreateRoomTask {


@ -41,17 +52,50 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
apiCall = roomAPI.createRoom(params)
}.flatMap { createRoomResponse ->
val roomId = createRoomResponse.roomId!!

// TODO Maybe do the same code for join room request ?
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}

rql.await()

return Try.just(roomId)
Try {
rql.await(timeout = 20L, timeUnit = TimeUnit.SECONDS)
roomId
}
}.flatMap { roomId ->
if (params.isDirect()) {
handleDirectChatCreation(params, roomId)
} else {
Try.just(roomId)
}
}.flatMap { roomId ->
setReadMarkers(roomId)
}
}

private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String): Try<String> {
val otherUserId = params.getFirstInvitedUserId()
?: return Try.raise(IllegalStateException("You can't create a direct room without an invitedUser"))

return monarchy.tryTransactionSync { realm ->
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
this.directUserId = otherUserId
this.isDirect = true
}
}.flatMap {
val directChats = directChatsHelper.getDirectChats()
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
}.flatMap {
Try.just(roomId)
}
}

private suspend fun setReadMarkers(roomId: String): Try<String> {
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
return readMarkersTask
.execute(setReadMarkerParams)
.flatMap {
Try.just(roomId)
}
}

}

View File

@ -42,9 +42,11 @@ internal class DefaultMembershipService @Inject constructor(private val roomId:
private val leaveRoomTask: LeaveRoomTask
) : MembershipService {

override fun loadRoomMembersIfNeeded(): Cancelable {
override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Boolean>): Cancelable {
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
return loadRoomMembersTask.configureWith(params)
.dispatchTo(matrixCallback)
.executeBy(taskExecutor)
}

override fun getRoomMember(userId: String): RoomMember? {
@ -73,23 +75,23 @@ internal class DefaultMembershipService @Inject constructor(private val roomId:
return result
}

override fun invite(userId: String, callback: MatrixCallback<Unit>) {
override fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteTask.Params(roomId, userId)
inviteTask.configureWith(params)
return inviteTask.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
}

override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>) {
override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = JoinRoomTask.Params(roomId, viaServers)
joinTask.configureWith(params)
return joinTask.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
}

override fun leave(callback: MatrixCallback<Unit>) {
override fun leave(callback: MatrixCallback<Unit>): Cancelable {
val params = LeaveRoomTask.Params(roomId)
leaveRoomTask.configureWith(params)
return leaveRoomTask.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
}

View File

@ -34,7 +34,6 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import io.realm.RealmResults
import javax.inject.Inject

/**
@ -81,10 +80,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:

val roomMembers = RoomMembers(realm, roomId)
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll()
val otherMembersSubset = loadedMembers.where()
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
.limit(3)
.findAll()


if (roomEntity?.membership == Membership.INVITE) {
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
@ -97,23 +93,29 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
} else {
context.getString(R.string.room_displayname_room_invite)
}
} else {
} else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val memberIds: List<String> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes
val otherMembersSubset: List<EventEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull {
roomMembers.getStateEvent(it)
}
} else {
otherMembersSubset.mapNotNull { it.stateKey }
loadedMembers.where()
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
.limit(3)
.findAll()
}
name = when (memberIds.size) {
val otherMembersCount = roomMembers.getNumberOfMembers() - 1
name = when (otherMembersCount) {
0 -> context.getString(R.string.room_displayname_empty_room)
1 -> resolveRoomMember(otherMembersSubset[0], roomMembers)
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> context.getString(R.string.room_displayname_two_members,
resolveRoomMember(otherMembersSubset[0], roomMembers),
resolveRoomMember(otherMembersSubset[1], roomMembers)
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMember(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1)
}
}
@ -122,8 +124,8 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return name ?: roomId
}

private fun resolveRoomMember(eventEntity: EventEntity?,
roomMembers: RoomMembers): String? {
private fun resolveRoomMemberName(eventEntity: EventEntity?,
roomMembers: RoomMembers): String? {
if (eventEntity == null) return null
val roomMember = eventEntity.toRoomMember() ?: return null
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)

View File

@ -42,12 +42,16 @@ internal class RoomMembers(private val realm: Realm,
RoomSummaryEntity.where(realm, roomId).findFirst()
}

fun get(userId: String): RoomMember? {
fun getStateEvent(userId: String): EventEntity? {
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.equalTo(EventEntityFields.STATE_KEY, userId)
.findFirst()
}

fun get(userId: String): RoomMember? {
return getStateEvent(userId)
?.let {
it.asDomain().content?.toModel<RoomMember>()
}

View File

@ -17,10 +17,16 @@
package im.vector.matrix.android.internal.session.room.membership.joining

import arrow.core.Try
import im.vector.matrix.android.internal.database.RealmQueryLatch
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration
import java.util.concurrent.TimeUnit
import javax.inject.Inject

internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
@ -30,12 +36,32 @@ internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
)
}

internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI) : JoinRoomTask {
internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI,
private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase private val realmConfiguration: RealmConfiguration) : JoinRoomTask {

override suspend fun execute(params: JoinRoomTask.Params): Try<Unit> {
return executeRequest {
return executeRequest<Unit> {
apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap())
}.flatMap {
val roomId = params.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}
Try {
rql.await(20L, TimeUnit.SECONDS)
roomId
}
}.flatMap { roomId ->
setReadMarkers(roomId)
}
}

private suspend fun setReadMarkers(roomId: String): Try<Unit> {
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
return readMarkersTask.execute(setReadMarkerParams)
}

}

View File

@ -22,14 +22,11 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied
import javax.inject.Inject

internal class DefaultReadService @Inject constructor(private val roomId: String,
@ -39,9 +36,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
private val credentials: Credentials) : ReadService {

override fun markAllAsRead(callback: MatrixCallback<Unit>) {
//TODO shouldn't it be latest synced event?
val latestEvent = getLatestEvent()
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
}

@ -55,9 +50,6 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
}

private fun getLatestEvent(): TimelineEventEntity? {
return monarchy.fetchCopied { TimelineEventEntity.latestEvent(it, roomId) }
}

override fun isEventRead(eventId: String): Boolean {
var isEventRead = false

View File

@ -20,7 +20,6 @@ import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -33,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionAsync
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject

@ -40,8 +40,9 @@ internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {

data class Params(
val roomId: String,
val fullyReadEventId: String?,
val readReceiptEventId: String?
val markAllAsRead: Boolean = false,
val fullyReadEventId: String? = null,
val readReceiptEventId: String? = null
)
}

@ -55,21 +56,35 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI

override suspend fun execute(params: SetReadMarkersTask.Params): Try<Unit> {
val markers = HashMap<String, String>()
if (params.fullyReadEventId != null) {
if (LocalEchoEventFactory.isLocalEchoId(params.fullyReadEventId)) {
val fullyReadEventId: String?
val readReceiptEventId: String?

if (params.markAllAsRead) {
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
}
fullyReadEventId = latestSyncedEventId
readReceiptEventId = latestSyncedEventId
} else {
fullyReadEventId = params.fullyReadEventId
readReceiptEventId = params.readReceiptEventId
}

if (fullyReadEventId != null) {
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
} else {
markers[READ_MARKER] = params.fullyReadEventId
markers[READ_MARKER] = fullyReadEventId
}
}
if (params.readReceiptEventId != null
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
if (readReceiptEventId != null
&& !isEventRead(params.roomId, readReceiptEventId)) {

if (LocalEchoEventFactory.isLocalEchoId(params.readReceiptEventId)) {
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
} else {
updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
markers[READ_RECEIPT] = params.readReceiptEventId
updateNotificationCountIfNecessary(params.roomId, readReceiptEventId)
markers[READ_RECEIPT] = readReceiptEventId
}
}
return if (markers.isEmpty()) {
@ -83,10 +98,10 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI

private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.tryTransactionAsync { realm ->
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId)?.eventId == eventId
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId
if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@tryTransactionAsync
?: return@tryTransactionAsync
roomSummary.notificationCount = 0
roomSummary.highlightCount = 0
}
@ -97,13 +112,13 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
?: return@doWithRealm
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
?: Int.MAX_VALUE
?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex
}
return isEventRead

View File

@ -18,27 +18,42 @@ package im.vector.matrix.android.internal.session.sync

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.auth.data.Credentials
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.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.isDirect
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.*
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -55,6 +70,9 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService,
private val processForPushTask: ProcessEventForPushTask,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val credentials: Credentials,
private val directChatsHelper: DirectChatsHelper,
private val taskExecutor: TaskExecutor) {

sealed class HandlingStrategy {
@ -118,7 +136,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
Timber.v("Handle join sync for room $roomId")

val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)

if (roomEntity.membership == Membership.INVITE) {
roomEntity.chunks.deleteAllFromRealm()
@ -128,7 +146,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
// State event
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
?: Int.MIN_VALUE
?: Int.MIN_VALUE
val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
@ -168,13 +186,27 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
InvitedRoomSync): RoomEntity {
Timber.v("Handle invited sync for room $roomId")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
roomEntity.addOrUpdate(chunkEntity)
}
roomSummaryUpdater.update(realm, roomId, Membership.INVITE)
val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId)
val inviterId = myUserStateEvent?.sender
val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
val isDirect = myUserRoomMember?.isDirect
if (isDirect == true && inviterId != null) {
val isAlreadyDirect = RoomSummaryEntity.isDirect(realm, roomId)
if (!isAlreadyDirect) {
val directChatsMap = directChatsHelper.getDirectChats(include = Pair(inviterId, roomId))
val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams(
directMessages = directChatsMap
)
updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor)
}
}
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, isDirect = isDirect, directUserId = inviterId)
return roomEntity
}

@ -182,7 +214,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomId: String,
roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)

roomEntity.membership = Membership.LEAVE
roomEntity.chunks.deleteAllFromRealm()

View File

@ -19,8 +19,8 @@ package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.getDirectRooms
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync
import javax.inject.Inject
@ -37,19 +37,22 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
}

private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) {
val newDirectRoomIds = directMessages.content.values.flatten()
monarchy.runTransactionSync { realm ->

val oldDirectRooms = RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.findAll()
oldDirectRooms.forEach { it.isDirect = false }

newDirectRoomIds.forEach { roomId ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummaryEntity != null) {
roomSummaryEntity.isDirect = true
realm.insertOrUpdate(roomSummaryEntity)
val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
oldDirectRooms.forEach {
it.isDirect = false
it.directUserId = null
}
directMessages.content.forEach {
val userId = it.key
it.value.forEach { roomId ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummaryEntity != null) {
roomSummaryEntity.isDirect = true
roomSummaryEntity.directUserId = userId
realm.insertOrUpdate(roomSummaryEntity)
}
}
}
}

View File

@ -18,18 +18,46 @@ package im.vector.matrix.android.internal.session.user

import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.paging.DataSource
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.model.UserEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.user.model.SearchUserTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied
import javax.inject.Inject

internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy) : UserService {
internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy,
private val searchUserTask: SearchUserTask,
private val taskExecutor: TaskExecutor) : UserService {

private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy {
monarchy.createDataSourceFactory { realm ->
realm.where(UserEntity::class.java)
.isNotEmpty(UserEntityFields.USER_ID)
.sort(UserEntityFields.DISPLAY_NAME)
}
}

private val domainDataSourceFactory: DataSource.Factory<Int, User> by lazy {
realmDataSourceFactory.map {
it.asDomain()
}
}

private val livePagedListBuilder: LivePagedListBuilder<Int, User> by lazy {
LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build())
}

override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
@ -38,7 +66,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
return userEntity.asDomain()
}

override fun observeUser(userId: String): LiveData<User?> {
override fun liveUser(userId: String): LiveData<User?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
UserEntity.where(realm, userId)
}
@ -48,4 +76,45 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
.firstOrNull()
}
}
}

override fun liveUsers(): LiveData<List<User>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
realm.where(UserEntity::class.java)
.isNotEmpty(UserEntityFields.USER_ID)
.sort(UserEntityFields.DISPLAY_NAME)
},
{ it.asDomain() }
)
}

override fun livePagedUsers(filter: String?): LiveData<PagedList<User>> {
realmDataSourceFactory.updateQuery { realm ->
val query = realm.where(UserEntity::class.java)
if (filter.isNullOrEmpty()) {
query.isNotEmpty(UserEntityFields.USER_ID)
} else {
query
.beginGroup()
.contains(UserEntityFields.DISPLAY_NAME, filter)
.or()
.contains(UserEntityFields.USER_ID, filter)
.endGroup()
}
query.sort(UserEntityFields.DISPLAY_NAME)
}
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}


override fun searchUsersDirectory(search: String,
limit: Int,
excludedUserIds: Set<String>,
callback: MatrixCallback<List<User>>): Cancelable {
val params = SearchUserTask.Params(limit, search, excludedUserIds)
return searchUserTask
.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.user

import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0
import im.vector.matrix.android.internal.session.user.model.SearchUsersParams
import im.vector.matrix.android.internal.session.user.model.SearchUsersRequestResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST

internal interface SearchUserAPI {

/**
* Perform a user search.
*
* @param searchUsersParams the search params.
*/
@POST(URI_API_PREFIX_PATH_R0 + "user_directory/search")
fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call<SearchUsersRequestResponse>
}

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.user
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.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.model.UserEntity

@ -29,9 +30,13 @@ internal object UserEntityFactory {
return null
}
val roomMember = event.content.toModel<RoomMember>() ?: return null
// We only use JOIN and INVITED memberships to create User data
if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) {
return null
}
return UserEntity(event.stateKey ?: "",
roomMember.displayName ?: "",
roomMember.avatarUrl ?: ""
roomMember.displayName ?: "",
roomMember.avatarUrl ?: ""
)
}


View File

@ -18,12 +18,31 @@ package im.vector.matrix.android.internal.session.user

import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncAPI
import im.vector.matrix.android.internal.session.user.model.DefaultSearchUserTask
import im.vector.matrix.android.internal.session.user.model.SearchUserTask
import retrofit2.Retrofit

@Module
internal abstract class UserModule {

@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesSearchUserAPI(retrofit: Retrofit): SearchUserAPI {
return retrofit.create(SearchUserAPI::class.java)
}
}

@Binds
abstract fun bindUserService(userService: DefaultUserService): UserService

@Binds
abstract fun bindSearchUserTask(searchUserTask: DefaultSearchUserTask): SearchUserTask

}

View File

@ -0,0 +1,48 @@
/*
* 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.user.accountdata

import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path

interface AccountDataAPI {

/**
* Set some account_data for the client.
*
* @param userId the user id
* @param type the type
* @param params the put params
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}")
fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call<Unit>

/**
* Gets a bearer token from the homeserver that the user can
* present to a third party in order to prove their ownership
* of the Matrix account they are logged into.
*
* @param userId the user id
* @param body the body content
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token")
fun openIdToken(@Path("userId") userId: String, @Body body: Map<Any, Any>): Call<Map<Any, Any>>
}

View File

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

import dagger.Binds
import dagger.Module
import dagger.Provides
import retrofit2.Retrofit

@Module
internal abstract class AccountDataModule {

@Module
companion object {

@JvmStatic
@Provides
fun providesAccountDataAPI(retrofit: Retrofit): AccountDataAPI {
return retrofit.create(AccountDataAPI::class.java)
}

}

@Binds
abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAcountDataTask): UpdateUserAccountDataTask

}

View File

@ -0,0 +1,54 @@
/*
* 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.user.accountdata

import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.getDirectRooms
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import javax.inject.Inject

internal class DirectChatsHelper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {

fun getDirectChats(include: Pair<String, String>? = null, filterRoomId: String? = null): Map<String, List<String>> {
return Realm.getInstance(realmConfiguration).use { realm ->
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
val directChatsMap = mutableMapOf<String, MutableList<String>>()
for (directRoom in currentDirectRooms) {
if (directRoom.roomId == filterRoomId) continue
val directUserId = directRoom.directUserId ?: continue
directChatsMap.getOrPut(directUserId, { arrayListOf() }).apply {
add(directRoom.roomId)
}
}
if (include != null) {
directChatsMap.getOrPut(include.first, { arrayListOf() }).apply {
if (contains(include.second)) {
Timber.v("Direct chats already include room ${include.second} with user ${include.first}")
} else {
add(include.second)
}
}
}
directChatsMap
}
}


}

View File

@ -0,0 +1,55 @@
/*
* 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.user.accountdata

import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject

internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Params, Unit> {

interface Params {
val type: String
fun getData(): Any
}

data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES,
private val directMessages: Map<String, List<String>>
) : Params {

override fun getData(): Any {
return directMessages
}
}


}

internal class DefaultUpdateUserAcountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI,
private val credentials: Credentials) : UpdateUserAccountDataTask {

override suspend fun execute(params: UpdateUserAccountDataTask.Params): Try<Unit> {

return executeRequest {
apiCall = accountDataApi.setAccountData(credentials.userId, params.type, params.getData())
}
}

}

View File

@ -0,0 +1,27 @@
/*
* 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.user.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
internal data class SearchUser(
@Json(name = "user_id") val userId: String,
@Json(name = "display_name") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null
)

View File

@ -0,0 +1,47 @@
/*
* 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.user.model

import arrow.core.Try
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.user.SearchUserAPI
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject

internal interface SearchUserTask : Task<SearchUserTask.Params, List<User>> {

data class Params(
val limit: Int,
val search: String,
val excludedUserIds: Set<String>
)
}

internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask {

override suspend fun execute(params: SearchUserTask.Params): Try<List<User>> {
return executeRequest<SearchUsersRequestResponse> {
apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit))
}.map { response ->
response.users.map {
User(it.userId, it.displayName, it.avatarUrl)
}
}
}

}

View File

@ -0,0 +1,31 @@
/*
* 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.user.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* Class representing an user search parameters
*/
@JsonClass(generateAdapter = true)
internal data class SearchUsersParams(
// the searched term
@Json(name = "search_term") val searchTerm: String,
// set a limit to the request response
@Json(name = "limit") val limit: Int
)

View File

@ -0,0 +1,14 @@
package im.vector.matrix.android.internal.session.user.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* Class representing an users search response
*/
@JsonClass(generateAdapter = true)
internal data class SearchUsersRequestResponse(
@Json(name = "limited") val limited: Boolean = false,
@Json(name = "results") val users: List<SearchUser> = emptyList()
)

View File

@ -16,6 +16,7 @@

package im.vector.matrix.android.internal.util

import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber

/**
@ -49,3 +50,10 @@ fun convertFromUTF8(s: String): String? {
null
}
}

fun String?.firstLetterOfDisplayName(): String {
if (this.isNullOrEmpty()) return ""
val isUserId = MatrixPatterns.isUserId(this)
val firstLetterIndex = if (isUserId) 1 else 0
return this[firstLetterIndex].toString().toUpperCase()
}

View File

@ -148,7 +148,7 @@ android {

dependencies {

def epoxy_version = "3.3.0"
def epoxy_version = "3.7.0"
def arrow_version = "0.8.2"
def coroutines_version = "1.0.1"
def markwon_version = '3.0.0'
@ -193,11 +193,15 @@ dependencies {

implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
implementation 'com.airbnb.android:mvrx:1.0.1'

// Work
implementation "androidx.work:work-runtime-ktx:2.1.0-rc01"

// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0"

// Functional Programming
implementation "io.arrow-kt:arrow-core:$arrow_version"

@ -206,7 +210,7 @@ dependencies {

// UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha07'
implementation 'com.google.android.material:material:1.1.0-alpha08'
implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"

View File

@ -64,6 +64,7 @@
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />

<!-- Services -->


View File

@ -36,6 +36,9 @@ import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.HomeDetailFragment
import im.vector.riotx.features.home.HomeDrawerFragment
import im.vector.riotx.features.home.HomeModule
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.timeline.action.*
@ -45,6 +48,7 @@ import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake
@ -73,6 +77,8 @@ interface ScreenComponent {

fun rageShake(): RageShake

fun navigator(): Navigator

fun inject(activity: HomeActivity)

fun inject(roomDetailFragment: RoomDetailFragment)
@ -153,6 +159,12 @@ interface ScreenComponent {

fun inject(pushGatewaysFragment: PushGatewaysFragment)

fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomKnownUsersFragment)

fun inject(createDirectRoomDirectoryUsersFragment: CreateDirectRoomDirectoryUsersFragment)

fun inject(createDirectRoomActivity: CreateDirectRoomActivity)

@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -30,6 +30,9 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.*
import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory
import im.vector.riotx.features.home.group.GroupListViewModel
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
@ -116,6 +119,11 @@ interface ViewModelModule {
@ViewModelKey(ConfigurationViewModel::class)
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(CreateDirectRoomNavigationViewModel::class)
fun bindCreateDirectRoomNavigationViewModel(viewModel: CreateDirectRoomNavigationViewModel): ViewModel

/**
* Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future.
*/
@ -168,6 +176,9 @@ interface ViewModelModule {
@Binds
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory

@Binds
fun bindCreateDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel_AssistedFactory): CreateDirectRoomViewModel.Factory

@Binds
fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory


View File

@ -23,13 +23,16 @@ import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.annotation.DrawableRes
import im.vector.riotx.R

fun EditText.setupAsSearch() {
fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_green) {

addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
val clearIcon = if (editable?.isNotEmpty() == true) R.drawable.ic_clear_white else 0
setCompoundDrawablesWithIntrinsicBounds(0, 0, clearIcon, 0)
val clearIcon = if (editable?.isNotEmpty() == true) clearIconRes else 0
setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0)
}

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

View File

@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import im.vector.riotx.core.utils.FirstThrottler
import im.vector.riotx.core.utils.EventObserver
@ -44,3 +45,7 @@ inline fun <T> LiveData<LiveEvent<T>>.observeEventFirstThrottle(owner: Lifecycle
}
})
}

fun <T> MutableLiveData<LiveEvent<T>>.postLiveEvent(content: T) {
this.postValue(LiveEvent(content))
}

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.mvrx
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.utils.LiveEvent

abstract class NavigationViewModel<NavigationClass> : ViewModel() {
@ -29,6 +30,6 @@ abstract class NavigationViewModel<NavigationClass> : ViewModel() {


fun goTo(navigation: NavigationClass) {
_navigateTo.postValue(LiveEvent(navigation))
_navigateTo.postLiveEvent(navigation)
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.riotx.core.platform

import android.annotation.TargetApi
import android.content.Context
import android.content.res.TypedArray
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.widget.ScrollView

import im.vector.riotx.R

private const val DEFAULT_MAX_HEIGHT = 200

class MaxHeightScrollView : ScrollView {

var maxHeight: Int = 0
set(value) {
field = value
requestLayout()
}

constructor(context: Context) : super(context) {}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
if (!isInEditMode) {
init(context, attrs)
}
}

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
if (!isInEditMode) {
init(context, attrs)
}
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
if (!isInEditMode) {
init(context, attrs)
}
}

private fun init(context: Context, attrs: AttributeSet?) {
if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
styledAttrs.recycle()
}
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
}
}

View File

@ -18,6 +18,7 @@ package im.vector.riotx.core.platform
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.core.view.isGone
import androidx.core.view.isVisible
import butterknife.BindView
@ -46,6 +47,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {

@Inject lateinit var session: Session

@CallSuper
override fun injectWith(injector: ScreenComponent) {
session = injector.session()
}

View File

@ -26,6 +26,7 @@ import androidx.annotation.*
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
@ -40,6 +41,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.*
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake
@ -70,6 +72,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
private lateinit var configurationViewModel: ConfigurationViewModel
protected lateinit var bugReporter: BugReporter
private lateinit var rageShake: RageShake
protected lateinit var navigator: Navigator

private var unBinder: Unbinder? = null

@ -121,6 +124,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
configurationViewModel = ViewModelProviders.of(this, viewModelFactory).get(ConfigurationViewModel::class.java)
bugReporter = screenComponent.bugReporter()
rageShake = screenComponent.rageShake()
navigator = screenComponent.navigator()
configurationViewModel.activityRestarter.observe(this, Observer {
if (!it.hasBeenHandled) {
// Recreate the Activity because configuration has changed
@ -262,6 +266,24 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
return super.onOptionsItemSelected(item)
}

protected fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
// if (fm.backStackEntryCount == 0)
// return false

val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
if (handledByChildFragments) {
return true
}
val backPressable = f as OnBackPressed
if (backPressable.onBackPressed()) {
return true
}
}
return false
}

/* ==========================================================================================
* PROTECTED METHODS
* ========================================================================================== */

View File

@ -65,7 +65,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen

override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = vectorBaseActivity.getVectorComponent().navigator()
navigator = screenComponent.navigator()
viewModelFactory = screenComponent.viewModelFactory()
injectWith(injector())
super.onAttach(context)

View File

@ -16,20 +16,36 @@

package im.vector.riotx.core.platform

import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.riotx.BuildConfig
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.Disposable

abstract class VectorViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, false) {

protected val cancelableBag = CancelableBag()

override fun onCleared() {
super.onCleared()
cancelableBag.cancel()
/**
* This method does the same thing as the execute function, but it doesn't subscribe to the stream
* so you can use this in a switchMap or a flatMap
*/
fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
setState { stateReducer(Loading()) }
return this.map { Success(it) as Async<T> }
.onErrorReturn { Fail(it) }
.doOnSuccess { setState { stateReducer(it) } }
}

/**
* This method does the same thing as the execute function, but it doesn't subscribe to the stream
* so you can use this in a switchMap or a flatMap
*/
fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
setState { stateReducer(Loading()) }
return this.map { Success(it) as Async<T> }
.onErrorReturn { Fail(it) }
.doOnNext { setState { stateReducer(it) } }
}

}

View File

@ -0,0 +1,32 @@
/*
* 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.riotx.core.utils

import io.reactivex.Completable
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import io.reactivex.functions.Consumer
import io.reactivex.internal.functions.Functions
import timber.log.Timber

fun <T> Single<T>.subscribeLogError(): Disposable {
return subscribe(Functions.emptyConsumer(), Consumer { Timber.e(it) })
}

fun Completable.subscribeLogError(): Disposable {
return subscribe({}, { Timber.e(it) })
}

View File

@ -43,6 +43,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
@Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory

override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}


View File

@ -26,10 +26,10 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
@ -41,7 +41,7 @@ import javax.inject.Inject
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
*/

class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {

companion object {
private const val THUMBNAIL_SIZE = 250
@ -92,9 +92,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
return if (text.isEmpty()) {
TextDrawable.builder().buildRound("", avatarColor)
} else {
val isUserId = MatrixPatterns.isUserId(text)
val firstLetterIndex = if (isUserId) 1 else 0
val firstLetter = text[firstLetterIndex].toString().toUpperCase()
val firstLetter = text.firstLetterOfDisplayName()
TextDrawable.builder()
.beginConfig()
.bold()

View File

@ -65,7 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
@Inject lateinit var homeNavigator: HomeNavigator
@Inject lateinit var navigator: Navigator
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@ -213,23 +212,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}

private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
// if (fm.backStackEntryCount == 0)
// return false

val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
if (handledByChildFragments) {
return true
}
val backPressable = f as OnBackPressed
if (backPressable.onBackPressed()) {
return true
}
}
return false
}


companion object {

View File

@ -73,21 +73,21 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
.subscribe { list ->
list.let { summaries ->
val peopleNotifications = summaries
.filter { it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
.filter { it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
val peopleHasHighlight = summaries
.filter { it.isDirect }
.any { it.highlightCount > 0 }

val roomsNotifications = summaries
.filter { !it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
.filter { !it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
val roomsHasHighlight = summaries
.filter { !it.isDirect }
.any { it.highlightCount > 0 }

View File

@ -51,8 +51,7 @@ class HomeDrawerFragment : VectorBaseFragment() {
val groupListFragment = GroupListFragment.newInstance()
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
}

session.observeUser(session.myUserId).observeK(this) { user ->
session.liveUser(session.myUserId).observeK(this) { user ->
if (user != null) {
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName

View File

@ -0,0 +1,30 @@
/*
* 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.riotx.features.home.createdirect

import im.vector.matrix.android.api.session.user.model.User

sealed class CreateDirectRoomActions {

object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions()
data class FilterKnownUsers(val value: String) : CreateDirectRoomActions()
data class SearchDirectoryUsers(val value: String) : CreateDirectRoomActions()
object ClearFilterKnownUsers : CreateDirectRoomActions()
data class SelectUser(val user: User) : CreateDirectRoomActions()
data class RemoveSelectedUser(val user: User) : CreateDirectRoomActions()

}

View File

@ -0,0 +1,116 @@
/*
*
* * 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.riotx.features.home.createdirect

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData
import kotlinx.android.synthetic.main.activity.*
import javax.inject.Inject

class CreateDirectRoomActivity : SimpleFragmentActivity() {

sealed class Navigation {
object UsersDirectory : Navigation()
object Close : Navigation()
object Previous : Navigation()
}

private val viewModel: CreateDirectRoomViewModel by viewModel()
lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter

override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java)
navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) {
is Navigation.UsersDirectory -> addFragmentToBackstack(CreateDirectRoomDirectoryUsersFragment(), R.id.container)
Navigation.Close -> finish()
Navigation.Previous -> onBackPressed()
}
}
if (isFirstCreation()) {
addFragment(CreateDirectRoomKnownUsersFragment(), R.id.container)
}
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
renderCreateAndInviteState(it)
}
}

private fun renderCreateAndInviteState(state: Async<String>) {
when (state) {
is Loading -> renderCreationLoading()
is Success -> renderCreationSuccess(state())
is Fail -> renderCreationFailure(state.error)
}
}

private fun renderCreationLoading() {
updateWaitingView(WaitingViewData(getString(R.string.creating_direct_room)))
}

private fun renderCreationFailure(error: Throwable) {
hideWaitingView()
AlertDialog.Builder(this)
.setMessage(errorFormatter.toHumanReadable(error))
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show()
}

private fun renderCreationSuccess(roomId: String?) {
// Navigate to freshly created room
if (roomId != null) {
navigator.openRoom(this, roomId)
}
finish()
}


companion object {
fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java)
}
}


}

View File

@ -0,0 +1,96 @@
/*
* 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.riotx.features.home.createdirect

import android.content.Context
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
import javax.inject.Inject

class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUsersController.Callback {

override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users

private val viewModel: CreateDirectRoomViewModel by activityViewModel()

@Inject lateinit var directRoomController: DirectoryUsersController
private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel

override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java)
setupRecyclerView()
setupSearchByMatrixIdView()
setupCloseView()
}

private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
directRoomController.callback = this
recyclerView.setController(directRoomController)
}

private fun setupSearchByMatrixIdView() {
createDirectRoomSearchById.setupAsSearch(searchIconRes = 0)
createDirectRoomSearchById
.textChanges()
.subscribe {
viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(it.toString()))
}
.disposeOnDestroy()
createDirectRoomSearchById.requestFocus()
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT)

}

private fun setupCloseView() {
createDirectRoomClose.setOnClickListener {
navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous)
}
}

override fun invalidate() = withState(viewModel) {
directRoomController.setData(it)
}

override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(CreateDirectRoomActions.SelectUser(user))
navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous)
}

override fun retryDirectoryUsersRequest() {
val currentSearch = createDirectRoomSearchById.text.toString()
viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(currentSearch))
}
}

View File

@ -0,0 +1,177 @@
/*
*
* * 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.riotx.features.home.createdirect

import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.ScrollView
import androidx.core.view.size
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionUtils
import im.vector.riotx.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
import javax.inject.Inject

class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersController.Callback {

override fun getLayoutResId() = R.layout.fragment_create_direct_room

override fun getMenuRes() = R.menu.vector_create_direct_room

private val viewModel: CreateDirectRoomViewModel by activityViewModel()

@Inject lateinit var directRoomController: KnownUsersController
@Inject lateinit var avatarRenderer: AvatarRenderer
private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel

override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java)
vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar)
setupRecyclerView()
setupFilterView()
setupAddByMatrixIdView()
setupCloseView()
viewModel.selectUserEvent.observeEvent(this) {
updateChipsView(it)
}
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
renderSelectedUsers(it)
}
}

override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) {
val createMenuItem = menu.findItem(R.id.action_create_direct_room)
val showMenuItem = it.selectedUsers.isNotEmpty()
createMenuItem.setVisible(showMenuItem)
}
super.onPrepareOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_create_direct_room -> {
viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers)
true
}
else ->
super.onOptionsItemSelected(item)
}
}

private fun setupAddByMatrixIdView() {
addByMatrixId.setOnClickListener {
navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.UsersDirectory)
}
}

private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
// Don't activate animation as we might have way to much item animation when filtering
recyclerView.itemAnimator = null
directRoomController.callback = this
recyclerView.setController(directRoomController)
}

private fun setupFilterView() {
createDirectRoomFilter
.textChanges()
.startWith(createDirectRoomFilter.text)
.subscribe { text ->
val filterValue = text.trim()
val action = if (filterValue.isBlank()) {
CreateDirectRoomActions.ClearFilterKnownUsers
} else {
CreateDirectRoomActions.FilterKnownUsers(filterValue.toString())
}
viewModel.handle(action)
}
.disposeOnDestroy()

createDirectRoomFilter.setupAsSearch()
createDirectRoomFilter.requestFocus()
}

private fun setupCloseView() {
createDirectRoomClose.setOnClickListener {
requireActivity().finish()
}
}

override fun invalidate() = withState(viewModel) {
directRoomController.setData(it)
}

private fun updateChipsView(data: SelectUserAction) {
if (data.isAdded) {
addChipToGroup(data.user, chipGroup)
} else {
if (chipGroup.size > data.index) {
chipGroup.removeViewAt(data.index)
}
}
}

private fun renderSelectedUsers(selectedUsers: Set<User>) {
vectorBaseActivity.invalidateOptionsMenu()
if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
selectedUsers.forEach { addChipToGroup(it, chipGroup) }
}
}

private fun addChipToGroup(user: User, chipGroup: ChipGroup) {
val chip = Chip(requireContext())
chip.setChipBackgroundColorResource(android.R.color.transparent)
chip.chipStrokeWidth = DimensionUtils.dpToPx(1, requireContext()).toFloat()
chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName
chip.isClickable = true
chip.isCheckable = false
chip.isCloseIconVisible = true
chipGroup.addView(chip)
chip.setOnCloseIconClickListener {
viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user))
}
chipGroupScrollView.post {
chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}

override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(CreateDirectRoomActions.SelectUser(user))
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.riotx.features.home.createdirect

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.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel

@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header)
abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel<CreateDirectRoomLetterHeaderItem.Holder>() {

@EpoxyAttribute var letter: String = ""

override fun bind(holder: Holder) {
holder.letterView.text = letter
}

class Holder : VectorEpoxyHolder() {
val letterView by bind<TextView>(R.id.createDirectRoomLetterView)
}

}

View File

@ -0,0 +1,22 @@
/*
* 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.riotx.features.home.createdirect

import im.vector.riotx.core.mvrx.NavigationViewModel
import javax.inject.Inject

class CreateDirectRoomNavigationViewModel @Inject constructor(): NavigationViewModel<CreateDirectRoomActivity.Navigation>()

View File

@ -0,0 +1,77 @@
/*
*
* * 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.riotx.features.home.createdirect

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.amulyakhare.textdrawable.TextDrawable
import im.vector.riotx.R
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_create_direct_room_user)
abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() {

@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var name: String? = null
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
@EpoxyAttribute var selected: Boolean = false


override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
// If name is empty, use userId as name and force it being centered
if (name.isNullOrEmpty()) {
holder.userIdView.visibility = View.GONE
holder.nameView.text = userId
} else {
holder.userIdView.visibility = View.VISIBLE
holder.nameView.text = name
holder.userIdView.text = userId
}
renderSelection(holder, selected)
}

private fun renderSelection(holder: Holder, isSelected: Boolean) {
if (isSelected) {
holder.avatarCheckedImageView.visibility = View.VISIBLE
val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
holder.avatarImageView.setImageDrawable(backgroundDrawable)
} else {
holder.avatarCheckedImageView.visibility = View.GONE
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
}
}

class Holder : VectorEpoxyHolder() {
val userIdView by bind<TextView>(R.id.createDirectRoomUserID)
val nameView by bind<TextView>(R.id.createDirectRoomUserName)
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar)
val avatarCheckedImageView by bind<ImageView>(R.id.createDirectRoomUserAvatarChecked)
}

}

View File

@ -0,0 +1,172 @@
/*
*
* * 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.riotx.features.home.createdirect

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.*
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
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.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit

private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String

data class SelectUserAction(
val user: User,
val isAdded: Boolean,
val index: Int
)

class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val session: Session)
: VectorViewModel<CreateDirectRoomViewState>(initialState) {

@AssistedInject.Factory
interface Factory {
fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel
}

private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()

private val _selectUserEvent = MutableLiveData<LiveEvent<SelectUserAction>>()
val selectUserEvent: LiveData<LiveEvent<SelectUserAction>>
get() = _selectUserEvent

companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? {
val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.createDirectRoomViewModelFactory.create(state)
}
}

init {
observeKnownUsers()
observeDirectoryUsers()
}

fun handle(action: CreateDirectRoomActions) {
when (action) {
is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers()
is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
is CreateDirectRoomActions.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
is CreateDirectRoomActions.SelectUser -> handleSelectUser(action)
is CreateDirectRoomActions.RemoveSelectedUser -> handleRemoveSelectedUser(action)
}
}

private fun createRoomAndInviteSelectedUsers() = withState { currentState ->
val isDirect = currentState.selectedUsers.size == 1
val roomParams = CreateRoomParams().apply {
invitedUserIds = ArrayList(currentState.selectedUsers.map { it.userId })
if (isDirect) {
setDirectMessage()
}
}
session.rx()
.createRoom(roomParams)
.execute {
copy(createAndInviteState = it)
}
.disposeOnClear()
}

private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state ->
val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
val selectedUsers = state.selectedUsers.minus(action.user)
setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index))
}

private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { state ->
//Reset the filter asap
directoryUsersSearch.accept("")
val isAddOperation: Boolean
val selectedUsers: Set<User>
val indexOfUser = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
val changeIndex: Int
if (indexOfUser == -1) {
changeIndex = state.selectedUsers.size
selectedUsers = state.selectedUsers.plus(action.user)
isAddOperation = true
} else {
changeIndex = indexOfUser
selectedUsers = state.selectedUsers.minus(action.user)
isAddOperation = false
}
setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex))
}

private fun observeDirectoryUsers() {
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, emptySet())
.map { users ->
users.sortedBy { it.displayName.firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, directorySearchTerm = search)
}
}
.subscribe()
.disposeOnClear()
}

private fun observeKnownUsers() {
knownUsersFilter
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it.orNull())
}
.execute { async ->
copy(
knownUsers = async,
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
)
}
}

}

View File

@ -0,0 +1,42 @@
/*
*
* * 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.riotx.features.home.createdirect

import androidx.paging.PagedList
import arrow.core.Option
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.user.model.User

data class CreateDirectRoomViewState(
val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
val selectedUsers: Set<User> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val directorySearchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty()
) : MvRxState {

enum class DisplayMode {
KNOWN_USERS,
DIRECTORY_USERS
}

}

View File

@ -0,0 +1,127 @@
/*
*
* * 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.riotx.features.home.createdirect

import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
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 DirectoryUsersController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {

private var state: CreateDirectRoomViewState? = null

var callback: Callback? = null

init {
requestModelBuild()
}

fun setData(state: CreateDirectRoomViewState) {
this.state = state
requestModelBuild()
}


override fun buildModels() {
val currentState = state ?: return
val hasSearch = currentState.directorySearchTerm.isNotBlank()
val asyncUsers = currentState.directoryUsers
when (asyncUsers) {
is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading()
is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch)
is Fail -> renderFailure(asyncUsers.error)
}
}

private fun renderLoading() {
loadingItem {
id("loading")
}
}

private fun renderFailure(failure: Throwable) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(failure))
listener { callback?.retryDirectoryUsersRequest() }
}
}

private fun renderSuccess(users: List<User>,
selectedUsers: List<String>,
hasSearch: Boolean) {
if (users.isEmpty()) {
renderEmptyState(hasSearch)
} else {
renderUsers(users, selectedUsers)
}
}

private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
for (user in users) {
if (user.userId == session.myUserId) {
continue
}
val isSelected = selectedUsers.contains(user.userId)
createDirectRoomUserItem {
id(user.userId)
selected(isSelected)
userId(user.userId)
name(user.displayName)
avatarUrl(user.avatarUrl)
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
}
}
}
}

private fun renderEmptyState(hasSearch: Boolean) {
val noResultRes = if (hasSearch) {
R.string.no_result_placeholder
} else {
R.string.direct_room_start_search
}
noResultItem {
id("noResult")
text(stringProvider.getString(noResultRes))
}
}

interface Callback {
fun onItemClick(user: User)
fun retryDirectoryUsersRequest()
}

}

View File

@ -0,0 +1,130 @@
/*
* 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.riotx.features.home.createdirect

import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.createUIHandler
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.EmptyItem_
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 KnownUsersController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider) : PagedListEpoxyController<User>(
modelBuildingHandler = createUIHandler()
) {

private var selectedUsers: List<String> = emptyList()
private var users: Async<List<User>> = Uninitialized
private var isFiltering: Boolean = false

var callback: Callback? = null

init {
requestModelBuild()
}

fun setData(state: CreateDirectRoomViewState) {
this.isFiltering = !state.filterKnownUsersValue.isEmpty()
val newSelection = state.selectedUsers.map { it.userId }
this.users = state.knownUsers
if (newSelection != selectedUsers) {
this.selectedUsers = newSelection
requestForcedModelBuild()
}
submitList(state.knownUsers())
}

override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> {
return if (item == null) {
EmptyItem_().id(currentPosition)
} else {
val isSelected = selectedUsers.contains(item.userId)
CreateDirectRoomUserItem_()
.id(item.userId)
.selected(isSelected)
.userId(item.userId)
.name(item.displayName)
.avatarUrl(item.avatarUrl)
.avatarRenderer(avatarRenderer)
.clickListener { _ ->
callback?.onItemClick(item)
}
}
}

override fun addModels(models: List<EpoxyModel<*>>) {
if (users is Incomplete) {
renderLoading()
} else if (models.isEmpty()) {
renderEmptyState()
} else {
var lastFirstLetter: String? = null
for (model in models) {
if (model is CreateDirectRoomUserItem) {
if (model.userId == session.myUserId) continue
val currentFirstLetter = model.name.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter

CreateDirectRoomLetterHeaderItem_()
.id(currentFirstLetter)
.letter(currentFirstLetter)
.addIf(showLetter, this)

model.addTo(this)
} else {
continue
}
}
}
}

private fun renderLoading() {
loadingItem {
id("loading")
}
}

private fun renderEmptyState() {
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.direct_room_no_known_users))
}
}

interface Callback {
fun onItemClick(user: User)
}

}

View File

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
@ -67,7 +68,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) {
if (it != null) {
_openGroupLiveData.postValue(LiveEvent(it))
_openGroupLiveData.postLiveEvent(it)
val optionGroup = Option.fromNullable(it)
selectedGroupHolder.post(optionGroup)
}

View File

@ -47,10 +47,12 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
@ -106,7 +108,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeRoomSummary()
observeEventDisplayedActions()
observeSummaryState()
cancelableBag += room.loadRoomMembersIfNeeded()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
}
@ -209,62 +211,62 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
}
is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))
}
is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown("/"))
}
is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))
}
is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult)
}
is ParsedCommand.SetUserPowerLevel -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.ClearScalarToken -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.SetMarkdown -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.UnbanUser -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.BanUser -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.KickUser -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.JoinRoom -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.PartRoom -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)
}
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
}
is ParsedCommand.ChangeDisplayName -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
}
}
@ -296,7 +298,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
sendMode = SendMode.REGULAR
)
}
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
}
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
@ -321,7 +323,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
sendMode = SendMode.REGULAR
)
}
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
}
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
@ -331,7 +333,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
sendMode = SendMode.REGULAR
)
}
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
}

}
@ -360,29 +362,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}

private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)

room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
}

override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure)))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
}
})
}

private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)

room.invite(invite.userId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
}

override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure)))
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
}
})
}
@ -494,19 +496,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
_downloadedFileEvent.postValue(LiveEvent(DownloadFileState(
_downloadedFileEvent.postLiveEvent(DownloadFileState(
action.messageFileContent.getMimeType(),
data,
null
)))
))
}

override fun onFailure(failure: Throwable) {
_downloadedFileEvent.postValue(LiveEvent(DownloadFileState(
_downloadedFileEvent.postLiveEvent(DownloadFileState(
action.messageFileContent.getMimeType(),
null,
failure
)))
))
}
})

@ -535,7 +537,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}

_navigateToEvent.postValue(LiveEvent(targetEventId))
_navigateToEvent.postLiveEvent(targetEventId)
} else {
// change timeline
timeline.dispose()
@ -560,7 +562,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}

_navigateToEvent.postValue(LiveEvent(targetEventId))
_navigateToEvent.postLiveEvent(targetEventId)
}
}


View File

@ -149,7 +149,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}

override fun createDirectChat() {
vectorBaseActivity.notImplemented("creating direct chat")
navigator.openCreateDirectRoom(requireActivity())
}

private fun setupRecyclerView() {
@ -253,7 +253,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
return super.onBackPressed()
}

// RoomSummaryController.Callback **************************************************************
// RoomSummaryController.Callback **************************************************************

override fun onRoomSelected(room: RoomSummary) {
roomListViewModel.accept(RoomListActions.SelectRoom(room))

View File

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.home.HomeRoomListObservableStore
@ -142,7 +143,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room

override fun onFailure(failure: Throwable) {
// Notify the user
_invitationAnswerErrorLiveData.postValue(LiveEvent(failure))
_invitationAnswerErrorLiveData.postLiveEvent(failure)

setState {
copy(
@ -178,7 +179,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room

override fun onFailure(failure: Throwable) {
// Notify the user
_invitationAnswerErrorLiveData.postValue(LiveEvent(failure))
_invitationAnswerErrorLiveData.postLiveEvent(failure)

setState {
copy(

View File

@ -69,39 +69,19 @@ object ServerUrlsRepository {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)

return prefs.getString(HOME_SERVER_URL_PREF,
prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
getDefaultHomeServerUrl(context)))
prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
getDefaultHomeServerUrl(context)))
}


/**
* Return last used identity server url, or the default one from referrer or the default one from resources
*/
fun getLastIdentityServerUrl(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)

return prefs.getString(IDENTITY_SERVER_URL_PREF,
prefs.getString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF,
getDefaultIdentityServerUrl(context)))
}

/**
* Return true if url is the default home server url form resources
*/
fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context)

/**
* Return true if url is the default identity server url form resources
*/
fun isDefaultIdentityServerUrl(context: Context, url: String) = url == getDefaultIdentityServerUrl(context)

/**
* Return default home server url from resources
*/
fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url)

/**
* Return default identity server url from resources
*/
fun getDefaultIdentityServerUrl(context: Context): String = context.getString(R.string.default_identity_server_url)
}

View File

@ -37,6 +37,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import io.reactivex.Observable
import io.reactivex.functions.Function3
@ -44,9 +45,6 @@ import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject

private const val DEFAULT_HOME_SERVER_URI = "https://matrix.org"
private const val DEFAULT_IDENTITY_SERVER_URI = "https://vector.im"
private const val DEFAULT_ANTIVIRUS_SERVER_URI = "https://matrix.org"

class LoginActivity : VectorBaseActivity() {

@ -66,7 +64,7 @@ class LoginActivity : VectorBaseActivity() {
setupNotice()
setupAuthButton()
setupPasswordReveal()
homeServerField.setText(DEFAULT_HOME_SERVER_URI)
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this))
}

private fun setupNotice() {
@ -118,8 +116,6 @@ class LoginActivity : VectorBaseActivity() {
val homeServerUri = homeServerField.text?.trim().toString()
HomeServerConnectionConfig.Builder()
.withHomeServerUri(homeServerUri)
.withIdentityServerUri(DEFAULT_IDENTITY_SERVER_URI)
.withAntiVirusServerUri(DEFAULT_ANTIVIRUS_SERVER_URI)
.build()
}
}

View File

@ -25,6 +25,7 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
@ -68,6 +69,11 @@ class DefaultNavigator @Inject constructor() : Navigator {
context.startActivity(intent)
}

override fun openCreateDirectRoom(context: Context) {
val intent = CreateDirectRoomActivity.getIntent(context)
context.startActivity(intent)
}

override fun openRoomsFiltering(context: Context) {
val intent = FilteredRoomsActivity.newIntent(context)
context.startActivity(intent)

View File

@ -29,6 +29,8 @@ interface Navigator {

fun openCreateRoom(context: Context, initialName: String = "")

fun openCreateDirectRoom(context: Context)

fun openRoomDirectory(context: Context, initialFilter: String = "")

fun openRoomsFiltering(context: Context)

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
@ -207,7 +208,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:

override fun onFailure(failure: Throwable) {
// Notify the user
_joinRoomErrorLiveData.postValue(LiveEvent(failure))
_joinRoomErrorLiveData.postLiveEvent(failure)

setState {
copy(

View File

@ -0,0 +1,144 @@
<?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/createDirectRoomToolbar"
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/createDirectRoomClose"
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/createDirectRoomTitle"
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/fab_menu_create_chat"
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/createDirectRoomClose"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.appcompat.widget.Toolbar>

<im.vector.riotx.core.platform.MaxHeightScrollView
android:id="@+id/chipGroupScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"
app:maxHeight="64dp">

<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:lineSpacing="2dp" />

</im.vector.riotx.core.platform.MaxHeightScrollView>

<EditText
android:id="@+id/createDirectRoomFilter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:background="@null"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:hint="@string/direct_room_filter_hint"
android:importantForAutofill="no"
android:maxHeight="80dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />

<View
android:id="@+id/createDirectRoomFilterDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilter" />

<com.google.android.material.button.MaterialButton
android:id="@+id/addByMatrixId"
style="@style/VectorButtonStyleFlat"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:minHeight="@dimen/layout_touch_size"
android:text="@string/add_by_matrix_id"
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/createDirectRoomFilterDivider" />

<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addByMatrixId"
tools:listitem="@layout/item_create_direct_room_user" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,108 @@
<?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/createRoomToolbar"
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/createDirectRoomClose"
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/createDirectRoomTitle"
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/direct_chats_header"
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/createDirectRoomClose"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.appcompat.widget.Toolbar>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/createDirectRoomSearchByIdContainer"
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/createRoomToolbar">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/createDirectRoomSearchById"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/add_by_matrix_id" />

</com.google.android.material.textfield.TextInputLayout>

<View
android:id="@+id/createDirectRoomFilterDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomSearchByIdContainer" />

<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:fastScrollEnabled="true"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilterDivider"
tools:listitem="@layout/item_create_direct_room_user" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/createDirectRoomLetterView"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:fontFamily="sans-serif-medium"
android:padding="8dp"
android:textColor="?attr/riotx_text_primary"
android:textSize="20sp"
android:textStyle="normal"
tools:text="C" />

View File

@ -0,0 +1,72 @@
<?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:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">

<FrameLayout
android:id="@+id/createDirectRoomUserAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<ImageView
android:id="@+id/createDirectRoomUserAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@tools:sample/avatars" />

<ImageView
android:id="@+id/createDirectRoomUserAvatarChecked"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerInside"
android:src="@drawable/ic_material_done"
android:tint="@android:color/white"
android:visibility="visible" />
</FrameLayout>

<TextView
android:id="@+id/createDirectRoomUserName"
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_toTopOf="@+id/createDirectRoomUserID"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatarContainer"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />

<TextView
android:id="@+id/createDirectRoomUserID"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/createDirectRoomUserName"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomUserName"
tools:text="Blabla" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/action_create_direct_room"
android:title="@string/create_room_action_create"
app:showAsAction="always" />

</menu>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MaxHeightScrollView">
<attr name="maxHeight" format="dimension" />
</declare-styleable>
</resources>

View File

@ -4,26 +4,16 @@
<!-- "app_name" is now defined in build.gradle -->

<!-- server urls -->
<string name="vector_im_server_url" translatable="false">https://vector.im</string>
<string name="matrix_org_server_url" translatable="false">https://matrix.org</string>
<string name="default_hs_server_url" translatable="false">https://matrix.org</string>
<string name="default_identity_server_url" translatable="false">https://vector.im</string>
<string name="piwik_server_url" translatable="false">https://piwik.riot.im</string>
<string name="bug_report_url" translatable="false">https://riot.im/bugreports/submit</string>

<!-- Widget urls -->
<string name="integrations_ui_url" translatable="false">"https://scalar-staging.riot.im/scalar-web/"</string>
<string name="integrations_rest_url" translatable="false">"https://scalar-staging.riot.im/scalar/api"</string>

<string-array name="integrations_widgets_urls" translatable="false">
<item>https://scalar-staging.riot.im/scalar/api</item>
<item>https://scalar.vector.im/api</item>
</string-array>

<!--
Pusher config for the application
https://matrix.org/docs/spec/client_server/r0.4.0#id128
-->

<string name="pusher_http_url" translatable="false">https://matrix.org/_matrix/push/v1/notify</string>
<string name="pusher_app_id" translatable="false">im.vector.app.android</string>


View File

@ -2,5 +2,9 @@
<resources>

<!-- Strings not defined in Riot -->

<string name="add_by_matrix_id">Add by matrix ID</string>
<string name="creating_direct_room">"Creating room…"</string>
<string name="direct_room_no_known_users">"No result found, use Add by matrix ID to search on server."</string>
<string name="direct_room_start_search">"Start typing to get results"</string>
<string name="direct_room_filter_hint">"Filter by username or ID…"</string>
</resources>