From 579efb016ae0e1873ac575252d59ab9afc7f1cad Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Mon, 16 Nov 2020 17:25:42 +0100
Subject: [PATCH] Room creation form: add advanced section to disable
 federation (#1314)

---
 CHANGES.md                                    |  1 +
 .../room/model/create/CreateRoomParams.kt     | 21 +++++++-
 .../session/room/create/CreateRoomBody.kt     |  4 +-
 .../room/create/CreateRoomBodyBuilder.kt      |  2 +-
 .../app/core/resources/DrawableProvider.kt    |  8 +--
 .../features/form/FormAdvancedToggleItem.kt   | 51 +++++++++++++++++++
 .../createroom/CreateRoomAction.kt            |  3 ++
 .../createroom/CreateRoomController.kt        | 19 +++++++
 .../createroom/CreateRoomFragment.kt          |  8 +++
 .../createroom/CreateRoomViewModel.kt         | 29 +++++++++++
 .../createroom/CreateRoomViewState.kt         |  3 ++
 .../res/layout/item_form_advanced_toggle.xml  | 34 +++++++++++++
 .../src/main/res/layout/item_form_switch.xml  |  2 -
 vector/src/main/res/values/strings.xml        |  6 +++
 14 files changed, 181 insertions(+), 10 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt
 create mode 100644 vector/src/main/res/layout/item_form_advanced_toggle.xml

diff --git a/CHANGES.md b/CHANGES.md
index 936e6b0ffe..a3338b2c75 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,6 +8,7 @@ Improvements 🙌:
  - Open an existing DM instead of creating a new one (#2319)
  - Ask for explicit user consent to send their contact details to the identity server (#2375)
  - Handle events of type "m.room.server_acl" (#890)
+ - Room creation form: add advanced section to disable federation (#1314)
 
 Bugfix 🐛:
  - Fix issue when restoring draft after sharing (#2287)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
index 892a865751..80e3741a0c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
@@ -94,7 +94,22 @@ class CreateRoomParams {
      * The server will clobber the following keys: creator.
      * Future versions of the specification may allow the server to clobber other keys.
      */
-    var creationContent: Any? = null
+    val creationContent = mutableMapOf<String, Any>()
+
+    /**
+     * Set to true to disable federation of this room.
+     * Default: false
+     */
+    var disableFederation = false
+        set(value) {
+            field = value
+            if (value) {
+                creationContent[CREATION_CONTENT_KEY_M_FEDERATE] = false
+            } else {
+                // This is the default value, we remove the field
+                creationContent.remove(CREATION_CONTENT_KEY_M_FEDERATE)
+            }
+        }
 
     /**
      * The power level content to override in the default power level event
@@ -120,4 +135,8 @@ class CreateRoomParams {
     fun enableEncryption() {
         algorithm = MXCRYPTO_ALGORITHM_MEGOLM
     }
+
+    companion object {
+        private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
index c30f11b9af..13d403e2e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
@@ -74,8 +74,8 @@ internal data class CreateRoomBody(
         val invite3pids: List<ThreePidInviteBody>?,
 
         /**
-         * Extra keys to be added to the content of the m.room.create.
-         * The server will clobber the following keys: creator.
+         * Extra keys, such as m.federate, to be added to the content of the m.room.create event.
+         * The server will clobber the following keys: creator, room_version.
          * Future versions of the specification may allow the server to clobber other keys.
          */
         @Json(name = "creation_content")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
index 632fcab70b..79ff9db087 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
@@ -81,7 +81,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                 topic = params.topic,
                 invitedUserIds = params.invitedUserIds,
                 invite3pids = invite3pids,
-                creationContent = params.creationContent,
+                creationContent = params.creationContent.takeIf { it.isNotEmpty() },
                 initialStates = initialStates,
                 preset = params.preset,
                 isDirect = params.isDirect,
diff --git a/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt b/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt
index c184b04bd9..96b1cfbb8e 100644
--- a/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/DrawableProvider.kt
@@ -26,12 +26,12 @@ import javax.inject.Inject
 
 class DrawableProvider @Inject constructor(private val context: Context) {
 
-    fun getDrawable(@DrawableRes colorRes: Int): Drawable? {
-        return ContextCompat.getDrawable(context, colorRes)
+    fun getDrawable(@DrawableRes drawableRes: Int): Drawable? {
+        return ContextCompat.getDrawable(context, drawableRes)
     }
 
-    fun getDrawable(@DrawableRes colorRes: Int, @ColorInt color: Int): Drawable? {
-        return ContextCompat.getDrawable(context, colorRes)?.let {
+    fun getDrawable(@DrawableRes drawableRes: Int, @ColorInt color: Int): Drawable? {
+        return ContextCompat.getDrawable(context, drawableRes)?.let {
             ThemeUtils.tintDrawableWithColor(it, color)
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt b/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt
new file mode 100644
index 0000000000..2d6535758e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.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.app.features.form
+
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+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
+import im.vector.app.features.themes.ThemeUtils
+
+@EpoxyModelClass(layout = R.layout.item_form_advanced_toggle)
+abstract class FormAdvancedToggleItem : VectorEpoxyModel<FormAdvancedToggleItem.Holder>() {
+
+    @EpoxyAttribute lateinit var title: CharSequence
+    @EpoxyAttribute var expanded: Boolean = false
+    @EpoxyAttribute var listener: (() -> Unit)? = null
+
+    override fun bind(holder: Holder) {
+        super.bind(holder)
+        val tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary)
+        val expandedArrowDrawableRes = if (expanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white
+        val expandedArrowDrawable = ContextCompat.getDrawable(holder.view.context, expandedArrowDrawableRes)?.also {
+            DrawableCompat.setTint(it, tintColor)
+        }
+        holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
+        holder.titleView.text = title
+        holder.view.setOnClickListener { listener?.invoke() }
+    }
+
+    class Holder : VectorEpoxyHolder() {
+        val titleView by bind<TextView>(R.id.itemFormAdvancedToggleTitleView)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt
index 4b3eacffaa..61799a741f 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt
@@ -27,6 +27,9 @@ sealed class CreateRoomAction : VectorViewModelAction {
     data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction()
     data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction()
 
+    object ToggleShowAdvanced : CreateRoomAction()
+    data class DisableFederation(val disableFederation: Boolean) : CreateRoomAction()
+
     object Create : CreateRoomAction()
     object Reset : CreateRoomAction()
 }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt
index d1cc884336..157e192668 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt
@@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.loadingItem
 import im.vector.app.core.error.ErrorFormatter
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.discovery.settingsSectionTitleItem
+import im.vector.app.features.form.formAdvancedToggleItem
 import im.vector.app.features.form.formEditTextItem
 import im.vector.app.features.form.formEditableAvatarItem
 import im.vector.app.features.form.formSubmitButtonItem
@@ -148,6 +149,22 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
                 listener?.setIsEncrypted(value)
             }
         }
+        formAdvancedToggleItem {
+            id("showAdvanced")
+            title(stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced))
+            expanded(!viewState.showAdvanced)
+            listener { listener?.toggleShowAdvanced() }
+        }
+        if (viewState.showAdvanced) {
+            formSwitchItem {
+                id("federation")
+                enabled(enableFormElement)
+                title(stringProvider.getString(R.string.create_room_disable_federation_title, viewState.homeServerName))
+                summary(stringProvider.getString(R.string.create_room_disable_federation_description))
+                switchChecked(viewState.disableFederation)
+                listener { value -> listener?.setDisableFederation(value) }
+            }
+        }
         formSubmitButtonItem {
             id("submit")
             enabled(enableFormElement)
@@ -165,6 +182,8 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
         fun setIsInRoomDirectory(isInRoomDirectory: Boolean)
         fun setIsEncrypted(isEncrypted: Boolean)
         fun retry()
+        fun toggleShowAdvanced()
+        fun setDisableFederation(disableFederation: Boolean)
         fun submit()
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
index 88b8a65a1c..729c97370c 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
@@ -110,6 +110,14 @@ class CreateRoomFragment @Inject constructor(
         viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted))
     }
 
+    override fun toggleShowAdvanced() {
+        viewModel.handle(CreateRoomAction.ToggleShowAdvanced)
+    }
+
+    override fun setDisableFederation(disableFederation: Boolean) {
+        viewModel.handle(CreateRoomAction.DisableFederation(disableFederation))
+    }
+
     override fun submit() {
         viewModel.handle(CreateRoomAction.Create)
     }
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
index 57af95b107..fcb98916cc 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
@@ -53,9 +53,18 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
     }
 
     init {
+        initHomeServerName()
         initAdminE2eByDefault()
     }
 
+    private fun initHomeServerName() {
+        setState {
+            copy(
+                    homeServerName = session.myUserId.substringAfter(":")
+            )
+        }
+    }
+
     private var adminE2EByDefault = true
 
     private fun initAdminE2eByDefault() {
@@ -99,9 +108,27 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
             is CreateRoomAction.SetIsEncrypted       -> setIsEncrypted(action)
             is CreateRoomAction.Create               -> doCreateRoom()
             CreateRoomAction.Reset                   -> doReset()
+            CreateRoomAction.ToggleShowAdvanced      -> toggleShowAdvanced()
+            is CreateRoomAction.DisableFederation    -> disableFederation(action)
         }.exhaustive
     }
 
+    private fun disableFederation(action: CreateRoomAction.DisableFederation) {
+        setState {
+            copy(disableFederation = action.disableFederation)
+        }
+    }
+
+    private fun toggleShowAdvanced() {
+        setState {
+            copy(
+                    showAdvanced = !showAdvanced,
+                    // Reset to false if advanced is hidden
+                    disableFederation = disableFederation && !showAdvanced
+            )
+        }
+    }
+
     private fun doReset() {
         setState {
             // Delete temporary file with the avatar
@@ -151,6 +178,8 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
                     visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE
                     // Public room
                     preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
+                    // Disabling federation
+                    disableFederation = state.disableFederation
 
                     // Encryption
                     if (state.isEncrypted) {
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt
index 433cc02cc9..d1e5c0b1bd 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt
@@ -28,6 +28,9 @@ data class CreateRoomViewState(
         val isPublic: Boolean = false,
         val isInRoomDirectory: Boolean = false,
         val isEncrypted: Boolean = false,
+        val showAdvanced: Boolean = false,
+        val disableFederation: Boolean = false,
+        val homeServerName: String = "",
         val hsAdminHasDisabledE2E: Boolean = false,
         val asyncCreateRoomRequest: Async<String> = Uninitialized
 ) : MvRxState {
diff --git a/vector/src/main/res/layout/item_form_advanced_toggle.xml b/vector/src/main/res/layout/item_form_advanced_toggle.xml
new file mode 100644
index 0000000000..46609b64ca
--- /dev/null
+++ b/vector/src/main/res/layout/item_form_advanced_toggle.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/itemFormAdvancedToggleRootView"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?riotx_background"
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:paddingStart="@dimen/layout_horizontal_margin"
+    android:paddingTop="12dp"
+    android:paddingEnd="@dimen/layout_horizontal_margin"
+    android:paddingBottom="12dp">
+
+    <TextView
+        android:id="@+id/itemFormAdvancedToggleTitleView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:ellipsize="end"
+        android:gravity="center_vertical"
+        android:maxLines="1"
+        android:textColor="?riotx_text_secondary"
+        android:textSize="14sp"
+        android:textStyle="bold"
+        app:drawableTint="?riotx_text_secondary"
+        tools:drawableEnd="@drawable/ic_expand_more_white"
+        tools:text="@string/show_advanced" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_form_switch.xml b/vector/src/main/res/layout/item_form_switch.xml
index 24425a5eb4..cc662680bb 100644
--- a/vector/src/main/res/layout/item_form_switch.xml
+++ b/vector/src/main/res/layout/item_form_switch.xml
@@ -15,8 +15,6 @@
         android:layout_marginStart="@dimen/layout_horizontal_margin"
         android:layout_marginEnd="@dimen/layout_horizontal_margin"
         android:duplicateParentState="true"
-        android:ellipsize="end"
-        android:maxLines="1"
         android:textColor="?riotx_text_primary"
         android:textSize="15sp"
         app:layout_constraintBottom_toTopOf="@+id/formSwitchSummary"
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 48729df815..5768750021 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2094,6 +2094,12 @@
     <string name="create_room_encryption_title">"Enable encryption"</string>
     <string name="create_room_encryption_description">"Once enabled, encryption cannot be disabled."</string>
 
+    <string name="show_advanced">Show advanced</string>
+    <string name="hide_advanced">Hide advanced</string>
+
+    <string name="create_room_disable_federation_title">Block anyone not part of %s from ever joining this room</string>
+    <string name="create_room_disable_federation_description">You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.</string>
+
     <string name="login_error_threepid_denied">Your email domain is not authorized to register on this server</string>
 
     <string name="verification_conclusion_warning">Untrusted sign in</string>