forked from GitHub-Mirror/riotX-android
Merge pull request #102 from vector-im/feature/completion
Add Slash command parser and handle room member invitation
This commit is contained in:
commit
eaff5ac9f0
@ -26,6 +26,10 @@ class RxRoom(private val room: Room) {
|
|||||||
return room.roomSummary.asObservable()
|
return room.roomSummary.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveRoomMemberIds(): Observable<List<String>> {
|
||||||
|
return room.getRoomMemberIdsLive().asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.rx(): RxRoom {
|
fun Room.rx(): RxRoom {
|
||||||
|
@ -17,16 +17,16 @@
|
|||||||
package im.vector.matrix.android.api.session.room
|
package im.vector.matrix.android.api.session.room
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import im.vector.matrix.android.api.session.room.members.RoomMembersService
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines methods to interact within a room.
|
* This interface defines methods to interact within a room.
|
||||||
*/
|
*/
|
||||||
interface Room : TimelineService, SendService, ReadService {
|
interface Room : TimelineService, SendService, ReadService, RoomMembersService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The roomId of this room
|
* The roomId of this room
|
||||||
@ -39,10 +39,4 @@ interface Room : TimelineService, SendService, ReadService {
|
|||||||
*/
|
*/
|
||||||
val roomSummary: LiveData<RoomSummary>
|
val roomSummary: LiveData<RoomSummary>
|
||||||
|
|
||||||
/**
|
|
||||||
* This methods load all room members if it was done yet.
|
|
||||||
* @return a [Cancelable]
|
|
||||||
*/
|
|
||||||
fun loadRoomMembersIfNeeded(): Cancelable
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.api.session.room.members
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines methods to retrieve room members of a room. It's implemented at the room level.
|
||||||
|
*/
|
||||||
|
interface RoomMembersService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods load all room members if it was done yet.
|
||||||
|
* @return a [Cancelable]
|
||||||
|
*/
|
||||||
|
fun loadRoomMembersIfNeeded(): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the roomMember with userId or null.
|
||||||
|
* @param userId the userId param to look for
|
||||||
|
*
|
||||||
|
* @return the roomMember with userId or null
|
||||||
|
*/
|
||||||
|
fun getRoomMember(userId: String): RoomMember?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the roomMembers ids of the room
|
||||||
|
*
|
||||||
|
* @return a [LiveData] of roomMember list.
|
||||||
|
*/
|
||||||
|
fun getRoomMemberIdsLive(): LiveData<List<String>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invite a user in the room
|
||||||
|
*/
|
||||||
|
fun invite(userId: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
}
|
@ -20,35 +20,31 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.members.RoomMembersService
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
|
||||||
import im.vector.matrix.android.internal.database.RealmLiveData
|
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
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.model.RoomSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
|
||||||
|
|
||||||
internal class DefaultRoom(
|
internal class DefaultRoom(
|
||||||
override val roomId: String,
|
override val roomId: String,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val timelineService: TimelineService,
|
private val timelineService: TimelineService,
|
||||||
private val sendService: SendService,
|
private val sendService: SendService,
|
||||||
private val readService: ReadService,
|
private val readService: ReadService,
|
||||||
private val taskExecutor: TaskExecutor
|
private val roomMembersService: RoomMembersService
|
||||||
|
|
||||||
|
|
||||||
) : Room,
|
) : Room,
|
||||||
TimelineService by timelineService,
|
TimelineService by timelineService,
|
||||||
SendService by sendService,
|
SendService by sendService,
|
||||||
ReadService by readService {
|
ReadService by readService,
|
||||||
|
RoomMembersService by roomMembersService {
|
||||||
|
|
||||||
override val roomSummary: LiveData<RoomSummary> by lazy {
|
override val roomSummary: LiveData<RoomSummary> by lazy {
|
||||||
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
|
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
|
||||||
@ -59,8 +55,4 @@ internal class DefaultRoom(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadRoomMembersIfNeeded(): Cancelable {
|
|
||||||
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
|
|
||||||
return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Event
|
|||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
|
import im.vector.matrix.android.internal.session.room.invite.InviteBody
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse
|
import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
||||||
@ -120,5 +121,14 @@ internal interface RoomAPI {
|
|||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers")
|
||||||
fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map<String, String>): Call<Unit>
|
fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map<String, String>): Call<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invite a user to the given room.
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-rooms-roomid-invite
|
||||||
|
*
|
||||||
|
* @param roomId the room id
|
||||||
|
* @param body a object that just contains a user id
|
||||||
|
*/
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
||||||
|
fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit>
|
||||||
|
|
||||||
}
|
}
|
@ -17,8 +17,9 @@
|
|||||||
package im.vector.matrix.android.internal.session.room
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
|
import im.vector.matrix.android.internal.session.room.invite.InviteTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService
|
||||||
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
|
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
|
||||||
@ -32,8 +33,8 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFact
|
|||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
|
||||||
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
|
private val inviteTask: InviteTask,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val credentials: Credentials,
|
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val setReadMarkersTask: SetReadMarkersTask,
|
private val setReadMarkersTask: SetReadMarkersTask,
|
||||||
@ -45,15 +46,16 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
|||||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
||||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||||
|
val roomMembersService = DefaultRoomMembersService(roomId, monarchy, loadRoomMembersTask, inviteTask, taskExecutor)
|
||||||
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
||||||
|
|
||||||
return DefaultRoom(
|
return DefaultRoom(
|
||||||
roomId,
|
roomId,
|
||||||
loadRoomMembersTask,
|
|
||||||
monarchy,
|
monarchy,
|
||||||
timelineService,
|
timelineService,
|
||||||
sendService,
|
sendService,
|
||||||
readService,
|
readService,
|
||||||
taskExecutor
|
roomMembersService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.session.room
|
|||||||
import im.vector.matrix.android.internal.session.DefaultSession
|
import im.vector.matrix.android.internal.session.DefaultSession
|
||||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.invite.DefaultInviteTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.invite.InviteTask
|
||||||
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
|
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
|
||||||
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||||
@ -70,5 +72,9 @@ class RoomModule {
|
|||||||
DefaultCreateRoomTask(get(), get()) as CreateRoomTask
|
DefaultCreateRoomTask(get(), get()) as CreateRoomTask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope(DefaultSession.SCOPE) {
|
||||||
|
DefaultInviteTask(get()) as InviteTask
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.invite
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class InviteBody(
|
||||||
|
@Json(name = "user_id") val userId: String
|
||||||
|
)
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.invite
|
||||||
|
|
||||||
|
import arrow.core.Try
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
|
||||||
|
|
||||||
|
internal interface InviteTask : Task<InviteTask.Params, Unit> {
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val userId: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultInviteTask(private val roomAPI: RoomAPI) : InviteTask {
|
||||||
|
|
||||||
|
override fun execute(params: InviteTask.Params): Try<Unit> {
|
||||||
|
return executeRequest {
|
||||||
|
val body = InviteBody(params.userId)
|
||||||
|
apiCall = roomAPI.invite(params.roomId, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.members
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.members.RoomMembersService
|
||||||
|
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.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.session.room.invite.InviteTask
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import im.vector.matrix.android.internal.util.fetchCopied
|
||||||
|
|
||||||
|
internal class DefaultRoomMembersService(private val roomId: String,
|
||||||
|
private val monarchy: Monarchy,
|
||||||
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
|
private val inviteTask: InviteTask,
|
||||||
|
private val taskExecutor: TaskExecutor
|
||||||
|
) : RoomMembersService {
|
||||||
|
|
||||||
|
override fun loadRoomMembersIfNeeded(): Cancelable {
|
||||||
|
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
|
||||||
|
return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRoomMember(userId: String): RoomMember? {
|
||||||
|
val eventEntity = monarchy.fetchCopied {
|
||||||
|
RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst()
|
||||||
|
}
|
||||||
|
return eventEntity?.asDomain()?.content.toModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRoomMemberIdsLive(): LiveData<List<String>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{
|
||||||
|
RoomMembers(it, roomId).queryRoomMembersEvent()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
it.stateKey!!
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invite(userId: String, callback: MatrixCallback<Unit>) {
|
||||||
|
val params = InviteTask.Params(roomId, userId)
|
||||||
|
inviteTask.configureWith(params)
|
||||||
|
.dispatchTo(callback)
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields
|
|||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
|
|
||||||
internal class RoomMembers(private val realm: Realm,
|
internal class RoomMembers(private val realm: Realm,
|
||||||
@ -47,10 +48,21 @@ internal class RoomMembers(private val realm: Realm,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLoaded(): Map<String, RoomMember> {
|
fun queryRoomMembersEvent(): RealmQuery<EventEntity> {
|
||||||
return EventEntity
|
return EventEntity
|
||||||
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
||||||
.sort(EventEntityFields.STATE_INDEX)
|
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
||||||
|
.distinct(EventEntityFields.STATE_KEY)
|
||||||
|
.isNotNull(EventEntityFields.CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
|
||||||
|
return queryRoomMembersEvent()
|
||||||
|
.equalTo(EventEntityFields.STATE_KEY, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLoaded(): Map<String, RoomMember> {
|
||||||
|
return queryRoomMembersEvent()
|
||||||
.findAll()
|
.findAll()
|
||||||
.map { it.asDomain() }
|
.map { it.asDomain() }
|
||||||
.associateBy { it.stateKey!! }
|
.associateBy { it.stateKey!! }
|
||||||
|
@ -171,6 +171,8 @@ dependencies {
|
|||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
implementation "ru.noties.markwon:html:$markwon_version"
|
implementation "ru.noties.markwon:html:$markwon_version"
|
||||||
|
|
||||||
|
implementation 'com.otaliastudios:autocomplete:1.1.0'
|
||||||
|
|
||||||
// Butterknife
|
// Butterknife
|
||||||
implementation 'com.jakewharton:butterknife:10.1.0'
|
implementation 'com.jakewharton:butterknife:10.1.0'
|
||||||
kapt 'com.jakewharton:butterknife-compiler:10.1.0'
|
kapt 'com.jakewharton:butterknife-compiler:10.1.0'
|
||||||
|
@ -27,7 +27,7 @@ import kotlin.reflect.KProperty
|
|||||||
* See [SampleKotlinModelWithHolder] for a usage example.
|
* See [SampleKotlinModelWithHolder] for a usage example.
|
||||||
*/
|
*/
|
||||||
abstract class VectorEpoxyHolder : EpoxyHolder() {
|
abstract class VectorEpoxyHolder : EpoxyHolder() {
|
||||||
private lateinit var view: View
|
lateinit var view: View
|
||||||
|
|
||||||
override fun bindView(itemView: View) {
|
override fun bindView(itemView: View) {
|
||||||
view = itemView
|
view = itemView
|
||||||
|
@ -19,6 +19,9 @@ package im.vector.riotredesign.core.epoxy
|
|||||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||||
import com.airbnb.epoxy.VisibilityState
|
import com.airbnb.epoxy.VisibilityState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EpoxyModelWithHolder which can listen to visibility state change
|
||||||
|
*/
|
||||||
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
|
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
|
||||||
|
|
||||||
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
|
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple generic listener interface
|
||||||
|
*/
|
||||||
|
interface AutocompleteClickListener<T> {
|
||||||
|
|
||||||
|
fun onItemClick(t: T)
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.DataSetObserver
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||||
|
import com.otaliastudios.autocomplete.AutocompletePresenter
|
||||||
|
|
||||||
|
abstract class EpoxyAutocompletePresenter<T>(context: Context) : AutocompletePresenter<T>(context), AutocompleteClickListener<T> {
|
||||||
|
|
||||||
|
private var recyclerView: EpoxyRecyclerView? = null
|
||||||
|
private var clicks: AutocompletePresenter.ClickProvider<T>? = null
|
||||||
|
private var observer: Observer? = null
|
||||||
|
|
||||||
|
override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider<T>) {
|
||||||
|
this.clicks = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerDataSetObserver(observer: DataSetObserver) {
|
||||||
|
this.observer = Observer(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getView(): ViewGroup? {
|
||||||
|
recyclerView = EpoxyRecyclerView(context).apply {
|
||||||
|
setController(providesController())
|
||||||
|
observer?.let {
|
||||||
|
adapter?.registerAdapterDataObserver(it)
|
||||||
|
}
|
||||||
|
itemAnimator = null
|
||||||
|
}
|
||||||
|
return recyclerView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewShown() {}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onViewHidden() {
|
||||||
|
recyclerView = null
|
||||||
|
observer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun providesController(): EpoxyController
|
||||||
|
|
||||||
|
protected fun dispatchLayoutChange() {
|
||||||
|
observer?.onChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(t: T) {
|
||||||
|
clicks?.click(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
root.onChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
|
root.onChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
|
root.onChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
root.onChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
|
root.onChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete.command
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
|
import im.vector.riotredesign.features.autocomplete.AutocompleteClickListener
|
||||||
|
import im.vector.riotredesign.features.command.Command
|
||||||
|
|
||||||
|
class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController<List<Command>>() {
|
||||||
|
|
||||||
|
var listener: AutocompleteClickListener<Command>? = null
|
||||||
|
|
||||||
|
override fun buildModels(data: List<Command>?) {
|
||||||
|
if (data.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.forEach { command ->
|
||||||
|
autocompleteCommandItem {
|
||||||
|
id(command.command)
|
||||||
|
name(command.command)
|
||||||
|
parameters(command.parameters)
|
||||||
|
description(stringProvider.getString(command.description))
|
||||||
|
clickListener { _ ->
|
||||||
|
listener?.onItemClick(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete.command
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_autocomplete_command)
|
||||||
|
abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var name: CharSequence? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var parameters: CharSequence? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var description: CharSequence? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var clickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
holder.view.setOnClickListener(clickListener)
|
||||||
|
|
||||||
|
holder.nameView.text = name
|
||||||
|
holder.parametersView.text = parameters
|
||||||
|
holder.descriptionView.text = description
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val nameView by bind<TextView>(R.id.commandName)
|
||||||
|
val parametersView by bind<TextView>(R.id.commandParameter)
|
||||||
|
val descriptionView by bind<TextView>(R.id.commandDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete.command
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.riotredesign.features.autocomplete.EpoxyAutocompletePresenter
|
||||||
|
import im.vector.riotredesign.features.command.Command
|
||||||
|
|
||||||
|
class AutocompleteCommandPresenter(context: Context,
|
||||||
|
private val controller: AutocompleteCommandController) :
|
||||||
|
EpoxyAutocompletePresenter<Command>(context) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
controller.listener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun providesController(): EpoxyController {
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQuery(query: CharSequence?) {
|
||||||
|
val data = Command.values().filter {
|
||||||
|
if (query.isNullOrEmpty()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
it.command.startsWith(query, 1, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.setData(data)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete.command
|
||||||
|
|
||||||
|
import android.text.Spannable
|
||||||
|
import com.otaliastudios.autocomplete.AutocompletePolicy
|
||||||
|
|
||||||
|
class CommandAutocompletePolicy : AutocompletePolicy {
|
||||||
|
override fun getQuery(text: Spannable): CharSequence {
|
||||||
|
if (text.length > 0) {
|
||||||
|
return text.substring(1, text.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not happen
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(text: Spannable?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only if text which starts with '/' and without space
|
||||||
|
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||||
|
return text?.startsWith("/") == true
|
||||||
|
&& !text.contains(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||||
|
return !shouldShowPopup(text, cursorPos)
|
||||||
|
}
|
||||||
|
}
|
@ -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.riotredesign.features.autocomplete.user
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
import im.vector.riotredesign.features.autocomplete.AutocompleteClickListener
|
||||||
|
|
||||||
|
class AutocompleteUserController : TypedEpoxyController<List<User>>() {
|
||||||
|
|
||||||
|
var listener: AutocompleteClickListener<User>? = null
|
||||||
|
|
||||||
|
override fun buildModels(data: List<User>?) {
|
||||||
|
if (data.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.forEach { user ->
|
||||||
|
autocompleteUserItem {
|
||||||
|
id(user.userId)
|
||||||
|
name(user.displayName)
|
||||||
|
avatarUrl(user.avatarUrl)
|
||||||
|
clickListener { _ ->
|
||||||
|
listener?.onItemClick(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.autocomplete.user
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
|
||||||
|
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var name: String? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var avatarUrl: String? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var clickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
holder.view.setOnClickListener(clickListener)
|
||||||
|
|
||||||
|
holder.nameView.text = name
|
||||||
|
AvatarRenderer.render(avatarUrl, name, holder.avatarImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val nameView by bind<TextView>(R.id.userAutocompleteName)
|
||||||
|
val avatarImageView by bind<ImageView>(R.id.userAutocompleteAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.riotredesign.features.autocomplete.user
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
import im.vector.riotredesign.features.autocomplete.EpoxyAutocompletePresenter
|
||||||
|
|
||||||
|
class AutocompleteUserPresenter(context: Context,
|
||||||
|
private val controller: AutocompleteUserController
|
||||||
|
) : EpoxyAutocompletePresenter<User>(context) {
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
controller.listener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun providesController(): EpoxyController {
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQuery(query: CharSequence?) {
|
||||||
|
callback?.onQueryUsers(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render(users: Async<List<User>>) {
|
||||||
|
if (users is Success) {
|
||||||
|
controller.setData(users())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onQueryUsers(query: CharSequence?)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.riotredesign.features.command
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the command line operations
|
||||||
|
* the user can write theses messages to perform some actions
|
||||||
|
* the list will be displayed in this order
|
||||||
|
*/
|
||||||
|
enum class Command(val command: String, val parameters: String, @StringRes val description: Int) {
|
||||||
|
EMOTE("/me", "<message>", R.string.command_description_emote),
|
||||||
|
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user),
|
||||||
|
UNBAN_USER("/unban", "<user-id>", R.string.command_description_unban_user),
|
||||||
|
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user),
|
||||||
|
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user),
|
||||||
|
INVITE("/invite", "<user-id>", R.string.command_description_invite_user),
|
||||||
|
JOIN_ROOM("/join", "<room-alias>", R.string.command_description_join_room),
|
||||||
|
PART("/part", "<room-alias>", R.string.command_description_part_room),
|
||||||
|
TOPIC("/topic", "<topic>", R.string.command_description_topic),
|
||||||
|
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
|
||||||
|
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
|
||||||
|
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
|
||||||
|
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token);
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.command
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.MatrixPatterns
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object CommandParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the text message into a Slash command.
|
||||||
|
*
|
||||||
|
* @param textMessage the text message
|
||||||
|
* @return a parsed slash command (ok or error)
|
||||||
|
*/
|
||||||
|
fun parseSplashCommand(textMessage: String): ParsedCommand {
|
||||||
|
// check if it has the Slash marker
|
||||||
|
if (!textMessage.startsWith("/")) {
|
||||||
|
return ParsedCommand.ErrorNotACommand
|
||||||
|
} else {
|
||||||
|
Timber.d("parseSplashCommand")
|
||||||
|
|
||||||
|
// "/" only
|
||||||
|
if (textMessage.length == 1) {
|
||||||
|
return ParsedCommand.ErrorEmptySlashCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude "//"
|
||||||
|
if ("/" == textMessage.substring(1, 2)) {
|
||||||
|
return ParsedCommand.ErrorNotACommand
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageParts: List<String>? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
messageParts = textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## manageSplashCommand() : split failed " + e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if the string cut fails
|
||||||
|
if (messageParts.isNullOrEmpty()) {
|
||||||
|
return ParsedCommand.ErrorEmptySlashCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
val slashCommand = messageParts[0]
|
||||||
|
|
||||||
|
when (slashCommand) {
|
||||||
|
Command.CHANGE_DISPLAY_NAME.command -> {
|
||||||
|
val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim()
|
||||||
|
|
||||||
|
return if (newDisplayName.isNotEmpty()) {
|
||||||
|
ParsedCommand.ChangeDisplayName(newDisplayName)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.TOPIC.command -> {
|
||||||
|
val newTopic = textMessage.substring(Command.TOPIC.command.length).trim()
|
||||||
|
|
||||||
|
return if (newTopic.isNotEmpty()) {
|
||||||
|
ParsedCommand.ChangeTopic(newTopic)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.TOPIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.EMOTE.command -> {
|
||||||
|
val message = textMessage.substring(Command.EMOTE.command.length).trim()
|
||||||
|
|
||||||
|
return ParsedCommand.SendEmote(message)
|
||||||
|
}
|
||||||
|
Command.JOIN_ROOM.command -> {
|
||||||
|
val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim()
|
||||||
|
|
||||||
|
return if (roomAlias.isNotEmpty()) {
|
||||||
|
ParsedCommand.JoinRoom(roomAlias)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.PART.command -> {
|
||||||
|
val roomAlias = textMessage.substring(Command.PART.command.length).trim()
|
||||||
|
|
||||||
|
return if (roomAlias.isNotEmpty()) {
|
||||||
|
ParsedCommand.PartRoom(roomAlias)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.PART)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.INVITE.command -> {
|
||||||
|
return if (messageParts.size == 2) {
|
||||||
|
val userId = messageParts[1]
|
||||||
|
|
||||||
|
if (MatrixPatterns.isUserId(userId)) {
|
||||||
|
ParsedCommand.Invite(userId)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.INVITE)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.INVITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.KICK_USER.command -> {
|
||||||
|
return if (messageParts.size >= 2) {
|
||||||
|
val userId = messageParts[1]
|
||||||
|
if (MatrixPatterns.isUserId(userId)) {
|
||||||
|
val reason = textMessage.substring(Command.KICK_USER.command.length
|
||||||
|
+ 1
|
||||||
|
+ userId.length).trim()
|
||||||
|
|
||||||
|
ParsedCommand.KickUser(userId, reason)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.KICK_USER)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.KICK_USER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.BAN_USER.command -> {
|
||||||
|
return if (messageParts.size >= 2) {
|
||||||
|
val userId = messageParts[1]
|
||||||
|
if (MatrixPatterns.isUserId(userId)) {
|
||||||
|
val reason = textMessage.substring(Command.BAN_USER.command.length
|
||||||
|
+ 1
|
||||||
|
+ userId.length).trim()
|
||||||
|
|
||||||
|
ParsedCommand.BanUser(userId, reason)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.BAN_USER)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.BAN_USER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.UNBAN_USER.command -> {
|
||||||
|
return if (messageParts.size == 2) {
|
||||||
|
val userId = messageParts[1]
|
||||||
|
|
||||||
|
if (MatrixPatterns.isUserId(userId)) {
|
||||||
|
ParsedCommand.UnbanUser(userId)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.SET_USER_POWER_LEVEL.command -> {
|
||||||
|
return if (messageParts.size == 3) {
|
||||||
|
val userId = messageParts[1]
|
||||||
|
if (MatrixPatterns.isUserId(userId)) {
|
||||||
|
val powerLevelsAsString = messageParts[2]
|
||||||
|
|
||||||
|
try {
|
||||||
|
val powerLevelsAsInt = Integer.parseInt(powerLevelsAsString)
|
||||||
|
|
||||||
|
ParsedCommand.SetUserPowerLevel(userId, powerLevelsAsInt)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.RESET_USER_POWER_LEVEL.command -> {
|
||||||
|
return if (messageParts.size == 2) {
|
||||||
|
val userId = messageParts[1]
|
||||||
|
|
||||||
|
if (MatrixPatterns.isUserId(userId)) {
|
||||||
|
ParsedCommand.SetUserPowerLevel(userId, 0)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.MARKDOWN.command -> {
|
||||||
|
return if (messageParts.size == 2) {
|
||||||
|
when {
|
||||||
|
"on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true)
|
||||||
|
"off".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(false)
|
||||||
|
else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.MARKDOWN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command.CLEAR_SCALAR_TOKEN.command -> {
|
||||||
|
return if (messageParts.size == 1) {
|
||||||
|
ParsedCommand.ClearScalarToken
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Unknown command
|
||||||
|
return ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.riotredesign.features.command
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent a parsed command
|
||||||
|
*/
|
||||||
|
sealed class ParsedCommand {
|
||||||
|
// This is not a Slash command
|
||||||
|
object ErrorNotACommand : ParsedCommand()
|
||||||
|
|
||||||
|
object ErrorEmptySlashCommand : ParsedCommand()
|
||||||
|
|
||||||
|
// Unknown/Unsupported slash command
|
||||||
|
class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand()
|
||||||
|
|
||||||
|
// A slash command is detected, but there is an error
|
||||||
|
class ErrorSyntax(val command: Command) : ParsedCommand()
|
||||||
|
|
||||||
|
// Valid commands:
|
||||||
|
|
||||||
|
class SendEmote(val message: String) : ParsedCommand()
|
||||||
|
class BanUser(val userId: String, val reason: String) : ParsedCommand()
|
||||||
|
class UnbanUser(val userId: String) : ParsedCommand()
|
||||||
|
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
|
||||||
|
class Invite(val userId: String) : ParsedCommand()
|
||||||
|
class JoinRoom(val roomAlias: String) : ParsedCommand()
|
||||||
|
class PartRoom(val roomAlias: String) : ParsedCommand()
|
||||||
|
class ChangeTopic(val topic: String) : ParsedCommand()
|
||||||
|
class KickUser(val userId: String, val reason: String) : ParsedCommand()
|
||||||
|
class ChangeDisplayName(val displayName: String) : ParsedCommand()
|
||||||
|
class SetMarkdown(val enable: Boolean) : ParsedCommand()
|
||||||
|
object ClearScalarToken : ParsedCommand()
|
||||||
|
}
|
@ -18,6 +18,10 @@ package im.vector.riotredesign.features.home
|
|||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
|
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandController
|
||||||
|
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
|
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserController
|
||||||
|
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
||||||
import im.vector.riotredesign.features.home.group.GroupSummaryController
|
import im.vector.riotredesign.features.home.group.GroupSummaryController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
||||||
@ -75,6 +79,15 @@ class HomeModule {
|
|||||||
GroupSummaryController()
|
GroupSummaryController()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
|
||||||
|
val commandController = AutocompleteCommandController(get())
|
||||||
|
AutocompleteCommandPresenter(fragment.requireContext(), commandController)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
|
||||||
|
val userController = AutocompleteUserController()
|
||||||
|
AutocompleteUserPresenter(fragment.requireContext(), userController)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,23 +16,43 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Spannable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.otaliastudios.autocomplete.Autocomplete
|
||||||
|
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||||
|
import com.otaliastudios.autocomplete.CharPolicy
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
|
import im.vector.riotredesign.core.extensions.observeEvent
|
||||||
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
|
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
|
||||||
|
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
||||||
|
import im.vector.riotredesign.features.command.Command
|
||||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
import im.vector.riotredesign.features.home.HomeModule
|
import im.vector.riotredesign.features.home.HomeModule
|
||||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||||
|
import im.vector.riotredesign.features.html.PillImageSpan
|
||||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||||
import im.vector.riotredesign.features.media.MediaViewerActivity
|
import im.vector.riotredesign.features.media.MediaViewerActivity
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
@ -50,7 +70,7 @@ data class RoomDetailArgs(
|
|||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback {
|
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -61,8 +81,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val session by inject<Session>()
|
||||||
|
private val glideRequests by lazy {
|
||||||
|
GlideApp.with(this)
|
||||||
|
}
|
||||||
|
|
||||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
|
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||||
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||||
|
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
|
||||||
|
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
|
||||||
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||||
|
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
@ -74,8 +102,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
setupSendButton()
|
setupComposer()
|
||||||
roomDetailViewModel.subscribe { renderState(it) }
|
roomDetailViewModel.subscribe { renderState(it) }
|
||||||
|
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||||
|
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@ -114,12 +144,73 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
timelineEventController.callback = this
|
timelineEventController.callback = this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSendButton() {
|
private fun setupComposer() {
|
||||||
|
val elevation = 6f
|
||||||
|
val backgroundDrawable = ColorDrawable(Color.WHITE)
|
||||||
|
Autocomplete.on<Command>(composerEditText)
|
||||||
|
.with(CommandAutocompletePolicy())
|
||||||
|
.with(autocompleteCommandPresenter)
|
||||||
|
.with(elevation)
|
||||||
|
.with(backgroundDrawable)
|
||||||
|
.with(object : AutocompleteCallback<Command> {
|
||||||
|
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
||||||
|
editable.clear()
|
||||||
|
editable
|
||||||
|
.append(item.command)
|
||||||
|
.append(" ")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
|
||||||
|
autocompleteUserPresenter.callback = this
|
||||||
|
Autocomplete.on<User>(composerEditText)
|
||||||
|
.with(CharPolicy('@', true))
|
||||||
|
.with(autocompleteUserPresenter)
|
||||||
|
.with(elevation)
|
||||||
|
.with(backgroundDrawable)
|
||||||
|
.with(object : AutocompleteCallback<User> {
|
||||||
|
override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
|
||||||
|
// Detect last '@' and remove it
|
||||||
|
var startIndex = editable.lastIndexOf("@")
|
||||||
|
if (startIndex == -1) {
|
||||||
|
startIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect next word separator
|
||||||
|
var endIndex = editable.indexOf(" ", startIndex)
|
||||||
|
if (endIndex == -1) {
|
||||||
|
endIndex = editable.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the word by its completion
|
||||||
|
val displayName = item.displayName ?: item.userId
|
||||||
|
|
||||||
|
// with a trailing space
|
||||||
|
editable.replace(startIndex, endIndex, "$displayName ")
|
||||||
|
|
||||||
|
// Add the span
|
||||||
|
val user = session.getUser(item.userId)
|
||||||
|
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
|
||||||
|
span.bind(composerEditText)
|
||||||
|
|
||||||
|
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
|
||||||
sendButton.setOnClickListener {
|
sendButton.setOnClickListener {
|
||||||
val textMessage = composerEditText.text.toString()
|
val textMessage = composerEditText.text.toString()
|
||||||
if (textMessage.isNotBlank()) {
|
if (textMessage.isNotBlank()) {
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||||
composerEditText.text = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,7 +233,44 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimelineEventController.Callback ************************************************************
|
private fun renderTextComposerState(state: TextComposerViewState) {
|
||||||
|
autocompleteUserPresenter.render(state.asyncUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
|
||||||
|
when (sendMessageResult) {
|
||||||
|
is SendMessageResult.MessageSent,
|
||||||
|
is SendMessageResult.SlashCommandHandled -> {
|
||||||
|
// Clear composer
|
||||||
|
composerEditText.text = null
|
||||||
|
}
|
||||||
|
is SendMessageResult.SlashCommandError -> {
|
||||||
|
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||||
|
}
|
||||||
|
is SendMessageResult.SlashCommandUnknown -> {
|
||||||
|
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||||
|
}
|
||||||
|
is SendMessageResult.SlashCommandResultOk -> {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
is SendMessageResult.SlashCommandResultError -> {
|
||||||
|
displayCommandError(sendMessageResult.throwable.localizedMessage)
|
||||||
|
}
|
||||||
|
is SendMessageResult.SlashCommandNotImplemented -> {
|
||||||
|
displayCommandError(getString(R.string.not_implemented))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayCommandError(message: String) {
|
||||||
|
AlertDialog.Builder(activity!!)
|
||||||
|
.setTitle(R.string.command_error)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimelineEventController.Callback ************************************************************
|
||||||
|
|
||||||
override fun onUrlClicked(url: String) {
|
override fun onUrlClicked(url: String) {
|
||||||
homePermalinkHandler.launch(url)
|
homePermalinkHandler.launch(url)
|
||||||
@ -157,4 +285,9 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
|
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
@ -24,6 +26,9 @@ import im.vector.matrix.android.api.session.Session
|
|||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotredesign.core.utils.LiveEvent
|
||||||
|
import im.vector.riotredesign.features.command.CommandParser
|
||||||
|
import im.vector.riotredesign.features.command.ParsedCommand
|
||||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
@ -63,17 +68,100 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
|
|
||||||
fun process(action: RoomDetailActions) {
|
fun process(action: RoomDetailActions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
|
||||||
|
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
||||||
|
get() = _sendMessageResultLiveData
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||||
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
|
// Handle slash command
|
||||||
|
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||||
|
|
||||||
|
when (slashCommandResult) {
|
||||||
|
is ParsedCommand.ErrorNotACommand -> {
|
||||||
|
// Send the text message to the room
|
||||||
|
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||||
|
}
|
||||||
|
is ParsedCommand.ErrorSyntax -> {
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
|
||||||
|
}
|
||||||
|
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
|
||||||
|
}
|
||||||
|
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
|
||||||
|
}
|
||||||
|
is ParsedCommand.Invite -> {
|
||||||
|
handleInviteSlashCommand(slashCommandResult)
|
||||||
|
}
|
||||||
|
is ParsedCommand.SetUserPowerLevel -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.ClearScalarToken -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.SetMarkdown -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.UnbanUser -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.BanUser -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.KickUser -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.JoinRoom -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.PartRoom -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.SendEmote -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.ChangeTopic -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
is ParsedCommand.ChangeDisplayName -> {
|
||||||
|
// TODO
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||||
|
|
||||||
|
room.invite(invite.userId, object : MatrixCallback<Unit> {
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure)))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||||
|
@ -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.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import im.vector.riotredesign.features.command.Command
|
||||||
|
|
||||||
|
sealed class SendMessageResult {
|
||||||
|
object MessageSent : SendMessageResult()
|
||||||
|
class SlashCommandError(val command: Command) : SendMessageResult()
|
||||||
|
class SlashCommandUnknown(val command: String) : SendMessageResult()
|
||||||
|
object SlashCommandHandled : SendMessageResult()
|
||||||
|
object SlashCommandResultOk : SendMessageResult()
|
||||||
|
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
|
||||||
|
// TODO Remove
|
||||||
|
object SlashCommandNotImplemented : SendMessageResult()
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.home.room.detail.composer
|
||||||
|
|
||||||
|
sealed class TextComposerActions {
|
||||||
|
data class QueryUsers(val query: CharSequence?) : TextComposerActions()
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.functions.BiFunction
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
typealias AutocompleteUserQuery = CharSequence
|
||||||
|
|
||||||
|
class TextComposerViewModel(initialState: TextComposerViewState,
|
||||||
|
private val session: Session
|
||||||
|
) : VectorViewModel<TextComposerViewState>(initialState) {
|
||||||
|
|
||||||
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
|
private val roomId = initialState.roomId
|
||||||
|
|
||||||
|
private val usersQueryObservable = BehaviorRelay.create<Option<AutocompleteUserQuery>>()
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<TextComposerViewModel, TextComposerViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? {
|
||||||
|
val currentSession = viewModelContext.activity.get<Session>()
|
||||||
|
return TextComposerViewModel(state, currentSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeUsersQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun process(action: TextComposerActions) {
|
||||||
|
when (action) {
|
||||||
|
is TextComposerActions.QueryUsers -> handleQueryUsers(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleQueryUsers(action: TextComposerActions.QueryUsers) {
|
||||||
|
val query = Option.fromNullable(action.query)
|
||||||
|
usersQueryObservable.accept(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeUsersQuery() {
|
||||||
|
Observable.combineLatest<List<String>, Option<AutocompleteUserQuery>, List<User>>(
|
||||||
|
room.rx().liveRoomMemberIds(),
|
||||||
|
usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||||
|
BiFunction { roomMembers, query ->
|
||||||
|
val users = roomMembers
|
||||||
|
.mapNotNull {
|
||||||
|
session.getUser(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val filter = query.orNull()
|
||||||
|
if (filter.isNullOrBlank()) {
|
||||||
|
users
|
||||||
|
} else {
|
||||||
|
users.filter {
|
||||||
|
it.displayName?.startsWith(prefix = filter, ignoreCase = true)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).execute { async ->
|
||||||
|
copy(
|
||||||
|
asyncUsers = async
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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.riotredesign.features.home.room.detail.composer
|
||||||
|
|
||||||
|
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
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
|
||||||
|
|
||||||
|
|
||||||
|
data class TextComposerViewState(val roomId: String,
|
||||||
|
val asyncUsers: Async<List<User>> = Uninitialized
|
||||||
|
) : MvRxState {
|
||||||
|
|
||||||
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
|
||||||
|
|
||||||
|
}
|
@ -36,7 +36,6 @@ import java.lang.ref.WeakReference
|
|||||||
* This span is able to replace a text by a [ChipDrawable]
|
* This span is able to replace a text by a [ChipDrawable]
|
||||||
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
|
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class PillImageSpan(private val glideRequests: GlideRequests,
|
class PillImageSpan(private val glideRequests: GlideRequests,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
|
46
vector/src/main/res/layout/item_autocomplete_command.xml
Normal file
46
vector/src/main/res/layout/item_autocomplete_command.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="6dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/commandName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="/invite" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/commandParameter"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_toEndOf="@+id/commandName"
|
||||||
|
android:layout_toRightOf="@+id/commandName"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="italic"
|
||||||
|
tools:text="<user-id>" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/commandDescription"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/commandName"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="@string/command_description_invite_user" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
28
vector/src/main/res/layout/item_autocomplete_user.xml
Normal file
28
vector/src/main/res/layout/item_autocomplete_user.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/userAutocompleteAvatar"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/userAutocompleteName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="name" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
Loading…
Reference in New Issue
Block a user