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()
|
||||
}
|
||||
|
||||
fun liveRoomMemberIds(): Observable<List<String>> {
|
||||
return room.getRoomMemberIdsLive().asObservable()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
@ -17,16 +17,16 @@
|
||||
package im.vector.matrix.android.api.session.room
|
||||
|
||||
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.read.ReadService
|
||||
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.util.Cancelable
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -39,10 +39,4 @@ interface Room : TimelineService, SendService, ReadService {
|
||||
*/
|
||||
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 com.zhuinden.monarchy.Monarchy
|
||||
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.read.ReadService
|
||||
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.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.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||
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(
|
||||
override val roomId: String,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val timelineService: TimelineService,
|
||||
private val sendService: SendService,
|
||||
private val readService: ReadService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
private val roomMembersService: RoomMembersService
|
||||
|
||||
|
||||
) : Room,
|
||||
TimelineService by timelineService,
|
||||
SendService by sendService,
|
||||
ReadService by readService {
|
||||
ReadService by readService,
|
||||
RoomMembersService by roomMembersService {
|
||||
|
||||
override val roomSummary: LiveData<RoomSummary> by lazy {
|
||||
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.CreateRoomResponse
|
||||
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.send.SendResponse
|
||||
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")
|
||||
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
|
||||
|
||||
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.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.RoomMemberExtractor
|
||||
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
|
||||
|
||||
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val inviteTask: InviteTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val credentials: Credentials,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val setReadMarkersTask: SetReadMarkersTask,
|
||||
@ -45,15 +46,16 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||
val roomMembersService = DefaultRoomMembersService(roomId, monarchy, loadRoomMembersTask, inviteTask, taskExecutor)
|
||||
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
||||
|
||||
return DefaultRoom(
|
||||
roomId,
|
||||
loadRoomMembersTask,
|
||||
monarchy,
|
||||
timelineService,
|
||||
sendService,
|
||||
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.room.create.CreateRoomTask
|
||||
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.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
@ -70,5 +72,9 @@ class RoomModule {
|
||||
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.query.where
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.Sort
|
||||
|
||||
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
|
||||
.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()
|
||||
.map { it.asDomain() }
|
||||
.associateBy { it.stateKey!! }
|
||||
|
@ -171,6 +171,8 @@ dependencies {
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
implementation "ru.noties.markwon:html:$markwon_version"
|
||||
|
||||
implementation 'com.otaliastudios:autocomplete:1.1.0'
|
||||
|
||||
// Butterknife
|
||||
implementation 'com.jakewharton:butterknife: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.
|
||||
*/
|
||||
abstract class VectorEpoxyHolder : EpoxyHolder() {
|
||||
private lateinit var view: View
|
||||
lateinit var view: View
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
view = itemView
|
||||
|
@ -19,6 +19,9 @@ package im.vector.riotredesign.core.epoxy
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
|
||||
/**
|
||||
* EpoxyModelWithHolder which can listen to visibility state change
|
||||
*/
|
||||
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
|
||||
|
||||
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 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.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
||||
@ -75,6 +79,15 @@ class HomeModule {
|
||||
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
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
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.user.model.User
|
||||
import im.vector.riotredesign.R
|
||||
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.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.HomeModule
|
||||
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.helper.EndlessRecyclerViewScrollListener
|
||||
import im.vector.riotredesign.features.html.PillImageSpan
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
import im.vector.riotredesign.features.media.MediaViewerActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
@ -50,7 +70,7 @@ data class RoomDetailArgs(
|
||||
) : Parcelable
|
||||
|
||||
|
||||
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback {
|
||||
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback {
|
||||
|
||||
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 textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||
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 lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||
@ -74,8 +102,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||
setupRecyclerView()
|
||||
setupToolbar()
|
||||
setupSendButton()
|
||||
setupComposer()
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@ -114,12 +144,73 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
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 {
|
||||
val textMessage = composerEditText.text.toString()
|
||||
if (textMessage.isNotBlank()) {
|
||||
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) {
|
||||
homePermalinkHandler.launch(url)
|
||||
@ -157,4 +285,9 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
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
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
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.rx.rx
|
||||
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.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
@ -63,17 +68,100 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
|
||||
fun process(action: RoomDetailActions) {
|
||||
when (action) {
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
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 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) {
|
||||
|
@ -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]
|
||||
* 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,
|
||||
private val context: Context,
|
||||
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