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 2fbed52bee..c9dbbdd068 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<List<String>> {
+        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 20ec0d534a..cdc971fd2d 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<RoomSummary>
 
-    /**
-     * 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 0000000000..930afd7abf
--- /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<List<String>>
+
+    /**
+     * Invite a user in the room
+     */
+    fun invite(userId: String, callback: MatrixCallback<Unit>)
+
+}
\ 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 ae6255a123..952a1ecac7 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<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)
-    }
 }
\ 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 bc44300f4d..1b89662daa 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<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>
 
 }
\ 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 b30ba3eebd..720d3804a9 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 34515d30da..ccdfe9ad99 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 0000000000..652f8d63fb
--- /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 0000000000..1086c6b5d9
--- /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<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)
+        }
+    }
+}
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 0000000000..a007a1eb63
--- /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<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)
+    }
+}
\ 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 1bbb792db8..6c6d78ed2e 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<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!! }
diff --git a/vector/build.gradle b/vector/build.gradle
index 8bb46c068d..bb99d9aca0 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 f715106a3b..7c6f200bd2 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 bb7bb10c3f..16c2401dd9 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<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
 
     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 0000000000..aa2226d5b2
--- /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<T> {
+
+    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 0000000000..ebdee91b16
--- /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<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()
+        }
+    }
+}
\ 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 0000000000..7356364c6c
--- /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<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)
+                }
+            }
+        }
+    }
+}
\ 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 0000000000..cd6a0ff06e
--- /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<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)
+    }
+
+}
\ 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 0000000000..ca8949486c
--- /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<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)
+    }
+}
\ 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 0000000000..74ee50aa06
--- /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 0000000000..bec9adb022
--- /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<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)
+                }
+            }
+        }
+    }
+}
\ 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 0000000000..6678bff543
--- /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<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)
+    }
+
+}
\ 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 0000000000..be4ec7e702
--- /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<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?)
+    }
+
+}
\ 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 0000000000..a41a3afd42
--- /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", "<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);
+}
\ 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 0000000000..f442d1d709
--- /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<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)
+                }
+            }
+        }
+    }
+}
\ 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 0000000000..350423b1d3
--- /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 a013a0dcae..9866605305 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 98e3164f32..a5b6738411 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<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))
+    }
 }
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 eeef99c0ad..895c665912 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<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) {
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 0000000000..189ad90d88
--- /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 0000000000..cb4e06c6f7
--- /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 0000000000..41c09a191b
--- /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<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
+            )
+        }
+
+    }
+}
\ 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 0000000000..e9317d71e8
--- /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<List<User>> = 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 a51894946f..5ab1853109 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 0000000000..3800443bf6
--- /dev/null
+++ b/vector/src/main/res/layout/item_autocomplete_command.xml
@@ -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="&lt;user-id&gt;" />
+
+    <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>
\ 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 0000000000..873e9c6a75
--- /dev/null
+++ b/vector/src/main/res/layout/item_autocomplete_user.xml
@@ -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>
\ No newline at end of file