From cbed1afaaad1a12c8fd907aace63c29a403f06a6 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoitm@matrix.org>
Date: Thu, 27 May 2021 14:05:13 +0200
Subject: [PATCH] Add custom server

---
 newsfragment/1458.feature                     |   1 +
 .../app/core/ui/list/GenericSpaceItem.kt      |  45 +++++++
 .../discovery/SettingsContinueCancelItem.kt   |   4 +
 .../app/features/form/FormEditTextItem.kt     |   1 +
 .../roomdirectory/RoomDirectoryServer.kt      |   5 +
 .../picker/RoomDirectoryListCreator.kt        |  25 +++-
 .../picker/RoomDirectoryPickerAction.kt       |   7 ++
 .../picker/RoomDirectoryPickerController.kt   | 111 +++++++++++++++++-
 .../picker/RoomDirectoryPickerFragment.kt     |  48 ++++++--
 .../picker/RoomDirectoryPickerViewModel.kt    |  97 ++++++++++++++-
 .../picker/RoomDirectoryPickerViewState.kt    |   6 +-
 .../picker/RoomDirectoryServerItem.kt         |  13 ++
 .../ui/SharedPreferencesUiStateRepository.kt  |  16 ++-
 .../app/features/ui/UiStateRepository.kt      |   4 +
 .../main/res/layout/item_generic_space.xml    |   5 +
 .../res/layout/item_room_directory_server.xml |  27 ++++-
 .../res/menu/menu_directory_server_picker.xml |  13 --
 vector/src/main/res/values/strings.xml        |   5 +-
 18 files changed, 387 insertions(+), 46 deletions(-)
 create mode 100644 newsfragment/1458.feature
 create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/GenericSpaceItem.kt
 create mode 100644 vector/src/main/res/layout/item_generic_space.xml
 delete mode 100644 vector/src/main/res/menu/menu_directory_server_picker.xml

diff --git a/newsfragment/1458.feature b/newsfragment/1458.feature
new file mode 100644
index 0000000000..ded4f549ed
--- /dev/null
+++ b/newsfragment/1458.feature
@@ -0,0 +1 @@
+Allow user to add custom "network" in room search
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericSpaceItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericSpaceItem.kt
new file mode 100644
index 0000000000..137fac9abe
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericSpaceItem.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 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.app.core.ui.list
+
+import android.view.View
+import androidx.core.view.updateLayoutParams
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+
+/**
+ * A generic item with empty space.
+ */
+@EpoxyModelClass(layout = R.layout.item_generic_space)
+abstract class GenericSpaceItem : VectorEpoxyModel<GenericSpaceItem.Holder>() {
+
+    @EpoxyAttribute
+    var heightInPx: Int = 0
+
+    override fun bind(holder: Holder) {
+        super.bind(holder)
+        holder.space.updateLayoutParams {
+            height = heightInPx
+        }
+    }
+
+    class Holder : VectorEpoxyHolder() {
+        val space by bind<View>(R.id.item_generic_space)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt
index b59b24fe55..47059128a1 100644
--- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt
+++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt
@@ -33,6 +33,9 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder<SettingsContinu
     @EpoxyAttribute
     var continueOnClick: ClickListener? = null
 
+    @EpoxyAttribute
+    var canContinue: Boolean = true
+
     @EpoxyAttribute
     var cancelOnClick: ClickListener? = null
 
@@ -43,6 +46,7 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder<SettingsContinu
 
         continueText?.let { holder.continueButton.text = it }
         holder.continueButton.onClick(continueOnClick)
+        holder.continueButton.isEnabled = canContinue
     }
 
     class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt
index f8bcbddd34..459e4ea838 100644
--- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt
+++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt
@@ -51,6 +51,7 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
     @EpoxyAttribute
     var inputType: Int? = null
 
+    // TODO Should be true by default
     @EpoxyAttribute
     var singleLine: Boolean? = null
 
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt
index 63ca4998f4..0f29ae5986 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt
@@ -24,6 +24,11 @@ data class RoomDirectoryServer(
          */
         val isUserServer: Boolean,
 
+        /**
+         * True if manually added, so it can be removed by the user
+         */
+        val isManuallyAdded: Boolean,
+
         /**
          * Supported protocols
          * TODO Rename RoomDirectoryData to RoomDirectoryProtocols
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt
index e3cd9278a3..65d8f2d1cb 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt
@@ -29,7 +29,8 @@ class RoomDirectoryListCreator @Inject constructor(
         private val session: Session
 ) {
 
-    fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>): List<RoomDirectoryServer> {
+    fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>,
+                           customHomeservers: Set<String>): List<RoomDirectoryServer> {
         val result = ArrayList<RoomDirectoryServer>()
 
         val protocols = ArrayList<RoomDirectoryData>()
@@ -75,6 +76,7 @@ class RoomDirectoryListCreator @Inject constructor(
                 RoomDirectoryServer(
                         serverName = userHsName,
                         isUserServer = true,
+                        isManuallyAdded = false,
                         protocols = protocols
                 )
         )
@@ -88,6 +90,7 @@ class RoomDirectoryListCreator @Inject constructor(
                             RoomDirectoryServer(
                                     serverName = it,
                                     isUserServer = false,
+                                    isManuallyAdded = false,
                                     protocols = listOf(
                                             RoomDirectoryData(
                                                     homeServer = it,
@@ -99,7 +102,25 @@ class RoomDirectoryListCreator @Inject constructor(
                     )
                 }
 
-        // TODO Add manually added server by the user
+        // Add manually added server by the user
+        customHomeservers
+                .forEach {
+                    // Use the server name as a default display name
+                    result.add(
+                            RoomDirectoryServer(
+                                    serverName = it,
+                                    isUserServer = false,
+                                    isManuallyAdded = true,
+                                    protocols = listOf(
+                                            RoomDirectoryData(
+                                                    homeServer = it,
+                                                    displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME,
+                                                    includeAllNetworks = false
+                                            )
+                                    )
+                            )
+                    )
+                }
 
         return result
     }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt
index 36f2cd4296..8be3c6b2b2 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt
@@ -17,7 +17,14 @@
 package im.vector.app.features.roomdirectory.picker
 
 import im.vector.app.core.platform.VectorViewModelAction
+import im.vector.app.features.roomdirectory.RoomDirectoryServer
 
 sealed class RoomDirectoryPickerAction : VectorViewModelAction {
     object Retry : RoomDirectoryPickerAction()
+    object EnterEditMode : RoomDirectoryPickerAction()
+    object ExitEditMode : RoomDirectoryPickerAction()
+    data class SetServerUrl(val url: String) : RoomDirectoryPickerAction()
+    data class RemoveServer(val roomDirectoryServer: RoomDirectoryServer) : RoomDirectoryPickerAction()
+
+    object Submit : RoomDirectoryPickerAction()
 }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt
index c3f461cb46..0a28ad5d52 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt
@@ -16,10 +16,14 @@
 
 package im.vector.app.features.roomdirectory.picker
 
+import android.text.InputType
+import android.view.View
 import com.airbnb.epoxy.TypedEpoxyController
 import com.airbnb.mvrx.Fail
 import com.airbnb.mvrx.Incomplete
+import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
 import im.vector.app.R
 import im.vector.app.core.epoxy.dividerItem
 import im.vector.app.core.epoxy.errorWithRetryItem
@@ -28,13 +32,22 @@ import im.vector.app.core.error.ErrorFormatter
 import im.vector.app.core.extensions.join
 import im.vector.app.core.resources.ColorProvider
 import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.genericButtonItem
+import im.vector.app.core.ui.list.genericSpaceItem
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.discovery.settingsContinueCancelItem
+import im.vector.app.features.discovery.settingsInformationItem
+import im.vector.app.features.form.formEditTextItem
 import im.vector.app.features.roomdirectory.RoomDirectoryData
 import im.vector.app.features.roomdirectory.RoomDirectoryServer
+import org.matrix.android.sdk.api.failure.Failure
 import javax.inject.Inject
+import javax.net.ssl.HttpsURLConnection
 
 class RoomDirectoryPickerController @Inject constructor(
         private val stringProvider: StringProvider,
-        colorProvider: ColorProvider,
+        private val colorProvider: ColorProvider,
+        private val dimensionConverter: DimensionConverter,
         private val errorFormatter: ErrorFormatter,
         private val roomDirectoryListCreator: RoomDirectoryListCreator
 ) : TypedEpoxyController<RoomDirectoryPickerViewState>() {
@@ -44,16 +57,24 @@ class RoomDirectoryPickerController @Inject constructor(
 
     private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
 
-    override fun buildModels(viewState: RoomDirectoryPickerViewState) {
+    override fun buildModels(data: RoomDirectoryPickerViewState) {
         val host = this
 
-        when (val asyncThirdPartyProtocol = viewState.asyncThirdPartyRequest) {
+        when (val asyncThirdPartyProtocol = data.asyncThirdPartyRequest) {
             is Success    -> {
-                val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol())
+                val directories = roomDirectoryListCreator.computeDirectories(
+                        asyncThirdPartyProtocol(),
+                        data.customHomeservers
+                )
                 directories.join(
                         each = { _, roomDirectoryServer -> buildDirectory(roomDirectoryServer) },
                         between = { idx, _ -> buildDivider(idx) }
                 )
+                buildForm(data)
+                genericSpaceItem {
+                    id("space_bottom")
+                    heightInPx(host.dimensionConverter.dpToPx(16))
+                }
             }
             is Incomplete -> {
                 loadingItem {
@@ -70,6 +91,77 @@ class RoomDirectoryPickerController @Inject constructor(
         }
     }
 
+    private fun buildForm(data: RoomDirectoryPickerViewState) {
+        buildDivider(1000)
+        val host = this
+        if (data.inEditMode) {
+            genericSpaceItem {
+                id("form_space")
+                heightInPx(host.dimensionConverter.dpToPx(16))
+            }
+            settingsInformationItem {
+                id("form_notice")
+                message(host.stringProvider.getString(R.string.directory_add_a_new_server_prompt))
+                colorProvider(host.colorProvider)
+            }
+            genericSpaceItem {
+                id("form_space_2")
+                heightInPx(host.dimensionConverter.dpToPx(8))
+            }
+            formEditTextItem {
+                id("edit")
+                showBottomSeparator(false)
+                value(data.enteredServer)
+                hint(host.stringProvider.getString(R.string.directory_server_placeholder))
+                inputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
+                onTextChange { text ->
+                    host.callback?.onEnterServerChange(text)
+                }
+                when (data.addServerAsync) {
+                    Uninitialized -> enabled(true)
+                    is Loading    -> enabled(false)
+                    is Success    -> enabled(false)
+                    is Fail       -> {
+                        enabled(true)
+                        errorMessage(host.getErrorMessage(data.addServerAsync.error))
+                    }
+                }
+            }
+            when (data.addServerAsync) {
+                Uninitialized,
+                is Fail    -> settingsContinueCancelItem {
+                    id("continueCancel")
+                    continueText(host.stringProvider.getString(R.string.ok))
+                    canContinue(data.enteredServer.isNotEmpty())
+                    continueOnClick { host.callback?.onSubmitServer() }
+                    cancelOnClick { host.callback?.onCancelEnterServer() }
+                }
+                is Loading -> loadingItem {
+                    id("addLoading")
+                }
+                is Success -> Unit /* This is a transitive state */
+            }
+        } else {
+            genericButtonItem {
+                id("add")
+                text(host.stringProvider.getString(R.string.directory_add_a_new_server))
+                textColor(host.colorProvider.getColor(R.color.riotx_accent))
+                buttonClickAction(View.OnClickListener {
+                    host.callback?.onStartEnterServer()
+                })
+            }
+        }
+    }
+
+    private fun getErrorMessage(error: Throwable): String {
+        return if (error is Failure.ServerError
+                && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) {
+            stringProvider.getString(R.string.directory_add_a_new_server_error)
+        } else {
+            errorFormatter.toHumanReadable(error)
+        }
+    }
+
     private fun buildDivider(idx: Int) {
         val host = this
         dividerItem {
@@ -81,8 +173,10 @@ class RoomDirectoryPickerController @Inject constructor(
     private fun buildDirectory(roomDirectoryServer: RoomDirectoryServer) {
         val host = this
         roomDirectoryServerItem {
-            id("server_" + roomDirectoryServer.serverName)
+            id("server_$roomDirectoryServer")
             serverName(roomDirectoryServer.serverName)
+            canRemove(roomDirectoryServer.isManuallyAdded)
+            removeListener { host.callback?.onRemoveServer(roomDirectoryServer) }
 
             if (roomDirectoryServer.isUserServer) {
                 serverDescription(host.stringProvider.getString(R.string.directory_your_server))
@@ -91,7 +185,7 @@ class RoomDirectoryPickerController @Inject constructor(
 
         roomDirectoryServer.protocols.forEach { roomDirectoryData ->
             roomDirectoryItem {
-                id("server_" + roomDirectoryServer.serverName + "_proto_" + roomDirectoryData.displayName)
+                id("server_${roomDirectoryServer}_proto_$roomDirectoryData")
                 directoryName(
                         if (roomDirectoryData.includeAllNetworks) {
                             host.stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryServer.serverName)
@@ -117,5 +211,10 @@ class RoomDirectoryPickerController @Inject constructor(
     interface Callback {
         fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData)
         fun retry()
+        fun onStartEnterServer()
+        fun onEnterServerChange(server: String)
+        fun onSubmitServer()
+        fun onCancelEnterServer()
+        fun onRemoveServer(roomDirectoryServer: RoomDirectoryServer)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt
index 701e8632c4..e3c39a2ccb 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt
@@ -28,20 +28,22 @@ import com.airbnb.mvrx.withState
 import im.vector.app.R
 import im.vector.app.core.extensions.cleanup
 import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.platform.OnBackPressed
 import im.vector.app.core.platform.VectorBaseFragment
 import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding
 import im.vector.app.features.roomdirectory.RoomDirectoryAction
 import im.vector.app.features.roomdirectory.RoomDirectoryData
+import im.vector.app.features.roomdirectory.RoomDirectoryServer
 import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
 import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
 import im.vector.app.features.roomdirectory.RoomDirectoryViewModel
 import timber.log.Timber
 import javax.inject.Inject
 
-// TODO Menu to add custom room directory (not done in RiotWeb so far...)
 class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory,
                                                       private val roomDirectoryPickerController: RoomDirectoryPickerController
 ) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(),
+        OnBackPressed,
         RoomDirectoryPickerController.Callback {
 
     private val viewModel: RoomDirectoryViewModel by activityViewModel()
@@ -77,18 +79,6 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
         super.onDestroyView()
     }
 
-    override fun getMenuRes() = R.menu.menu_directory_server_picker
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        if (item.itemId == R.id.action_add_custom_hs) {
-            // TODO
-            vectorBaseActivity.notImplemented("Entering custom homeserver")
-            return true
-        }
-
-        return super.onOptionsItemSelected(item)
-    }
-
     private fun setupRecyclerView() {
         views.roomDirectoryPickerList.configureWith(roomDirectoryPickerController)
         roomDirectoryPickerController.callback = this
@@ -101,6 +91,26 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
         sharedActionViewModel.post(RoomDirectorySharedAction.Back)
     }
 
+    override fun onStartEnterServer() {
+        pickerViewModel.handle(RoomDirectoryPickerAction.EnterEditMode)
+    }
+
+    override fun onCancelEnterServer() {
+        pickerViewModel.handle(RoomDirectoryPickerAction.ExitEditMode)
+    }
+
+    override fun onEnterServerChange(server: String) {
+        pickerViewModel.handle(RoomDirectoryPickerAction.SetServerUrl(server))
+    }
+
+    override fun onSubmitServer() {
+        pickerViewModel.handle(RoomDirectoryPickerAction.Submit)
+    }
+
+    override fun onRemoveServer(roomDirectoryServer: RoomDirectoryServer) {
+        pickerViewModel.handle(RoomDirectoryPickerAction.RemoveServer(roomDirectoryServer))
+    }
+
     override fun onResume() {
         super.onResume()
         (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory)
@@ -115,4 +125,16 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
         // Populate list with Epoxy
         roomDirectoryPickerController.setData(state)
     }
+
+    override fun onBackPressed(toolbarButton: Boolean): Boolean {
+        // Leave the add server mode if started
+        return withState(pickerViewModel) {
+            if (it.inEditMode) {
+                pickerViewModel.handle(RoomDirectoryPickerAction.ExitEditMode)
+                true
+            } else {
+                false
+            }
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
index d85b7937a2..049293cafd 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
@@ -22,18 +22,24 @@ import com.airbnb.mvrx.FragmentViewModelContext
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.MvRxViewModelFactory
 import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
 import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.platform.EmptyViewEvents
 import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.ui.UiStateRepository
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
 
-class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState,
-                                                               private val session: Session)
-    : VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) {
+class RoomDirectoryPickerViewModel @AssistedInject constructor(
+        @Assisted initialState: RoomDirectoryPickerViewState,
+        private val session: Session,
+        private val uiStateRepository: UiStateRepository
+) : VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) {
 
     @AssistedFactory
     interface Factory {
@@ -51,6 +57,7 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial
 
     init {
         load()
+        loadCustomRoomDirectoryHomeservers()
     }
 
     private fun load() {
@@ -71,9 +78,89 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial
         }
     }
 
+    private fun loadCustomRoomDirectoryHomeservers() {
+        setState {
+            copy(
+                    customHomeservers = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId)
+            )
+        }
+    }
+
     override fun handle(action: RoomDirectoryPickerAction) {
         when (action) {
-            RoomDirectoryPickerAction.Retry -> load()
+            RoomDirectoryPickerAction.Retry           -> load()
+            RoomDirectoryPickerAction.EnterEditMode   -> handleEnterEditMode()
+            RoomDirectoryPickerAction.ExitEditMode    -> handleExitEditMode()
+            is RoomDirectoryPickerAction.SetServerUrl -> handleSetServerUrl(action)
+            RoomDirectoryPickerAction.Submit          -> handleSubmit()
+            is RoomDirectoryPickerAction.RemoveServer -> handleRemoveServer(action)
+        }.exhaustive
+    }
+
+    private fun handleEnterEditMode() {
+        setState {
+            copy(
+                    inEditMode = true,
+                    enteredServer = "",
+                    addServerAsync = Uninitialized
+            )
+        }
+    }
+
+    private fun handleExitEditMode() {
+        setState {
+            copy(
+                    inEditMode = false,
+                    enteredServer = "",
+                    addServerAsync = Uninitialized
+            )
+        }
+    }
+
+    private fun handleSetServerUrl(action: RoomDirectoryPickerAction.SetServerUrl) {
+        setState {
+            copy(
+                    enteredServer = action.url,
+            )
+        }
+    }
+
+    private fun handleSubmit() = withState { state ->
+        viewModelScope.launch {
+            setState {
+                copy(addServerAsync = Loading())
+            }
+            try {
+                session.getPublicRooms(
+                        server = state.enteredServer,
+                        publicRoomsParams = PublicRoomsParams(limit = 1)
+                )
+                // Success, let add the server to our local repository, and update the state
+                val newSet = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) + state.enteredServer
+                uiStateRepository.setCustomRoomDirectoryHomeservers(session.sessionId, newSet)
+                setState {
+                    copy(
+                            inEditMode = false,
+                            enteredServer = "",
+                            addServerAsync = Uninitialized,
+                            customHomeservers = newSet
+                    )
+                }
+            } catch (failure: Throwable) {
+                setState {
+                    copy(addServerAsync = Fail(failure))
+                }
+            }
+        }
+    }
+
+    private fun handleRemoveServer(action: RoomDirectoryPickerAction.RemoveServer) {
+        val newSet = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) - action.roomDirectoryServer.serverName
+        uiStateRepository.setCustomRoomDirectoryHomeservers(session.sessionId, newSet)
+        setState {
+            copy(
+                    customHomeservers = newSet
+            )
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt
index 61cf50e8dd..c78d4ac55c 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt
@@ -22,5 +22,9 @@ import com.airbnb.mvrx.Uninitialized
 import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
 
 data class RoomDirectoryPickerViewState(
-        val asyncThirdPartyRequest: Async<Map<String, ThirdPartyProtocol>> = Uninitialized
+        val asyncThirdPartyRequest: Async<Map<String, ThirdPartyProtocol>> = Uninitialized,
+        val customHomeservers: Set<String> = emptySet(),
+        val inEditMode: Boolean = false,
+        val enteredServer: String = "",
+        val addServerAsync: Async<Unit> = Uninitialized
 ) : MvRxState
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt
index 83acfa581c..6efb41d5b1 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt
@@ -16,12 +16,16 @@
 
 package im.vector.app.features.roomdirectory.picker
 
+import android.view.View
 import android.widget.TextView
+import androidx.core.view.isVisible
 import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
 import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
 import im.vector.app.core.epoxy.VectorEpoxyHolder
 import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
 import im.vector.app.core.extensions.setTextOrHide
 
 @EpoxyModelClass(layout = R.layout.item_room_directory_server)
@@ -33,14 +37,23 @@ abstract class RoomDirectoryServerItem : VectorEpoxyModel<RoomDirectoryServerIte
     @EpoxyAttribute
     var serverDescription: String? = null
 
+    @EpoxyAttribute
+    var canRemove: Boolean = false
+
+    @EpoxyAttribute
+    var removeListener: ClickListener? = null
+
     override fun bind(holder: Holder) {
         super.bind(holder)
         holder.nameView.text = serverName
         holder.descriptionView.setTextOrHide(serverDescription)
+        holder.deleteView.isVisible = canRemove
+        holder.deleteView.onClick(removeListener)
     }
 
     class Holder : VectorEpoxyHolder() {
         val nameView by bind<TextView>(R.id.itemRoomDirectoryServerName)
         val descriptionView by bind<TextView>(R.id.itemRoomDirectoryServerDescription)
+        val deleteView by bind<View>(R.id.itemRoomDirectoryServerRemove)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt
index 1ec3a8ab46..e46c3516ca 100644
--- a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt
+++ b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt
@@ -39,7 +39,7 @@ class SharedPreferencesUiStateRepository @Inject constructor(
     override fun getDisplayMode(): RoomListDisplayMode {
         return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) {
             VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE
-            VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS
+            VALUE_DISPLAY_MODE_ROOMS  -> RoomListDisplayMode.ROOMS
             else                      -> if (vectorPreferences.labAddNotificationTab()) {
                 RoomListDisplayMode.NOTIFICATIONS
             } else {
@@ -89,6 +89,18 @@ class SharedPreferencesUiStateRepository @Inject constructor(
         return sharedPreferences.getBoolean("$KEY_SELECTED_METHOD@$sessionId", true)
     }
 
+    override fun setCustomRoomDirectoryHomeservers(sessionId: String, servers: Set<String>) {
+        sharedPreferences.edit {
+            putStringSet("$KEY_CUSTOM_DIRECTORY_HOMESERVER@$sessionId", servers)
+        }
+    }
+
+    override fun getCustomRoomDirectoryHomeservers(sessionId: String): Set<String> {
+        return sharedPreferences.getStringSet("$KEY_CUSTOM_DIRECTORY_HOMESERVER@$sessionId", null)
+                .orEmpty()
+                .toSet()
+    }
+
     companion object {
         private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE"
         private const val VALUE_DISPLAY_MODE_CATCHUP = 0
@@ -98,5 +110,7 @@ class SharedPreferencesUiStateRepository @Inject constructor(
         private const val KEY_SELECTED_SPACE = "UI_STATE_SELECTED_SPACE"
         private const val KEY_SELECTED_GROUP = "UI_STATE_SELECTED_GROUP"
         private const val KEY_SELECTED_METHOD = "UI_STATE_SELECTED_METHOD"
+
+        private const val KEY_CUSTOM_DIRECTORY_HOMESERVER = "KEY_CUSTOM_DIRECTORY_HOMESERVER"
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt
index 935da83f5d..3c48f8972d 100644
--- a/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt
+++ b/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt
@@ -32,6 +32,7 @@ interface UiStateRepository {
 
     fun storeDisplayMode(displayMode: RoomListDisplayMode)
 
+    // TODO Handle SharedPreference per session in a better way, also to cleanup when login out
     fun storeSelectedSpace(spaceId: String?, sessionId: String)
     fun storeSelectedGroup(groupId: String?, sessionId: String)
 
@@ -40,4 +41,7 @@ interface UiStateRepository {
     fun getSelectedSpace(sessionId: String): String?
     fun getSelectedGroup(sessionId: String): String?
     fun isGroupingMethodSpace(sessionId: String): Boolean
+
+    fun setCustomRoomDirectoryHomeservers(sessionId: String, servers: Set<String>)
+    fun getCustomRoomDirectoryHomeservers(sessionId: String): Set<String>
 }
diff --git a/vector/src/main/res/layout/item_generic_space.xml b/vector/src/main/res/layout/item_generic_space.xml
new file mode 100644
index 0000000000..aef6664f94
--- /dev/null
+++ b/vector/src/main/res/layout/item_generic_space.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Space xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/item_generic_space"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/layout_vertical_margin" />
diff --git a/vector/src/main/res/layout/item_room_directory_server.xml b/vector/src/main/res/layout/item_room_directory_server.xml
index 5459652e2c..5705e1c623 100644
--- a/vector/src/main/res/layout/item_room_directory_server.xml
+++ b/vector/src/main/res/layout/item_room_directory_server.xml
@@ -12,7 +12,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/layout_horizontal_margin"
-        android:layout_marginEnd="@dimen/layout_horizontal_margin"
+        android:layout_marginEnd="8dp"
         android:ellipsize="end"
         android:lines="1"
         android:maxLines="2"
@@ -20,10 +20,11 @@
         android:textSize="16sp"
         android:textStyle="bold"
         app:layout_constraintBottom_toTopOf="@+id/itemRoomDirectoryServerDescription"
-        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/itemRoomDirectoryServerRemove"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_chainStyle="packed"
+        app:layout_goneMarginEnd="@dimen/layout_horizontal_margin"
         tools:text="@tools:sample/lorem/random" />
 
     <TextView
@@ -31,7 +32,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/layout_horizontal_margin"
-        android:layout_marginEnd="@dimen/layout_horizontal_margin"
+        android:layout_marginEnd="8dp"
         android:ellipsize="end"
         android:lines="1"
         android:maxLines="2"
@@ -39,10 +40,28 @@
         android:textSize="15sp"
         android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/itemRoomDirectoryServerRemove"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/itemRoomDirectoryServerName"
+        app:layout_goneMarginEnd="@dimen/layout_horizontal_margin"
         tools:text="@string/directory_your_server"
         tools:visibility="visible" />
 
+    <ImageView
+        android:id="@+id/itemRoomDirectoryServerRemove"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/layout_horizontal_margin"
+        android:layout_marginEnd="@dimen/layout_horizontal_margin"
+        android:contentDescription="@string/avatar"
+        android:padding="8dp"
+        android:src="@drawable/ic_delete"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:tint="@color/vector_error_color"
+        tools:ignore="MissingPrefix"
+        tools:visibility="visible" />
+
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/vector/src/main/res/menu/menu_directory_server_picker.xml b/vector/src/main/res/menu/menu_directory_server_picker.xml
deleted file mode 100644
index c544c80f8c..0000000000
--- a/vector/src/main/res/menu/menu_directory_server_picker.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/action_add_custom_hs"
-        android:icon="@drawable/ic_add_black"
-        android:title="@string/action_open"
-        android:visible="@bool/false_not_implemented"
-        app:iconTint="?colorAccent"
-        app:showAsAction="always" />
-
-</menu>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 442b79b5b0..7b8a933d30 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -1584,10 +1584,13 @@
     <string name="select_room_directory">Select a room directory</string>
     <string name="directory_server_fail_to_retrieve_server">The server may be unavailable or overloaded</string>
     <string name="directory_server_type_homeserver">Type a homeserver to list public rooms from</string>
-    <string name="directory_server_placeholder">Homeserver URL</string>
+    <string name="directory_server_placeholder">Server name</string>
     <string name="directory_server_all_rooms_on_server">All rooms on %s server</string>
     <string name="directory_server_native_rooms">All native %s rooms</string>
     <string name="directory_your_server">Your server</string>
+    <string name="directory_add_a_new_server">Add a new server</string>
+    <string name="directory_add_a_new_server_prompt">Enter the name of a new server you want to explore.</string>
+    <string name="directory_add_a_new_server_error">"Can't find this server or its room list"</string>
 
     <!-- Lock screen-->
     <string name="lock_screen_hint">Type hereā€¦</string>