diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 2fbed52b..c9dbbdd0 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -26,6 +26,10 @@ class RxRoom(private val room: Room) { return room.roomSummary.asObservable() } + fun liveRoomMemberIds(): Observable> { + return room.getRoomMemberIdsLive().asObservable() + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 20ec0d53..cdc971fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -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 - /** - * This methods load all room members if it was done yet. - * @return a [Cancelable] - */ - fun loadRoomMembersIfNeeded(): Cancelable - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt new file mode 100644 index 00000000..930afd7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt @@ -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> + + /** + * Invite a user in the room + */ + fun invite(userId: String, callback: MatrixCallback) + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index ae6255a1..952a1eca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -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 by lazy { val liveRealmData = RealmLiveData(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) - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index bc44300f..1b89662d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -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): Call + /** + * 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 } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index b30ba3ee..720d3804 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -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 ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 34515d30..ccdfe9ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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 + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt new file mode 100644 index 00000000..652f8d63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt new file mode 100644 index 00000000..1086c6b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt @@ -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 { + data class Params( + val roomId: String, + val userId: String + ) +} + +internal class DefaultInviteTask(private val roomAPI: RoomAPI) : InviteTask { + + override fun execute(params: InviteTask.Params): Try { + return executeRequest { + val body = InviteBody(params.userId) + apiCall = roomAPI.invite(params.roomId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt new file mode 100644 index 00000000..a007a1eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt @@ -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> { + return monarchy.findAllMappedWithChanges( + { + RoomMembers(it, roomId).queryRoomMembersEvent() + }, + { + it.stateKey!! + } + ) + } + + override fun invite(userId: String, callback: MatrixCallback) { + val params = InviteTask.Params(roomId, userId) + inviteTask.configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt index 1bbb792d..6c6d78ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt @@ -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 { + fun queryRoomMembersEvent(): RealmQuery { 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 { + return queryRoomMembersEvent() + .equalTo(EventEntityFields.STATE_KEY, userId) + } + + fun getLoaded(): Map { + return queryRoomMembersEvent() .findAll() .map { it.asDomain() } .associateBy { it.stateKey!! } diff --git a/vector/build.gradle b/vector/build.gradle index 8bb46c06..bb99d9ac 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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' diff --git a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt index f715106a..7c6f200b 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyHolder.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt index bb7bb10c..16c2401d 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/epoxy/VectorEpoxyModel.kt @@ -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 : EpoxyModelWithHolder() { private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/AutocompleteClickListener.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/AutocompleteClickListener.kt new file mode 100644 index 00000000..aa2226d5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/AutocompleteClickListener.kt @@ -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 { + + fun onItemClick(t: T) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt new file mode 100644 index 00000000..ebdee91b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/EpoxyAutocompletePresenter.kt @@ -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(context: Context) : AutocompletePresenter(context), AutocompleteClickListener { + + private var recyclerView: EpoxyRecyclerView? = null + private var clicks: AutocompletePresenter.ClickProvider? = null + private var observer: Observer? = null + + override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider) { + 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() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt new file mode 100644 index 00000000..7356364c --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandController.kt @@ -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>() { + + var listener: AutocompleteClickListener? = null + + override fun buildModels(data: List?) { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt new file mode 100644 index 00000000..cd6a0ff0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandItem.kt @@ -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() { + + @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(R.id.commandName) + val parametersView by bind(R.id.commandParameter) + val descriptionView by bind(R.id.commandDescription) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt new file mode 100644 index 00000000..ca894948 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -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(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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt new file mode 100644 index 00000000..74ee50aa --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt @@ -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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt new file mode 100644 index 00000000..bec9adb0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.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>() { + + var listener: AutocompleteClickListener? = null + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { user -> + autocompleteUserItem { + id(user.userId) + name(user.displayName) + avatarUrl(user.avatarUrl) + clickListener { _ -> + listener?.onItemClick(user) + } + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt new file mode 100644 index 00000000..6678bff5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt @@ -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() { + + @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(R.id.userAutocompleteName) + val avatarImageView by bind(R.id.userAutocompleteAvatar) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt new file mode 100644 index 00000000..be4ec7e7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserPresenter.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.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(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>) { + if (users is Success) { + controller.setData(users()) + } + } + + interface Callback { + fun onQueryUsers(query: CharSequence?) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt b/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt new file mode 100644 index 00000000..a41a3afd --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/command/Command.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.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", "", R.string.command_description_emote), + BAN_USER("/ban", " [reason]", R.string.command_description_ban_user), + UNBAN_USER("/unban", "", R.string.command_description_unban_user), + SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), + RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), + INVITE("/invite", "", R.string.command_description_invite_user), + JOIN_ROOM("/join", "", R.string.command_description_join_room), + PART("/part", "", R.string.command_description_part_room), + TOPIC("/topic", "", R.string.command_description_topic), + KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), + CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), + MARKDOWN("/markdown", "", R.string.command_description_markdown), + CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token); +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt new file mode 100644 index 00000000..f442d1d7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/command/CommandParser.kt @@ -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? = 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) + } + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt new file mode 100644 index 00000000..350423b1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/command/ParsedCommand.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.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() +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index a013a0dc..98666053 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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) + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 98e3164f..a5b67384 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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() + 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(composerEditText) + .with(CommandAutocompletePolicy()) + .with(autocompleteCommandPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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(composerEditText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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)) + } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index eeef99c0..895c6659 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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>() + val sendMessageResultLiveData: LiveData> + get() = _sendMessageResultLiveData + // PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailActions.SendMessage) { - room.sendTextMessage(action.text, callback = object : MatrixCallback {}) + // 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 {}) + _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 { + 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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt new file mode 100644 index 00000000..189ad90d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/SendMessageResult.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.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() +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt new file mode 100644 index 00000000..cb4e06c6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerActions.kt @@ -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() +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt new file mode 100644 index 00000000..41c09a19 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewModel.kt @@ -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(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + private val roomId = initialState.roomId + + private val usersQueryObservable = BehaviorRelay.create>() + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? { + val currentSession = viewModelContext.activity.get() + 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, Option, List>( + 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 + ) + } + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt new file mode 100644 index 00000000..e9317d71 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerViewState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.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> = Uninitialized +) : MvRxState { + + constructor(args: RoomDetailArgs) : this(roomId = args.roomId) + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index a5189494..5ab18531 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -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, diff --git a/vector/src/main/res/layout/item_autocomplete_command.xml b/vector/src/main/res/layout/item_autocomplete_command.xml new file mode 100644 index 00000000..3800443b --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_command.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_autocomplete_user.xml b/vector/src/main/res/layout/item_autocomplete_user.xml new file mode 100644 index 00000000..873e9c6a --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_user.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file