diff --git a/CHANGES.md b/CHANGES.md index f96588e5ad..de52836bce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.3.0 (2019-XX-XX) =================================================== Features: - - + - Create Direct Room flow Improvements: - UI for pending edits (#193) diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 546922f25b..655df2c2ab 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -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' diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt index d4c9a79fc6..a1943bbe1c 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt @@ -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( private val liveData: LiveData, @@ -57,5 +59,5 @@ private class LiveDataObservable( } fun LiveData.asObservable(): Observable { - return LiveDataObservable(this) + return LiveDataObservable(this).observeOn(Schedulers.computation()) } \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt new file mode 100644 index 0000000000..5c6e749881 --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt @@ -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(private val completableEmitter: CompletableEmitter) : MatrixCallback { + + override fun onSuccess(data: T) { + completableEmitter.onComplete() + } + + override fun onFailure(failure: Throwable) { + completableEmitter.onError(failure) + } +} + +fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) { + completableEmitter.setCancellable { + this.cancel() + } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt new file mode 100644 index 0000000000..05756d49ae --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt @@ -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(private val singleEmitter: SingleEmitter) : MatrixCallback { + + override fun onSuccess(data: T) { + singleEmitter.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + singleEmitter.onError(failure) + } +} + +fun Cancelable.toSingle(singleEmitter: SingleEmitter) { + singleEmitter.setCancellable { + this.cancel() + } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 2c9c7d8b14..419fb6c913 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -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 { - return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation()) + return room.liveRoomSummary().asObservable() } fun liveRoomMemberIds(): Observable> { - return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation()) + return room.getRoomMemberIdsLive().asObservable() } fun liveAnnotationSummary(eventId: String): Observable { - return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation()) + return room.getEventSummaryLive(eventId).asObservable() } fun liveTimelineEvent(eventId: String): Observable { - return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation()) + return room.liveTimeLineEvent(eventId).asObservable() + } + + fun loadRoomMembersIfNeeded(): Single = Single.create { + room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) } } diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 30d31f942d..97661cebd1 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -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> { - return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation()) + return session.liveRoomSummaries().asObservable() } fun liveGroupSummaries(): Observable> { - return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation()) + return session.liveGroupSummaries().asObservable() } fun liveSyncState(): Observable { - return session.syncState().asObservable().observeOn(Schedulers.computation()) + return session.syncState().asObservable() } fun livePushers(): Observable> { - return session.livePushers().asObservable().observeOn(Schedulers.computation()) + return session.livePushers().asObservable() + } + + fun liveUsers(): Observable> { + return session.liveUsers().asObservable() + } + + fun livePagedUsers(filter: String? = null): Observable> { + return session.livePagedUsers(filter).asObservable() + } + + fun createRoom(roomParams: CreateRoomParams): Single = Single.create { + session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) + } + + fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set): Single> = Single.create { + session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index 5cb7f4ca0c..e843128e78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -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" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt index 1fc37cd467..28fcff0c76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -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) + fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable fun addPushRuleListener(listener: PushRuleListener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt index 5b66ddd8e7..272ab5675b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -30,20 +30,17 @@ interface RoomDirectoryService { /** * Get rooms from directory */ - fun getPublicRooms(server: String?, - publicRoomsParams: PublicRoomsParams, - callback: MatrixCallback): Cancelable + fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable /** * Join a room by id */ - fun joinRoom(roomId: String, - callback: MatrixCallback) + fun joinRoom(roomId: String, callback: MatrixCallback): 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>) + fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index fc0bf49955..837ea5bb84 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -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) + fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable /** * Get a room from a roomId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index ca3b99b6bf..870c1075de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -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): Cancelable /** * Return the roomMember with userId or null. @@ -52,16 +52,16 @@ interface MembershipService { /** * Invite a user in the room */ - fun invite(userId: String, callback: MatrixCallback) + fun invite(userId: String, callback: MatrixCallback): Cancelable /** * Join the room, or accept an invitation. */ - fun join(callback: MatrixCallback) + fun join(callback: MatrixCallback): Cancelable /** * Leave the room, or reject an invitation. */ - fun leave(callback: MatrixCallback) + fun leave(callback: MatrixCallback): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index eb09fbd20d..d3c58edd94 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -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, callback: MatrixCallback>): 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 + fun liveUser(userId: String): LiveData + + /** + * Observe a live list of users sorted alphabetically + * @return a Livedata of users + */ + fun liveUsers(): LiveData> + + /** + * 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> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 64afa3d494..1fc60d8098 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -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(private val realmConfiguration: RealmConfiguration, private val realmQueryBuilder: (Realm) -> RealmQuery) { - 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(private val realmConfiguration: RealmConf }) } handler.post(runnable) - latch.await() - handlerThread.quit() + try { + latch.await(timeout, timeUnit) + } catch (exception: InterruptedException) { + throw exception + } finally { + handlerThread.quit() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index c178711cb3..6f60fd944b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -32,6 +32,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 = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index 7cc0713fea..f2c260421f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -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 { @@ -29,3 +30,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n } return query } + +internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { + 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() +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 3669ada720..cfa5691447 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -65,11 +65,12 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, roomId: String, + includesSending: Boolean, includedTypes: List = emptyList(), excludedTypes: List = 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 1738ceddcd..36f428955c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt index 7397f0eefb..7cddebdeaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt @@ -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) { - updatePushRuleEnableStatusTask + override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + return updatePushRuleEnableStatusTask .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) // TODO Fetch the rules .dispatchTo(callback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt index 0b13fa3c18..158802f86c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -44,15 +44,15 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, callback: MatrixCallback) { - joinRoomTask + override fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable { + return joinRoomTask .configureWith(JoinRoomTask.Params(roomId)) .dispatchTo(callback) .executeBy(taskExecutor) } - override fun getThirdPartyProtocol(callback: MatrixCallback>) { - getThirdPartyProtocolsTask + override fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable { + return getThirdPartyProtocolsTask .toConfigurableTask() .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 2c0f1ce90c..7b54836802 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.Room 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.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 @@ -40,8 +41,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) { - createRoomTask + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { + return createRoomTask .configureWith(createRoomParams) .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 6bcac9b8f3..a65c466a68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -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()?.topic diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index 73d9b6f21d..f9cad783a6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -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 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(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 { + 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 { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + return readMarkersTask + .execute(setReadMarkerParams) + .flatMap { + Try.just(roomId) + } + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 01fb746133..405c1ad63e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -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): 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) { + override fun invite(userId: String, callback: MatrixCallback): Cancelable { val params = InviteTask.Params(roomId, userId) - inviteTask.configureWith(params) + return inviteTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) } - override fun join(callback: MatrixCallback) { + override fun join(callback: MatrixCallback): Cancelable { val params = JoinRoomTask.Params(roomId) - joinTask.configureWith(params) + return joinTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) } - override fun leave(callback: MatrixCallback) { + override fun leave(callback: MatrixCallback): Cancelable { val params = LeaveRoomTask.Params(roomId) - leaveRoomTask.configureWith(params) + return leaveRoomTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 948f1741db..53e9e55a96 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -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 = if (roomSummary?.heroes?.isNotEmpty() == true) { - roomSummary.heroes + val otherMembersSubset: List = 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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index fb8326f287..8db3f170ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -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() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 96454cbfeb..7d78069e78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -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 { @@ -29,12 +35,32 @@ internal interface JoinRoomTask : Task { ) } -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 { - return executeRequest { + return executeRequest { apiCall = roomAPI.join(params.roomId, 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(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 { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + return readMarkersTask.execute(setReadMarkerParams) + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index ff8999689c..7830ce0e66 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -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) { - //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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 2106ab5555..c32fc59845 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -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 { 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 { val markers = HashMap() - 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 215321bd42..4b42ee9970 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -18,27 +18,41 @@ 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.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 +69,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 +135,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 +145,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) @@ -169,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 } @@ -183,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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 9c8760493e..e0be3b14eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -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) + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 477d5a7854..8d47d401a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -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 by lazy { + monarchy.createDataSourceFactory { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + } + } + + private val domainDataSourceFactory: DataSource.Factory by lazy { + realmDataSourceFactory.map { + it.asDomain() + } + } + + private val livePagedListBuilder: LivePagedListBuilder 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 { + override fun liveUser(userId: String): LiveData { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> UserEntity.where(realm, userId) } @@ -48,4 +76,45 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .firstOrNull() } } -} \ No newline at end of file + + override fun liveUsers(): LiveData> { + 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> { + 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, + callback: MatrixCallback>): Cancelable { + val params = SearchUserTask.Params(limit, search, excludedUserIds) + return searchUserTask + .configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt new file mode 100644 index 0000000000..aa4d50df59 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -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 +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt index 188c7d84bf..7873bf2f98 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt @@ -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() ?: 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 ?: "" ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt index 00368dfa9d..46ae4e388b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt @@ -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 + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt new file mode 100644 index 0000000000..824af2d1c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt @@ -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 + + /** + * 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): Call> +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt new file mode 100644 index 0000000000..e4b76ca188 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt @@ -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 + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt new file mode 100644 index 0000000000..5d135b7bd5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt @@ -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? = null, filterRoomId: String? = null): Map> { + return Realm.getInstance(realmConfiguration).use { realm -> + val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + val directChatsMap = mutableMapOf>() + 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 + } + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt new file mode 100644 index 0000000000..57ee9632f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -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 { + + interface Params { + val type: String + fun getData(): Any + } + + data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES, + private val directMessages: Map> + ) : 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 { + + return executeRequest { + apiCall = accountDataApi.setAccountData(credentials.userId, params.type, params.getData()) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt new file mode 100644 index 0000000000..da447830d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt @@ -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 +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt new file mode 100644 index 0000000000..85264dba73 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt @@ -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> { + + data class Params( + val limit: Int, + val search: String, + val excludedUserIds: Set + ) +} + +internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask { + + override suspend fun execute(params: SearchUserTask.Params): Try> { + return executeRequest { + apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) + }.map { response -> + response.users.map { + User(it.userId, it.displayName, it.avatarUrl) + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt new file mode 100644 index 0000000000..6ea689e5f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt new file mode 100644 index 0000000000..b0a8f93720 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt @@ -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 = emptyList() +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt index a83ab0134c..a277498526 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -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() +} \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index db9ad6d63a..85a4dae165 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e0deced966..e4cdaee2e4 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -64,6 +64,7 @@ + diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index a42eec4940..35cda2e6c6 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 534a346a1c..80410f879f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt index ecc7795a7b..ace13754aa 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt index a278eab087..97215e1e0a 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt @@ -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 LiveData>.observeEventFirstThrottle(owner: Lifecycle } }) } + +fun MutableLiveData>.postLiveEvent(content: T) { + this.postValue(LiveEvent(content)) +} diff --git a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt index ab3ce7c82d..a6bf07e046 100644 --- a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt @@ -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 : ViewModel() { @@ -29,6 +30,6 @@ abstract class NavigationViewModel : ViewModel() { fun goTo(navigation: NavigationClass) { - _navigateTo.postValue(LiveEvent(navigation)) + _navigateTo.postLiveEvent(navigation) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt new file mode 100644 index 0000000000..92796bbda8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index 546937da59..ff30138990 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 92f72de66e..e9c60942b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -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 * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index ec5e419dee..aac19d8097 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index 1570a7f82e..1c2f1d53f0 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -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(initialState: S) : BaseMvRxViewModel(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 Single.toAsync(stateReducer: S.(Async) -> S): Single> { + setState { stateReducer(Loading()) } + return this.map { Success(it) as Async } + .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 Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { + setState { stateReducer(Loading()) } + return this.map { Success(it) as Async } + .onErrorReturn { Fail(it) } + .doOnNext { setState { stateReducer(it) } } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt b/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt new file mode 100644 index 0000000000..0541599125 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt @@ -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 Single.subscribeLogError(): Disposable { + return subscribe(Functions.emptyConsumer(), Consumer { Timber.e(it) }) +} + +fun Completable.subscribeLogError(): Disposable { + return subscribe({}, { Timber.e(it) }) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt index 32e0859706..69ad2cd1b7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -43,6 +43,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { @Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) injector.inject(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 83829e468c..2b6c1eb474 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -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 */ -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() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 4ec2c0ad95..07d9416cf5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -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 @@ -214,23 +213,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 { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 7f0b610d65..917cafe149 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index ac4cc08dfc..ad39839321 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt new file mode 100644 index 0000000000..50f99a6dc3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt @@ -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() + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt new file mode 100644 index 0000000000..13bc93686f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -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) { + 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) + } + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt new file mode 100644 index 0000000000..3916ff7bbb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -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)) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt new file mode 100644 index 0000000000..7747336627 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt @@ -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) { + 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)) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt new file mode 100644 index 0000000000..fcb3b10ca6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt @@ -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() { + + @EpoxyAttribute var letter: String = "" + + override fun bind(holder: Holder) { + holder.letterView.text = letter + } + + class Holder : VectorEpoxyHolder() { + val letterView by bind(R.id.createDirectRoomLetterView) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt new file mode 100644 index 0000000000..442dc23d23 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt @@ -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() \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt new file mode 100644 index 0000000000..c6d7f85b5a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -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() { + + @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(R.id.createDirectRoomUserID) + val nameView by bind(R.id.createDirectRoomUserName) + val avatarImageView by bind(R.id.createDirectRoomUserAvatar) + val avatarCheckedImageView by bind(R.id.createDirectRoomUserAvatarChecked) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt new file mode 100644 index 0000000000..b0fed9b8e8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt @@ -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(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel + } + + private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) + private val directoryUsersSearch = BehaviorRelay.create() + + private val _selectUserEvent = MutableLiveData>() + val selectUserEvent: LiveData> + get() = _selectUserEvent + + companion object : MvRxViewModelFactory { + + @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 + 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() + ) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt new file mode 100644 index 0000000000..e1c9ad4609 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt @@ -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> = Uninitialized, + val directoryUsers: Async> = Uninitialized, + val selectedUsers: Set = emptySet(), + val createAndInviteState: Async = Uninitialized, + val directorySearchTerm: String = "", + val filterKnownUsersValue: Option = Option.empty() +) : MvRxState { + + enum class DisplayMode { + KNOWN_USERS, + DIRECTORY_USERS + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt new file mode 100644 index 0000000000..c174ac6b46 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt @@ -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, + selectedUsers: List, + hasSearch: Boolean) { + if (users.isEmpty()) { + renderEmptyState(hasSearch) + } else { + renderUsers(users, selectedUsers) + } + } + + private fun renderUsers(users: List, selectedUsers: List) { + 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() + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt new file mode 100644 index 0000000000..fbb1cfcc4e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt @@ -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( + modelBuildingHandler = createUIHandler() +) { + + private var selectedUsers: List = emptyList() + private var users: Async> = 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>) { + 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) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 513379bdcf..7aff4a327d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d38561fa88..4b734c9557 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -40,10 +40,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 @@ -95,7 +97,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeInvitationState() - cancelableBag += room.loadRoomMembersIfNeeded() + room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } @@ -167,62 +169,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) } } } @@ -239,12 +241,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -254,12 +256,12 @@ 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? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -279,7 +281,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 { @@ -289,7 +291,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } } @@ -318,29 +320,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 { 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 { 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)) } }) } @@ -452,19 +454,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { 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 - ))) + )) } }) @@ -493,7 +495,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } else { // change timeline timeline.dispose() @@ -518,7 +520,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index b61e69ac7a..4e8fe28407 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index a1ae4fdf8a..f590a7897e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -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( diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2e34a7d1d8..1428a0ac89 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index bf888ffe55..c2da76432c 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index c47e8bbdbf..8d0b628482 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -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( diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml new file mode 100644 index 0000000000..427df61588 --- /dev/null +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml new file mode 100644 index 0000000000..8416f35dfa --- /dev/null +++ b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_create_direct_room_letter_header.xml b/vector/src/main/res/layout/item_create_direct_room_letter_header.xml new file mode 100644 index 0000000000..80a0fc4ce1 --- /dev/null +++ b/vector/src/main/res/layout/item_create_direct_room_letter_header.xml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_create_direct_room_user.xml b/vector/src/main/res/layout/item_create_direct_room_user.xml new file mode 100644 index 0000000000..fa7e742584 --- /dev/null +++ b/vector/src/main/res/layout/item_create_direct_room_user.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_create_direct_room.xml b/vector/src/main/res/menu/vector_create_direct_room.xml new file mode 100755 index 0000000000..8c6eab1c52 --- /dev/null +++ b/vector/src/main/res/menu/vector_create_direct_room.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/vector/src/main/res/values/attrs_max_height_scroll_view.xml b/vector/src/main/res/values/attrs_max_height_scroll_view.xml new file mode 100644 index 0000000000..1b13506674 --- /dev/null +++ b/vector/src/main/res/values/attrs_max_height_scroll_view.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 118fde8442..45dc5c5308 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -2,5 +2,9 @@ - + Add by matrix ID + "Creating room…" + "No result found, use Add by matrix ID to search on server." + "Start typing to get results" + "Filter by username or ID…" \ No newline at end of file