mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-02 20:26:47 +01:00
Merge pull request #437 from vector-im/feature/create_direct_room
Feature/create direct room
This commit is contained in:
commit
ab25980c4e
@ -2,7 +2,7 @@ Changes in RiotX 0.3.0 (2019-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
-
|
||||
- Create Direct Room flow
|
||||
|
||||
Improvements:
|
||||
- UI for pending edits (#193)
|
||||
|
@ -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'
|
||||
|
@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.MainThreadDisposable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
private class LiveDataObservable<T>(
|
||||
private val liveData: LiveData<T>,
|
||||
@ -57,5 +59,5 @@ private class LiveDataObservable<T>(
|
||||
}
|
||||
|
||||
fun <T> LiveData<T>.asObservable(): Observable<T> {
|
||||
return LiveDataObservable(this)
|
||||
return LiveDataObservable(this).observeOn(Schedulers.computation())
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.CompletableEmitter
|
||||
import io.reactivex.SingleEmitter
|
||||
|
||||
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
completableEmitter.onComplete()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
completableEmitter.onError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
|
||||
completableEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.SingleEmitter
|
||||
|
||||
internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
singleEmitter.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
singleEmitter.onError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
|
||||
singleEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@ -21,24 +21,28 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.Single
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
fun liveRoomSummary(): Observable<RoomSummary> {
|
||||
return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation())
|
||||
return room.liveRoomSummary().asObservable()
|
||||
}
|
||||
|
||||
fun liveRoomMemberIds(): Observable<List<String>> {
|
||||
return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation())
|
||||
return room.getRoomMemberIdsLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
|
||||
return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation())
|
||||
return room.getEventSummaryLive(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
|
||||
return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation())
|
||||
return room.liveTimeLineEvent(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun loadRoomMembersIfNeeded(): Single<Boolean> = Single.create {
|
||||
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,30 +16,51 @@
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.Single
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
fun liveRoomSummaries(): Observable<List<RoomSummary>> {
|
||||
return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation())
|
||||
return session.liveRoomSummaries().asObservable()
|
||||
}
|
||||
|
||||
fun liveGroupSummaries(): Observable<List<GroupSummary>> {
|
||||
return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation())
|
||||
return session.liveGroupSummaries().asObservable()
|
||||
}
|
||||
|
||||
fun liveSyncState(): Observable<SyncState> {
|
||||
return session.syncState().asObservable().observeOn(Schedulers.computation())
|
||||
return session.syncState().asObservable()
|
||||
}
|
||||
|
||||
fun livePushers(): Observable<List<Pusher>> {
|
||||
return session.livePushers().asObservable().observeOn(Schedulers.computation())
|
||||
return session.livePushers().asObservable()
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
return session.liveUsers().asObservable()
|
||||
}
|
||||
|
||||
fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
|
||||
return session.livePagedUsers(filter).asObservable()
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
|
||||
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun searchUsersDirectory(search: String,
|
||||
limit: Int,
|
||||
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
|
||||
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.pushrules
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface PushRuleService {
|
||||
|
||||
@ -31,7 +32,7 @@ interface PushRuleService {
|
||||
|
||||
//TODO update rule
|
||||
|
||||
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>)
|
||||
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun addPushRuleListener(listener: PushRuleListener)
|
||||
|
||||
|
@ -30,20 +30,17 @@ interface RoomDirectoryService {
|
||||
/**
|
||||
* Get rooms from directory
|
||||
*/
|
||||
fun getPublicRooms(server: String?,
|
||||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
*/
|
||||
fun joinRoom(roomId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||
*/
|
||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>)
|
||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable
|
||||
|
||||
}
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to get rooms. It's implemented at the session level.
|
||||
@ -27,10 +28,9 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
interface RoomService {
|
||||
|
||||
/**
|
||||
* Create a room
|
||||
* Create a room asynchronously
|
||||
*/
|
||||
fun createRoom(createRoomParams: CreateRoomParams,
|
||||
callback: MatrixCallback<String>)
|
||||
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Get a room from a roomId
|
||||
|
@ -30,7 +30,7 @@ interface MembershipService {
|
||||
* This methods load all room members if it was done yet.
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun loadRoomMembersIfNeeded(): Cancelable
|
||||
fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Boolean>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the roomMember with userId or null.
|
||||
@ -52,16 +52,16 @@ interface MembershipService {
|
||||
/**
|
||||
* Invite a user in the room
|
||||
*/
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>)
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Join the room, or accept an invitation.
|
||||
*/
|
||||
fun join(callback: MatrixCallback<Unit>)
|
||||
fun join(callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Leave the room, or reject an invitation.
|
||||
*/
|
||||
fun leave(callback: MatrixCallback<Unit>)
|
||||
fun leave(callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
}
|
@ -17,7 +17,10 @@
|
||||
package im.vector.matrix.android.api.session.user
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to get users. It's implemented at the session level.
|
||||
@ -31,11 +34,34 @@ interface UserService {
|
||||
*/
|
||||
fun getUser(userId: String): User?
|
||||
|
||||
/**
|
||||
* Search list of users on server directory.
|
||||
* @param search the searched term
|
||||
* @param limit the max number of users to return
|
||||
* @param excludedUserIds the user ids to filter from the search
|
||||
* @param callback the async callback
|
||||
* @return Cancelable
|
||||
*/
|
||||
fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set<String>, callback: MatrixCallback<List<User>>): Cancelable
|
||||
|
||||
/**
|
||||
* Observe a live user from a userId
|
||||
* @param userId the userId to look for.
|
||||
* @return a Livedata of user with userId
|
||||
*/
|
||||
fun observeUser(userId: String): LiveData<User?>
|
||||
fun liveUser(userId: String): LiveData<User?>
|
||||
|
||||
/**
|
||||
* Observe a live list of users sorted alphabetically
|
||||
* @return a Livedata of users
|
||||
*/
|
||||
fun liveUsers(): LiveData<List<User>>
|
||||
|
||||
/**
|
||||
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
|
||||
* @param filter the filter. It will look into userId and displayName.
|
||||
* @return a Livedata of users
|
||||
*/
|
||||
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>>
|
||||
|
||||
}
|
@ -19,14 +19,17 @@ package im.vector.matrix.android.internal.database
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import io.realm.*
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val THREAD_NAME = "REALM_QUERY_LATCH"
|
||||
|
||||
class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
|
||||
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {
|
||||
|
||||
fun await() {
|
||||
@Throws(InterruptedException::class)
|
||||
fun await(timeout: Long = Long.MAX_VALUE, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) {
|
||||
val latch = CountDownLatch(1)
|
||||
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
|
||||
handlerThread.start()
|
||||
@ -46,8 +49,13 @@ class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConf
|
||||
})
|
||||
}
|
||||
handler.post(runnable)
|
||||
latch.await()
|
||||
handlerThread.quit()
|
||||
try {
|
||||
latch.await(timeout, timeUnit)
|
||||
} catch (exception: InterruptedException) {
|
||||
throw exception
|
||||
} finally {
|
||||
handlerThread.quit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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<String> = RealmList(),
|
||||
var notificationCount: Int = 0,
|
||||
var highlightCount: Int = 0,
|
||||
|
@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
|
||||
@ -29,3 +30,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
|
||||
return RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
.findAll()
|
||||
}
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): Boolean {
|
||||
return RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
.findAll()
|
||||
.isNotEmpty()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -65,11 +65,12 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm:
|
||||
|
||||
internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
|
||||
roomId: String,
|
||||
includesSending: Boolean,
|
||||
includedTypes: List<String> = emptyList(),
|
||||
excludedTypes: List<String> = emptyList()): TimelineEventEntity? {
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
||||
val eventList = if (roomEntity.sendingTimelineEvents.isNotEmpty()) {
|
||||
val eventList = if (includesSending && roomEntity.sendingTimelineEvents.isNotEmpty()) {
|
||||
roomEntity.sendingTimelineEvents
|
||||
} else {
|
||||
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents
|
||||
|
@ -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 {
|
||||
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.pushrules.Action
|
||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
|
||||
import im.vector.matrix.android.internal.database.model.PushRulesEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
@ -80,8 +81,8 @@ internal class DefaultPushRuleService @Inject constructor(
|
||||
return contentRules + overrideRules + roomRules + senderRules + underrideRules
|
||||
}
|
||||
|
||||
override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>) {
|
||||
updatePushRuleEnableStatusTask
|
||||
override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return updatePushRuleEnableStatusTask
|
||||
.configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled))
|
||||
// TODO Fetch the rules
|
||||
.dispatchTo(callback)
|
||||
|
@ -44,15 +44,15 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>) {
|
||||
joinRoomTask
|
||||
override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return joinRoomTask
|
||||
.configureWith(JoinRoomTask.Params(roomId))
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>) {
|
||||
getThirdPartyProtocolsTask
|
||||
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
|
||||
return getThirdPartyProtocolsTask
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
|
@ -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<String>) {
|
||||
createRoomTask
|
||||
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
|
||||
return createRoomTask
|
||||
.configureWith(createRoomParams)
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
|
@ -62,7 +62,9 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||
roomId: String,
|
||||
membership: Membership? = null,
|
||||
roomSummary: RoomSyncSummary? = null,
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null) {
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
||||
isDirect: Boolean? = null,
|
||||
directUserId: String? = null) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
@ -85,7 +87,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||
roomSummaryEntity.membership = membership
|
||||
}
|
||||
|
||||
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
|
||||
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
|
||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
|
||||
|
||||
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||
@ -95,6 +97,10 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||
.asSequence()
|
||||
.map { it.stateKey }
|
||||
|
||||
if (isDirect != null) {
|
||||
roomSummaryEntity.isDirect = isDirect
|
||||
roomSummaryEntity.directUserId = directUserId
|
||||
}
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
|
||||
|
@ -17,22 +17,33 @@
|
||||
package im.vector.matrix.android.internal.session.room.create
|
||||
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||
import im.vector.matrix.android.internal.database.RealmQueryLatch
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.RealmConfiguration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface CreateRoomTask : Task<CreateRoomParams, String>
|
||||
|
||||
internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: RoomAPI,
|
||||
private val monarchy: Monarchy,
|
||||
private val directChatsHelper: DirectChatsHelper,
|
||||
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
||||
private val readMarkersTask: SetReadMarkersTask,
|
||||
@SessionDatabase private val realmConfiguration: RealmConfiguration) : CreateRoomTask {
|
||||
|
||||
|
||||
@ -41,17 +52,50 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
|
||||
apiCall = roomAPI.createRoom(params)
|
||||
}.flatMap { createRoomResponse ->
|
||||
val roomId = createRoomResponse.roomId!!
|
||||
|
||||
// TODO Maybe do the same code for join room request ?
|
||||
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
|
||||
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
|
||||
realm.where(RoomEntity::class.java)
|
||||
.equalTo(RoomEntityFields.ROOM_ID, roomId)
|
||||
}
|
||||
|
||||
rql.await()
|
||||
|
||||
return Try.just(roomId)
|
||||
Try {
|
||||
rql.await(timeout = 20L, timeUnit = TimeUnit.SECONDS)
|
||||
roomId
|
||||
}
|
||||
}.flatMap { roomId ->
|
||||
if (params.isDirect()) {
|
||||
handleDirectChatCreation(params, roomId)
|
||||
} else {
|
||||
Try.just(roomId)
|
||||
}
|
||||
}.flatMap { roomId ->
|
||||
setReadMarkers(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String): Try<String> {
|
||||
val otherUserId = params.getFirstInvitedUserId()
|
||||
?: return Try.raise(IllegalStateException("You can't create a direct room without an invitedUser"))
|
||||
|
||||
return monarchy.tryTransactionSync { realm ->
|
||||
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||
this.directUserId = otherUserId
|
||||
this.isDirect = true
|
||||
}
|
||||
}.flatMap {
|
||||
val directChats = directChatsHelper.getDirectChats()
|
||||
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
|
||||
}.flatMap {
|
||||
Try.just(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setReadMarkers(roomId: String): Try<String> {
|
||||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
|
||||
return readMarkersTask
|
||||
.execute(setReadMarkerParams)
|
||||
.flatMap {
|
||||
Try.just(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,9 +42,11 @@ internal class DefaultMembershipService @Inject constructor(private val roomId:
|
||||
private val leaveRoomTask: LeaveRoomTask
|
||||
) : MembershipService {
|
||||
|
||||
override fun loadRoomMembersIfNeeded(): Cancelable {
|
||||
override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Boolean>): Cancelable {
|
||||
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
|
||||
return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
|
||||
return loadRoomMembersTask.configureWith(params)
|
||||
.dispatchTo(matrixCallback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getRoomMember(userId: String): RoomMember? {
|
||||
@ -73,23 +75,23 @@ internal class DefaultMembershipService @Inject constructor(private val roomId:
|
||||
return result
|
||||
}
|
||||
|
||||
override fun invite(userId: String, callback: MatrixCallback<Unit>) {
|
||||
override fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = InviteTask.Params(roomId, userId)
|
||||
inviteTask.configureWith(params)
|
||||
return inviteTask.configureWith(params)
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun join(callback: MatrixCallback<Unit>) {
|
||||
override fun join(callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = JoinRoomTask.Params(roomId)
|
||||
joinTask.configureWith(params)
|
||||
return joinTask.configureWith(params)
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun leave(callback: MatrixCallback<Unit>) {
|
||||
override fun leave(callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = LeaveRoomTask.Params(roomId)
|
||||
leaveRoomTask.configureWith(params)
|
||||
return leaveRoomTask.configureWith(params)
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.RealmResults
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -81,10 +80,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
||||
|
||||
val roomMembers = RoomMembers(realm, roomId)
|
||||
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll()
|
||||
val otherMembersSubset = loadedMembers.where()
|
||||
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
|
||||
.limit(3)
|
||||
.findAll()
|
||||
|
||||
|
||||
if (roomEntity?.membership == Membership.INVITE) {
|
||||
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
|
||||
@ -97,23 +93,29 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
||||
} else {
|
||||
context.getString(R.string.room_displayname_room_invite)
|
||||
}
|
||||
} else {
|
||||
} else if (roomEntity?.membership == Membership.JOIN) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
val memberIds: List<String> = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||
roomSummary.heroes
|
||||
val otherMembersSubset: List<EventEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||
roomSummary.heroes.mapNotNull {
|
||||
roomMembers.getStateEvent(it)
|
||||
}
|
||||
} else {
|
||||
otherMembersSubset.mapNotNull { it.stateKey }
|
||||
loadedMembers.where()
|
||||
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
|
||||
.limit(3)
|
||||
.findAll()
|
||||
}
|
||||
name = when (memberIds.size) {
|
||||
val otherMembersCount = roomMembers.getNumberOfMembers() - 1
|
||||
name = when (otherMembersCount) {
|
||||
0 -> context.getString(R.string.room_displayname_empty_room)
|
||||
1 -> resolveRoomMember(otherMembersSubset[0], roomMembers)
|
||||
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
|
||||
2 -> context.getString(R.string.room_displayname_two_members,
|
||||
resolveRoomMember(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMember(otherMembersSubset[1], roomMembers)
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
|
||||
)
|
||||
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||
roomMembers.getNumberOfJoinedMembers() - 1,
|
||||
resolveRoomMember(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
roomMembers.getNumberOfJoinedMembers() - 1)
|
||||
}
|
||||
}
|
||||
@ -122,8 +124,8 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
||||
return name ?: roomId
|
||||
}
|
||||
|
||||
private fun resolveRoomMember(eventEntity: EventEntity?,
|
||||
roomMembers: RoomMembers): String? {
|
||||
private fun resolveRoomMemberName(eventEntity: EventEntity?,
|
||||
roomMembers: RoomMembers): String? {
|
||||
if (eventEntity == null) return null
|
||||
val roomMember = eventEntity.toRoomMember() ?: return null
|
||||
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
|
||||
|
@ -42,12 +42,16 @@ internal class RoomMembers(private val realm: Realm,
|
||||
RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
}
|
||||
|
||||
fun get(userId: String): RoomMember? {
|
||||
fun getStateEvent(userId: String): EventEntity? {
|
||||
return EventEntity
|
||||
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
||||
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
||||
.equalTo(EventEntityFields.STATE_KEY, userId)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
fun get(userId: String): RoomMember? {
|
||||
return getStateEvent(userId)
|
||||
?.let {
|
||||
it.asDomain().content?.toModel<RoomMember>()
|
||||
}
|
||||
|
@ -17,10 +17,16 @@
|
||||
package im.vector.matrix.android.internal.session.room.membership.joining
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.database.RealmQueryLatch
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import io.realm.RealmConfiguration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
|
||||
@ -29,12 +35,32 @@ internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI) : JoinRoomTask {
|
||||
internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI,
|
||||
private val readMarkersTask: SetReadMarkersTask,
|
||||
@SessionDatabase private val realmConfiguration: RealmConfiguration) : JoinRoomTask {
|
||||
|
||||
override suspend fun execute(params: JoinRoomTask.Params): Try<Unit> {
|
||||
return executeRequest {
|
||||
return executeRequest<Unit> {
|
||||
apiCall = roomAPI.join(params.roomId, HashMap())
|
||||
}.flatMap {
|
||||
val roomId = params.roomId
|
||||
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
|
||||
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
|
||||
realm.where(RoomEntity::class.java)
|
||||
.equalTo(RoomEntityFields.ROOM_ID, roomId)
|
||||
}
|
||||
Try {
|
||||
rql.await(20L, TimeUnit.SECONDS)
|
||||
roomId
|
||||
}
|
||||
}.flatMap { roomId ->
|
||||
setReadMarkers(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setReadMarkers(roomId: String): Try<Unit> {
|
||||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
|
||||
return readMarkersTask.execute(setReadMarkerParams)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,14 +22,11 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultReadService @Inject constructor(private val roomId: String,
|
||||
@ -39,9 +36,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
|
||||
private val credentials: Credentials) : ReadService {
|
||||
|
||||
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
|
||||
//TODO shouldn't it be latest synced event?
|
||||
val latestEvent = getLatestEvent()
|
||||
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
|
||||
val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
|
||||
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@ -55,9 +50,6 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
|
||||
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
private fun getLatestEvent(): TimelineEventEntity? {
|
||||
return monarchy.fetchCopied { TimelineEventEntity.latestEvent(it, roomId) }
|
||||
}
|
||||
|
||||
override fun isEventRead(eventId: String): Boolean {
|
||||
var isEventRead = false
|
||||
|
@ -20,7 +20,6 @@ import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
@ -33,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -40,8 +40,9 @@ internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val fullyReadEventId: String?,
|
||||
val readReceiptEventId: String?
|
||||
val markAllAsRead: Boolean = false,
|
||||
val fullyReadEventId: String? = null,
|
||||
val readReceiptEventId: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@ -55,21 +56,35 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
|
||||
override suspend fun execute(params: SetReadMarkersTask.Params): Try<Unit> {
|
||||
val markers = HashMap<String, String>()
|
||||
if (params.fullyReadEventId != null) {
|
||||
if (LocalEchoEventFactory.isLocalEchoId(params.fullyReadEventId)) {
|
||||
val fullyReadEventId: String?
|
||||
val readReceiptEventId: String?
|
||||
|
||||
if (params.markAllAsRead) {
|
||||
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
|
||||
}
|
||||
fullyReadEventId = latestSyncedEventId
|
||||
readReceiptEventId = latestSyncedEventId
|
||||
} else {
|
||||
fullyReadEventId = params.fullyReadEventId
|
||||
readReceiptEventId = params.readReceiptEventId
|
||||
}
|
||||
|
||||
if (fullyReadEventId != null) {
|
||||
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
|
||||
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
|
||||
} else {
|
||||
markers[READ_MARKER] = params.fullyReadEventId
|
||||
markers[READ_MARKER] = fullyReadEventId
|
||||
}
|
||||
}
|
||||
if (params.readReceiptEventId != null
|
||||
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
|
||||
if (readReceiptEventId != null
|
||||
&& !isEventRead(params.roomId, readReceiptEventId)) {
|
||||
|
||||
if (LocalEchoEventFactory.isLocalEchoId(params.readReceiptEventId)) {
|
||||
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
|
||||
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
|
||||
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
|
||||
} else {
|
||||
updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
|
||||
markers[READ_RECEIPT] = params.readReceiptEventId
|
||||
updateNotificationCountIfNecessary(params.roomId, readReceiptEventId)
|
||||
markers[READ_RECEIPT] = readReceiptEventId
|
||||
}
|
||||
}
|
||||
return if (markers.isEmpty()) {
|
||||
@ -83,10 +98,10 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
|
||||
private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId)?.eventId == eventId
|
||||
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId
|
||||
if (isLatestReceived) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: return@tryTransactionAsync
|
||||
?: return@tryTransactionAsync
|
||||
roomSummary.notificationCount = 0
|
||||
roomSummary.highlightCount = 0
|
||||
}
|
||||
@ -97,13 +112,13 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
var isEventRead = false
|
||||
monarchy.doWithRealm {
|
||||
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
||||
?: return@doWithRealm
|
||||
?: return@doWithRealm
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
||||
?: return@doWithRealm
|
||||
?: return@doWithRealm
|
||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||
?: Int.MIN_VALUE
|
||||
?: Int.MIN_VALUE
|
||||
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
||||
?: Int.MAX_VALUE
|
||||
?: Int.MAX_VALUE
|
||||
isEventRead = eventToCheckIndex <= readReceiptIndex
|
||||
}
|
||||
return isEventRead
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,18 +18,46 @@ package im.vector.matrix.android.internal.session.user
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.DataSource
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||
import im.vector.matrix.android.internal.database.model.UserEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.user.model.SearchUserTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy) : UserService {
|
||||
internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy,
|
||||
private val searchUserTask: SearchUserTask,
|
||||
private val taskExecutor: TaskExecutor) : UserService {
|
||||
|
||||
private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy {
|
||||
monarchy.createDataSourceFactory { realm ->
|
||||
realm.where(UserEntity::class.java)
|
||||
.isNotEmpty(UserEntityFields.USER_ID)
|
||||
.sort(UserEntityFields.DISPLAY_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
private val domainDataSourceFactory: DataSource.Factory<Int, User> by lazy {
|
||||
realmDataSourceFactory.map {
|
||||
it.asDomain()
|
||||
}
|
||||
}
|
||||
|
||||
private val livePagedListBuilder: LivePagedListBuilder<Int, User> by lazy {
|
||||
LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build())
|
||||
}
|
||||
|
||||
override fun getUser(userId: String): User? {
|
||||
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
|
||||
@ -38,7 +66,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
|
||||
return userEntity.asDomain()
|
||||
}
|
||||
|
||||
override fun observeUser(userId: String): LiveData<User?> {
|
||||
override fun liveUser(userId: String): LiveData<User?> {
|
||||
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||
UserEntity.where(realm, userId)
|
||||
}
|
||||
@ -48,4 +76,45 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun liveUsers(): LiveData<List<User>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ realm ->
|
||||
realm.where(UserEntity::class.java)
|
||||
.isNotEmpty(UserEntityFields.USER_ID)
|
||||
.sort(UserEntityFields.DISPLAY_NAME)
|
||||
},
|
||||
{ it.asDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun livePagedUsers(filter: String?): LiveData<PagedList<User>> {
|
||||
realmDataSourceFactory.updateQuery { realm ->
|
||||
val query = realm.where(UserEntity::class.java)
|
||||
if (filter.isNullOrEmpty()) {
|
||||
query.isNotEmpty(UserEntityFields.USER_ID)
|
||||
} else {
|
||||
query
|
||||
.beginGroup()
|
||||
.contains(UserEntityFields.DISPLAY_NAME, filter)
|
||||
.or()
|
||||
.contains(UserEntityFields.USER_ID, filter)
|
||||
.endGroup()
|
||||
}
|
||||
query.sort(UserEntityFields.DISPLAY_NAME)
|
||||
}
|
||||
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
|
||||
}
|
||||
|
||||
|
||||
override fun searchUsersDirectory(search: String,
|
||||
limit: Int,
|
||||
excludedUserIds: Set<String>,
|
||||
callback: MatrixCallback<List<User>>): Cancelable {
|
||||
val params = SearchUserTask.Params(limit, search, excludedUserIds)
|
||||
return searchUserTask
|
||||
.configureWith(params)
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.user
|
||||
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0
|
||||
import im.vector.matrix.android.internal.session.user.model.SearchUsersParams
|
||||
import im.vector.matrix.android.internal.session.user.model.SearchUsersRequestResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
internal interface SearchUserAPI {
|
||||
|
||||
/**
|
||||
* Perform a user search.
|
||||
*
|
||||
* @param searchUsersParams the search params.
|
||||
*/
|
||||
@POST(URI_API_PREFIX_PATH_R0 + "user_directory/search")
|
||||
fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call<SearchUsersRequestResponse>
|
||||
}
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.user
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||
|
||||
@ -29,9 +30,13 @@ internal object UserEntityFactory {
|
||||
return null
|
||||
}
|
||||
val roomMember = event.content.toModel<RoomMember>() ?: return null
|
||||
// We only use JOIN and INVITED memberships to create User data
|
||||
if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) {
|
||||
return null
|
||||
}
|
||||
return UserEntity(event.stateKey ?: "",
|
||||
roomMember.displayName ?: "",
|
||||
roomMember.avatarUrl ?: ""
|
||||
roomMember.displayName ?: "",
|
||||
roomMember.avatarUrl ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.user.accountdata
|
||||
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface AccountDataAPI {
|
||||
|
||||
/**
|
||||
* Set some account_data for the client.
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param type the type
|
||||
* @param params the put params
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}")
|
||||
fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call<Unit>
|
||||
|
||||
/**
|
||||
* Gets a bearer token from the homeserver that the user can
|
||||
* present to a third party in order to prove their ownership
|
||||
* of the Matrix account they are logged into.
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param body the body content
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token")
|
||||
fun openIdToken(@Path("userId") userId: String, @Body body: Map<Any, Any>): Call<Map<Any, Any>>
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.user.accountdata
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.getDirectRooms
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DirectChatsHelper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {
|
||||
|
||||
fun getDirectChats(include: Pair<String, String>? = null, filterRoomId: String? = null): Map<String, List<String>> {
|
||||
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
||||
val directChatsMap = mutableMapOf<String, MutableList<String>>()
|
||||
for (directRoom in currentDirectRooms) {
|
||||
if (directRoom.roomId == filterRoomId) continue
|
||||
val directUserId = directRoom.directUserId ?: continue
|
||||
directChatsMap.getOrPut(directUserId, { arrayListOf() }).apply {
|
||||
add(directRoom.roomId)
|
||||
}
|
||||
}
|
||||
if (include != null) {
|
||||
directChatsMap.getOrPut(include.first, { arrayListOf() }).apply {
|
||||
if (contains(include.second)) {
|
||||
Timber.v("Direct chats already include room ${include.second} with user ${include.first}")
|
||||
} else {
|
||||
add(include.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
directChatsMap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.user.accountdata
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Params, Unit> {
|
||||
|
||||
interface Params {
|
||||
val type: String
|
||||
fun getData(): Any
|
||||
}
|
||||
|
||||
data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES,
|
||||
private val directMessages: Map<String, List<String>>
|
||||
) : Params {
|
||||
|
||||
override fun getData(): Any {
|
||||
return directMessages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
internal class DefaultUpdateUserAcountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI,
|
||||
private val credentials: Credentials) : UpdateUserAccountDataTask {
|
||||
|
||||
override suspend fun execute(params: UpdateUserAccountDataTask.Params): Try<Unit> {
|
||||
|
||||
return executeRequest {
|
||||
apiCall = accountDataApi.setAccountData(credentials.userId, params.type, params.getData())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.user.model
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.user.SearchUserAPI
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SearchUserTask : Task<SearchUserTask.Params, List<User>> {
|
||||
|
||||
data class Params(
|
||||
val limit: Int,
|
||||
val search: String,
|
||||
val excludedUserIds: Set<String>
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask {
|
||||
|
||||
override suspend fun execute(params: SearchUserTask.Params): Try<List<User>> {
|
||||
return executeRequest<SearchUsersRequestResponse> {
|
||||
apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit))
|
||||
}.map { response ->
|
||||
response.users.map {
|
||||
User(it.userId, it.displayName, it.avatarUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
package im.vector.matrix.android.internal.session.user.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class representing an users search response
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SearchUsersRequestResponse(
|
||||
@Json(name = "limited") val limited: Boolean = false,
|
||||
@Json(name = "results") val users: List<SearchUser> = emptyList()
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
@ -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"
|
||||
|
@ -64,6 +64,7 @@
|
||||
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
|
||||
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
|
||||
<activity android:name=".features.debug.DebugMenuActivity" />
|
||||
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.riotx.core.utils.FirstThrottler
|
||||
import im.vector.riotx.core.utils.EventObserver
|
||||
@ -44,3 +45,7 @@ inline fun <T> LiveData<LiveEvent<T>>.observeEventFirstThrottle(owner: Lifecycle
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun <T> MutableLiveData<LiveEvent<T>>.postLiveEvent(content: T) {
|
||||
this.postValue(LiveEvent(content))
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package im.vector.riotx.core.mvrx
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import im.vector.riotx.core.extensions.postLiveEvent
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
|
||||
abstract class NavigationViewModel<NavigationClass> : ViewModel() {
|
||||
@ -29,6 +30,6 @@ abstract class NavigationViewModel<NavigationClass> : ViewModel() {
|
||||
|
||||
|
||||
fun goTo(navigation: NavigationClass) {
|
||||
_navigateTo.postValue(LiveEvent(navigation))
|
||||
_navigateTo.postLiveEvent(navigation)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
@ -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)
|
||||
|
@ -16,20 +16,36 @@
|
||||
|
||||
package im.vector.riotx.core.platform
|
||||
|
||||
import com.airbnb.mvrx.BaseMvRxViewModel
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.*
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.riotx.BuildConfig
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
||||
: BaseMvRxViewModel<S>(initialState, false) {
|
||||
|
||||
protected val cancelableBag = CancelableBag()
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
cancelableBag.cancel()
|
||||
/**
|
||||
* This method does the same thing as the execute function, but it doesn't subscribe to the stream
|
||||
* so you can use this in a switchMap or a flatMap
|
||||
*/
|
||||
fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
|
||||
setState { stateReducer(Loading()) }
|
||||
return this.map { Success(it) as Async<T> }
|
||||
.onErrorReturn { Fail(it) }
|
||||
.doOnSuccess { setState { stateReducer(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does the same thing as the execute function, but it doesn't subscribe to the stream
|
||||
* so you can use this in a switchMap or a flatMap
|
||||
*/
|
||||
fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
|
||||
setState { stateReducer(Loading()) }
|
||||
return this.map { Success(it) as Async<T> }
|
||||
.onErrorReturn { Fail(it) }
|
||||
.doOnNext { setState { stateReducer(it) } }
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.internal.functions.Functions
|
||||
import timber.log.Timber
|
||||
|
||||
fun <T> Single<T>.subscribeLogError(): Disposable {
|
||||
return subscribe(Functions.emptyConsumer(), Consumer { Timber.e(it) })
|
||||
}
|
||||
|
||||
fun Completable.subscribeLogError(): Disposable {
|
||||
return subscribe({}, { Timber.e(it) })
|
||||
}
|
@ -43,6 +43,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
|
||||
@Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
super.injectWith(injector)
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
|
@ -26,10 +26,10 @@ import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.glide.GlideApp
|
||||
@ -41,7 +41,7 @@ import javax.inject.Inject
|
||||
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
|
||||
*/
|
||||
|
||||
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
|
||||
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
|
||||
|
||||
companion object {
|
||||
private const val THUMBNAIL_SIZE = 250
|
||||
@ -92,9 +92,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
|
||||
return if (text.isEmpty()) {
|
||||
TextDrawable.builder().buildRound("", avatarColor)
|
||||
} else {
|
||||
val isUserId = MatrixPatterns.isUserId(text)
|
||||
val firstLetterIndex = if (isUserId) 1 else 0
|
||||
val firstLetter = text[firstLetterIndex].toString().toUpperCase()
|
||||
val firstLetter = text.firstLetterOfDisplayName()
|
||||
TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.bold()
|
||||
|
@ -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 {
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.extensions.addFragment
|
||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||
import im.vector.riotx.core.extensions.observeEvent
|
||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import kotlinx.android.synthetic.main.activity.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||
|
||||
sealed class Navigation {
|
||||
object UsersDirectory : Navigation()
|
||||
object Close : Navigation()
|
||||
object Previous : Navigation()
|
||||
}
|
||||
|
||||
private val viewModel: CreateDirectRoomViewModel by viewModel()
|
||||
lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel
|
||||
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
super.injectWith(injector)
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
toolbar.visibility = View.GONE
|
||||
navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java)
|
||||
navigationViewModel.navigateTo.observeEvent(this) { navigation ->
|
||||
when (navigation) {
|
||||
is Navigation.UsersDirectory -> addFragmentToBackstack(CreateDirectRoomDirectoryUsersFragment(), R.id.container)
|
||||
Navigation.Close -> finish()
|
||||
Navigation.Previous -> onBackPressed()
|
||||
}
|
||||
}
|
||||
if (isFirstCreation()) {
|
||||
addFragment(CreateDirectRoomKnownUsersFragment(), R.id.container)
|
||||
}
|
||||
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
|
||||
renderCreateAndInviteState(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderCreateAndInviteState(state: Async<String>) {
|
||||
when (state) {
|
||||
is Loading -> renderCreationLoading()
|
||||
is Success -> renderCreationSuccess(state())
|
||||
is Fail -> renderCreationFailure(state.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderCreationLoading() {
|
||||
updateWaitingView(WaitingViewData(getString(R.string.creating_direct_room)))
|
||||
}
|
||||
|
||||
private fun renderCreationFailure(error: Throwable) {
|
||||
hideWaitingView()
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(errorFormatter.toHumanReadable(error))
|
||||
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun renderCreationSuccess(roomId: String?) {
|
||||
// Navigate to freshly created room
|
||||
if (roomId != null) {
|
||||
navigator.openRoom(this, roomId)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun getIntent(context: Context): Intent {
|
||||
return Intent(context, CreateDirectRoomActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.ScrollView
|
||||
import androidx.core.view.size
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.observeEvent
|
||||
import im.vector.riotx.core.extensions.setupAsSearch
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.DimensionUtils
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersController.Callback {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_create_direct_room
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_create_direct_room
|
||||
|
||||
private val viewModel: CreateDirectRoomViewModel by activityViewModel()
|
||||
|
||||
@Inject lateinit var directRoomController: KnownUsersController
|
||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||
private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java)
|
||||
vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar)
|
||||
setupRecyclerView()
|
||||
setupFilterView()
|
||||
setupAddByMatrixIdView()
|
||||
setupCloseView()
|
||||
viewModel.selectUserEvent.observeEvent(this) {
|
||||
updateChipsView(it)
|
||||
}
|
||||
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
|
||||
renderSelectedUsers(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
withState(viewModel) {
|
||||
val createMenuItem = menu.findItem(R.id.action_create_direct_room)
|
||||
val showMenuItem = it.selectedUsers.isNotEmpty()
|
||||
createMenuItem.setVisible(showMenuItem)
|
||||
}
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_create_direct_room -> {
|
||||
viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers)
|
||||
true
|
||||
}
|
||||
else ->
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAddByMatrixIdView() {
|
||||
addByMatrixId.setOnClickListener {
|
||||
navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.UsersDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
recyclerView.setHasFixedSize(true)
|
||||
// Don't activate animation as we might have way to much item animation when filtering
|
||||
recyclerView.itemAnimator = null
|
||||
directRoomController.callback = this
|
||||
recyclerView.setController(directRoomController)
|
||||
}
|
||||
|
||||
private fun setupFilterView() {
|
||||
createDirectRoomFilter
|
||||
.textChanges()
|
||||
.startWith(createDirectRoomFilter.text)
|
||||
.subscribe { text ->
|
||||
val filterValue = text.trim()
|
||||
val action = if (filterValue.isBlank()) {
|
||||
CreateDirectRoomActions.ClearFilterKnownUsers
|
||||
} else {
|
||||
CreateDirectRoomActions.FilterKnownUsers(filterValue.toString())
|
||||
}
|
||||
viewModel.handle(action)
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
createDirectRoomFilter.setupAsSearch()
|
||||
createDirectRoomFilter.requestFocus()
|
||||
}
|
||||
|
||||
private fun setupCloseView() {
|
||||
createDirectRoomClose.setOnClickListener {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
directRoomController.setData(it)
|
||||
}
|
||||
|
||||
private fun updateChipsView(data: SelectUserAction) {
|
||||
if (data.isAdded) {
|
||||
addChipToGroup(data.user, chipGroup)
|
||||
} else {
|
||||
if (chipGroup.size > data.index) {
|
||||
chipGroup.removeViewAt(data.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSelectedUsers(selectedUsers: Set<User>) {
|
||||
vectorBaseActivity.invalidateOptionsMenu()
|
||||
if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
|
||||
selectedUsers.forEach { addChipToGroup(it, chipGroup) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun addChipToGroup(user: User, chipGroup: ChipGroup) {
|
||||
val chip = Chip(requireContext())
|
||||
chip.setChipBackgroundColorResource(android.R.color.transparent)
|
||||
chip.chipStrokeWidth = DimensionUtils.dpToPx(1, requireContext()).toFloat()
|
||||
chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName
|
||||
chip.isClickable = true
|
||||
chip.isCheckable = false
|
||||
chip.isCloseIconVisible = true
|
||||
chipGroup.addView(chip)
|
||||
chip.setOnCloseIconClickListener {
|
||||
viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user))
|
||||
}
|
||||
chipGroupScrollView.post {
|
||||
chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(user: User) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(CreateDirectRoomActions.SelectUser(user))
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header)
|
||||
abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel<CreateDirectRoomLetterHeaderItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var letter: String = ""
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.letterView.text = letter
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val letterView by bind<TextView>(R.id.createDirectRoomLetterView)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import im.vector.riotx.core.mvrx.NavigationViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateDirectRoomNavigationViewModel @Inject constructor(): NavigationViewModel<CreateDirectRoomActivity.Navigation>()
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_create_direct_room_user)
|
||||
abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||
@EpoxyAttribute var name: String? = null
|
||||
@EpoxyAttribute var userId: String = ""
|
||||
@EpoxyAttribute var avatarUrl: String? = null
|
||||
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||
@EpoxyAttribute var selected: Boolean = false
|
||||
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.view.setOnClickListener(clickListener)
|
||||
// If name is empty, use userId as name and force it being centered
|
||||
if (name.isNullOrEmpty()) {
|
||||
holder.userIdView.visibility = View.GONE
|
||||
holder.nameView.text = userId
|
||||
} else {
|
||||
holder.userIdView.visibility = View.VISIBLE
|
||||
holder.nameView.text = name
|
||||
holder.userIdView.text = userId
|
||||
}
|
||||
renderSelection(holder, selected)
|
||||
}
|
||||
|
||||
private fun renderSelection(holder: Holder, isSelected: Boolean) {
|
||||
if (isSelected) {
|
||||
holder.avatarCheckedImageView.visibility = View.VISIBLE
|
||||
val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
|
||||
val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
|
||||
holder.avatarImageView.setImageDrawable(backgroundDrawable)
|
||||
} else {
|
||||
holder.avatarCheckedImageView.visibility = View.GONE
|
||||
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val userIdView by bind<TextView>(R.id.createDirectRoomUserID)
|
||||
val nameView by bind<TextView>(R.id.createDirectRoomUserName)
|
||||
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar)
|
||||
val avatarCheckedImageView by bind<ImageView>(R.id.createDirectRoomUserAvatarChecked)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.*
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.core.extensions.postLiveEvent
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.BiFunction
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private typealias KnowUsersFilter = String
|
||||
private typealias DirectoryUsersSearch = String
|
||||
|
||||
data class SelectUserAction(
|
||||
val user: User,
|
||||
val isAdded: Boolean,
|
||||
val index: Int
|
||||
)
|
||||
|
||||
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: CreateDirectRoomViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<CreateDirectRoomViewState>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel
|
||||
}
|
||||
|
||||
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
|
||||
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
|
||||
|
||||
private val _selectUserEvent = MutableLiveData<LiveEvent<SelectUserAction>>()
|
||||
val selectUserEvent: LiveData<LiveEvent<SelectUserAction>>
|
||||
get() = _selectUserEvent
|
||||
|
||||
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? {
|
||||
val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return activity.createDirectRoomViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeKnownUsers()
|
||||
observeDirectoryUsers()
|
||||
}
|
||||
|
||||
fun handle(action: CreateDirectRoomActions) {
|
||||
when (action) {
|
||||
is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers()
|
||||
is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
|
||||
is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
|
||||
is CreateDirectRoomActions.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
|
||||
is CreateDirectRoomActions.SelectUser -> handleSelectUser(action)
|
||||
is CreateDirectRoomActions.RemoveSelectedUser -> handleRemoveSelectedUser(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRoomAndInviteSelectedUsers() = withState { currentState ->
|
||||
val isDirect = currentState.selectedUsers.size == 1
|
||||
val roomParams = CreateRoomParams().apply {
|
||||
invitedUserIds = ArrayList(currentState.selectedUsers.map { it.userId })
|
||||
if (isDirect) {
|
||||
setDirectMessage()
|
||||
}
|
||||
}
|
||||
session.rx()
|
||||
.createRoom(roomParams)
|
||||
.execute {
|
||||
copy(createAndInviteState = it)
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state ->
|
||||
val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
|
||||
val selectedUsers = state.selectedUsers.minus(action.user)
|
||||
setState { copy(selectedUsers = selectedUsers) }
|
||||
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index))
|
||||
}
|
||||
|
||||
private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { state ->
|
||||
//Reset the filter asap
|
||||
directoryUsersSearch.accept("")
|
||||
val isAddOperation: Boolean
|
||||
val selectedUsers: Set<User>
|
||||
val indexOfUser = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
|
||||
val changeIndex: Int
|
||||
if (indexOfUser == -1) {
|
||||
changeIndex = state.selectedUsers.size
|
||||
selectedUsers = state.selectedUsers.plus(action.user)
|
||||
isAddOperation = true
|
||||
} else {
|
||||
changeIndex = indexOfUser
|
||||
selectedUsers = state.selectedUsers.minus(action.user)
|
||||
isAddOperation = false
|
||||
}
|
||||
setState { copy(selectedUsers = selectedUsers) }
|
||||
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex))
|
||||
}
|
||||
|
||||
private fun observeDirectoryUsers() {
|
||||
directoryUsersSearch
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.switchMapSingle { search ->
|
||||
val stream = if (search.isBlank()) {
|
||||
Single.just(emptyList())
|
||||
} else {
|
||||
session.rx()
|
||||
.searchUsersDirectory(search, 50, emptySet())
|
||||
.map { users ->
|
||||
users.sortedBy { it.displayName.firstLetterOfDisplayName() }
|
||||
}
|
||||
}
|
||||
stream.toAsync {
|
||||
copy(directoryUsers = it, directorySearchTerm = search)
|
||||
}
|
||||
}
|
||||
.subscribe()
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun observeKnownUsers() {
|
||||
knownUsersFilter
|
||||
.throttleLast(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.switchMap {
|
||||
session.rx().livePagedUsers(it.orNull())
|
||||
}
|
||||
.execute { async ->
|
||||
copy(
|
||||
knownUsers = async,
|
||||
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
||||
data class CreateDirectRoomViewState(
|
||||
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
||||
val directoryUsers: Async<List<User>> = Uninitialized,
|
||||
val selectedUsers: Set<User> = emptySet(),
|
||||
val createAndInviteState: Async<String> = Uninitialized,
|
||||
val directorySearchTerm: String = "",
|
||||
val filterKnownUsersValue: Option<String> = Option.empty()
|
||||
) : MvRxState {
|
||||
|
||||
enum class DisplayMode {
|
||||
KNOWN_USERS,
|
||||
DIRECTORY_USERS
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.mvrx.*
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
import im.vector.riotx.core.epoxy.noResultItem
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
class DirectoryUsersController @Inject constructor(private val session: Session,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val stringProvider: StringProvider,
|
||||
private val errorFormatter: ErrorFormatter) : EpoxyController() {
|
||||
|
||||
private var state: CreateDirectRoomViewState? = null
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setData(state: CreateDirectRoomViewState) {
|
||||
this.state = state
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
|
||||
override fun buildModels() {
|
||||
val currentState = state ?: return
|
||||
val hasSearch = currentState.directorySearchTerm.isNotBlank()
|
||||
val asyncUsers = currentState.directoryUsers
|
||||
when (asyncUsers) {
|
||||
is Uninitialized -> renderEmptyState(false)
|
||||
is Loading -> renderLoading()
|
||||
is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch)
|
||||
is Fail -> renderFailure(asyncUsers.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderLoading() {
|
||||
loadingItem {
|
||||
id("loading")
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderFailure(failure: Throwable) {
|
||||
errorWithRetryItem {
|
||||
id("error")
|
||||
text(errorFormatter.toHumanReadable(failure))
|
||||
listener { callback?.retryDirectoryUsersRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSuccess(users: List<User>,
|
||||
selectedUsers: List<String>,
|
||||
hasSearch: Boolean) {
|
||||
if (users.isEmpty()) {
|
||||
renderEmptyState(hasSearch)
|
||||
} else {
|
||||
renderUsers(users, selectedUsers)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
|
||||
for (user in users) {
|
||||
if (user.userId == session.myUserId) {
|
||||
continue
|
||||
}
|
||||
val isSelected = selectedUsers.contains(user.userId)
|
||||
createDirectRoomUserItem {
|
||||
id(user.userId)
|
||||
selected(isSelected)
|
||||
userId(user.userId)
|
||||
name(user.displayName)
|
||||
avatarUrl(user.avatarUrl)
|
||||
avatarRenderer(avatarRenderer)
|
||||
clickListener { _ ->
|
||||
callback?.onItemClick(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderEmptyState(hasSearch: Boolean) {
|
||||
val noResultRes = if (hasSearch) {
|
||||
R.string.no_result_placeholder
|
||||
} else {
|
||||
R.string.direct_room_start_search
|
||||
}
|
||||
noResultItem {
|
||||
id("noResult")
|
||||
text(stringProvider.getString(noResultRes))
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onItemClick(user: User)
|
||||
fun retryDirectoryUsersRequest()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.createdirect
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.internal.util.createUIHandler
|
||||
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.EmptyItem_
|
||||
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
||||
import im.vector.riotx.core.epoxy.loadingItem
|
||||
import im.vector.riotx.core.epoxy.noResultItem
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
class KnownUsersController @Inject constructor(private val session: Session,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val stringProvider: StringProvider) : PagedListEpoxyController<User>(
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
||||
private var selectedUsers: List<String> = emptyList()
|
||||
private var users: Async<List<User>> = Uninitialized
|
||||
private var isFiltering: Boolean = false
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setData(state: CreateDirectRoomViewState) {
|
||||
this.isFiltering = !state.filterKnownUsersValue.isEmpty()
|
||||
val newSelection = state.selectedUsers.map { it.userId }
|
||||
this.users = state.knownUsers
|
||||
if (newSelection != selectedUsers) {
|
||||
this.selectedUsers = newSelection
|
||||
requestForcedModelBuild()
|
||||
}
|
||||
submitList(state.knownUsers())
|
||||
}
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> {
|
||||
return if (item == null) {
|
||||
EmptyItem_().id(currentPosition)
|
||||
} else {
|
||||
val isSelected = selectedUsers.contains(item.userId)
|
||||
CreateDirectRoomUserItem_()
|
||||
.id(item.userId)
|
||||
.selected(isSelected)
|
||||
.userId(item.userId)
|
||||
.name(item.displayName)
|
||||
.avatarUrl(item.avatarUrl)
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.clickListener { _ ->
|
||||
callback?.onItemClick(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addModels(models: List<EpoxyModel<*>>) {
|
||||
if (users is Incomplete) {
|
||||
renderLoading()
|
||||
} else if (models.isEmpty()) {
|
||||
renderEmptyState()
|
||||
} else {
|
||||
var lastFirstLetter: String? = null
|
||||
for (model in models) {
|
||||
if (model is CreateDirectRoomUserItem) {
|
||||
if (model.userId == session.myUserId) continue
|
||||
val currentFirstLetter = model.name.firstLetterOfDisplayName()
|
||||
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
|
||||
lastFirstLetter = currentFirstLetter
|
||||
|
||||
CreateDirectRoomLetterHeaderItem_()
|
||||
.id(currentFirstLetter)
|
||||
.letter(currentFirstLetter)
|
||||
.addIf(showLetter, this)
|
||||
|
||||
model.addTo(this)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderLoading() {
|
||||
loadingItem {
|
||||
id("loading")
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderEmptyState() {
|
||||
noResultItem {
|
||||
id("noResult")
|
||||
text(stringProvider.getString(R.string.direct_room_no_known_users))
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onItemClick(user: User)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk))
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure)))
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)
|
||||
|
||||
room.invite(invite.userId, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk))
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure)))
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -452,19 +454,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
_downloadedFileEvent.postValue(LiveEvent(DownloadFileState(
|
||||
_downloadedFileEvent.postLiveEvent(DownloadFileState(
|
||||
action.messageFileContent.getMimeType(),
|
||||
data,
|
||||
null
|
||||
)))
|
||||
))
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_downloadedFileEvent.postValue(LiveEvent(DownloadFileState(
|
||||
_downloadedFileEvent.postLiveEvent(DownloadFileState(
|
||||
action.messageFileContent.getMimeType(),
|
||||
null,
|
||||
failure
|
||||
)))
|
||||
))
|
||||
}
|
||||
})
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
144
vector/src/main/res/layout/fragment_create_direct_room.xml
Normal file
144
vector/src/main/res/layout/fragment_create_direct_room.xml
Normal file
@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/createDirectRoomToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/createDirectRoomClose"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_x_18dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createDirectRoomTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/fab_menu_create_chat"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/createDirectRoomClose"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<im.vector.riotx.core.platform.MaxHeightScrollView
|
||||
android:id="@+id/chipGroupScrollView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomToolbar"
|
||||
app:maxHeight="64dp">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:lineSpacing="2dp" />
|
||||
|
||||
</im.vector.riotx.core.platform.MaxHeightScrollView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/createDirectRoomFilter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:background="@null"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/direct_room_filter_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxHeight="80dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />
|
||||
|
||||
<View
|
||||
android:id="@+id/createDirectRoomFilterDivider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilter" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/addByMatrixId"
|
||||
style="@style/VectorButtonStyleFlat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:minHeight="@dimen/layout_touch_size"
|
||||
android:text="@string/add_by_matrix_id"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_plus_circle"
|
||||
app:iconPadding="13dp"
|
||||
app:iconTint="@color/riotx_accent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterDivider" />
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fastScrollEnabled="true"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/addByMatrixId"
|
||||
tools:listitem="@layout/item_create_direct_room_user" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/createRoomToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/createDirectRoomClose"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_x_18dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createDirectRoomTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/direct_chats_header"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/createDirectRoomClose"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/createDirectRoomSearchByIdContainer"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createRoomToolbar">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/createDirectRoomSearchById"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/add_by_matrix_id" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/createDirectRoomFilterDivider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?attr/vctr_list_divider_color"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomSearchByIdContainer" />
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:fastScrollEnabled="true"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomFilterDivider"
|
||||
tools:listitem="@layout/item_create_direct_room_user" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/createDirectRoomLetterView"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:padding="8dp"
|
||||
android:textColor="?attr/riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="C" />
|
72
vector/src/main/res/layout/item_create_direct_room_user.xml
Normal file
72
vector/src/main/res/layout/item_create_direct_room_user.xml
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?riotx_background"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/createDirectRoomUserAvatarContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/createDirectRoomUserAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/createDirectRoomUserAvatarChecked"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_material_done"
|
||||
android:tint="@android:color/white"
|
||||
android:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createDirectRoomUserName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/createDirectRoomUserID"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatarContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createDirectRoomUserID"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/createDirectRoomUserName"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomUserName"
|
||||
tools:text="Blabla" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
10
vector/src/main/res/menu/vector_create_direct_room.xml
Executable file
10
vector/src/main/res/menu/vector_create_direct_room.xml
Executable file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_create_direct_room"
|
||||
android:title="@string/create_room_action_create"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="MaxHeightScrollView">
|
||||
<attr name="maxHeight" format="dimension" />
|
||||
</declare-styleable>
|
||||
</resources>
|
@ -2,5 +2,9 @@
|
||||
<resources>
|
||||
|
||||
<!-- Strings not defined in Riot -->
|
||||
|
||||
<string name="add_by_matrix_id">Add by matrix ID</string>
|
||||
<string name="creating_direct_room">"Creating room…"</string>
|
||||
<string name="direct_room_no_known_users">"No result found, use Add by matrix ID to search on server."</string>
|
||||
<string name="direct_room_start_search">"Start typing to get results"</string>
|
||||
<string name="direct_room_filter_hint">"Filter by username or ID…"</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user