Base URL dropdown; working

This commit is contained in:
Philipp Heckel 2022-02-12 15:26:18 -05:00
parent 29a40080db
commit 3dcf4939c8
5 changed files with 199 additions and 91 deletions

View file

@ -20,9 +20,8 @@ import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.User import io.heckel.ntfy.db.User
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -51,7 +50,6 @@ class AddFragment : DialogFragment() {
private lateinit var subscribeErrorTextImage: View private lateinit var subscribeErrorTextImage: View
// Login page // Login page
private lateinit var users: List<User>
private lateinit var loginUsernameText: TextInputEditText private lateinit var loginUsernameText: TextInputEditText
private lateinit var loginPasswordText: TextInputEditText private lateinit var loginPasswordText: TextInputEditText
private lateinit var loginProgress: ProgressBar private lateinit var loginProgress: ProgressBar
@ -90,6 +88,7 @@ class AddFragment : DialogFragment() {
subscribeTopicText = view.findViewById(R.id.add_dialog_subscribe_topic_text) subscribeTopicText = view.findViewById(R.id.add_dialog_subscribe_topic_text)
subscribeBaseUrlLayout = view.findViewById(R.id.add_dialog_subscribe_base_url_layout) subscribeBaseUrlLayout = view.findViewById(R.id.add_dialog_subscribe_base_url_layout)
subscribeBaseUrlLayout.background = view.background subscribeBaseUrlLayout.background = view.background
subscribeBaseUrlLayout.makeEndIconSmaller(resources) // Hack!
subscribeBaseUrlText = view.findViewById(R.id.add_dialog_subscribe_base_url_text) subscribeBaseUrlText = view.findViewById(R.id.add_dialog_subscribe_base_url_text)
subscribeBaseUrlText.background = view.background subscribeBaseUrlText.background = view.background
subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_box) subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_box)
@ -103,13 +102,6 @@ class AddFragment : DialogFragment() {
subscribeErrorTextImage = view.findViewById(R.id.add_dialog_subscribe_error_text_image) subscribeErrorTextImage = view.findViewById(R.id.add_dialog_subscribe_error_text_image)
subscribeErrorTextImage.visibility = View.GONE subscribeErrorTextImage.visibility = View.GONE
// Hack: Make end icon smaller, see https://stackoverflow.com/a/57098715/1440785
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30f, resources.displayMetrics)
val endIconImageView = subscribeBaseUrlLayout.findViewById<ImageView>(R.id.text_input_end_icon)
endIconImageView.minimumHeight = dimension.toInt()
endIconImageView.minimumWidth = dimension.toInt()
subscribeBaseUrlLayout.requestLayout()
// Fields for "login page" // Fields for "login page"
loginUsernameText = view.findViewById(R.id.add_dialog_login_username) loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
loginPasswordText = view.findViewById(R.id.add_dialog_login_password) loginPasswordText = view.findViewById(R.id.add_dialog_login_password)
@ -124,45 +116,11 @@ class AddFragment : DialogFragment() {
getString(R.string.add_dialog_use_another_server_description_noinstant) getString(R.string.add_dialog_use_another_server_description_noinstant)
} }
// Base URL dropdown behavior; Oh my, why is this so complicated?! // Show/hide based on flavor
val toggleEndIcon = { subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
if (subscribeBaseUrlText.text.isNotEmpty()) {
subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
} else if (baseUrls.isEmpty()) {
subscribeBaseUrlLayout.setEndIconDrawable(0)
} else {
subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
}
}
subscribeBaseUrlLayout.setEndIconOnClickListener {
if (subscribeBaseUrlText.text.isNotEmpty()) {
subscribeBaseUrlText.text.clear()
if (baseUrls.isEmpty()) {
subscribeBaseUrlLayout.setEndIconDrawable(0)
} else {
subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
}
} else if (subscribeBaseUrlText.text.isEmpty() && baseUrls.isNotEmpty()) {
subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_up_gray_24dp)
subscribeBaseUrlText.showDropDown()
}
}
subscribeBaseUrlText.setOnDismissListener { toggleEndIcon() }
subscribeBaseUrlText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
toggleEndIcon()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
})
// Fill autocomplete for base URL & users drop-down // Add baseUrl auto-complete behavior
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
// Auto-complete
val appBaseUrl = getString(R.string.app_base_url) val appBaseUrl = getString(R.string.app_base_url)
baseUrls = repository.getSubscriptions() baseUrls = repository.getSubscriptions()
.groupBy { it.baseUrl } .groupBy { it.baseUrl }
@ -170,27 +128,11 @@ class AddFragment : DialogFragment() {
.filterNot { it == appBaseUrl } .filterNot { it == appBaseUrl }
.sorted() .sorted()
val activity = activity ?: return@launch // We may have pressed "Cancel" val activity = activity ?: return@launch // We may have pressed "Cancel"
val adapter = ArrayAdapter(activity, R.layout.fragment_add_dialog_dropdown_item, baseUrls)
activity.runOnUiThread { activity.runOnUiThread {
subscribeBaseUrlText.threshold = 1 initBaseUrlDropdown(baseUrls, subscribeBaseUrlText, subscribeBaseUrlLayout)
subscribeBaseUrlText.setAdapter(adapter)
if (baseUrls.count() == 1) {
subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
subscribeBaseUrlText.setText(baseUrls.first())
} else if (baseUrls.count() > 1) {
subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
} else {
subscribeBaseUrlLayout.setEndIconDrawable(0)
}
} }
// Users dropdown
users = repository.getUsers()
} }
// Show/hide based on flavor
subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
// Username/password validation on type // Username/password validation on type
val loginTextWatcher = object : TextWatcher { val loginTextWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
@ -384,13 +326,9 @@ class AddFragment : DialogFragment() {
if (subscription != null || DISALLOWED_TOPICS.contains(topic)) { if (subscription != null || DISALLOWED_TOPICS.contains(topic)) {
positiveButton.isEnabled = false positiveButton.isEnabled = false
} else if (subscribeUseAnotherServerCheckbox.isChecked) { } else if (subscribeUseAnotherServerCheckbox.isChecked) {
positiveButton.isEnabled = topic.isNotBlank() positiveButton.isEnabled = validTopic(topic) && validUrl(baseUrl)
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic)
&& baseUrl.isNotBlank()
&& "^https?://.+".toRegex().matches(baseUrl)
} else { } else {
positiveButton.isEnabled = topic.isNotBlank() positiveButton.isEnabled = validTopic(topic)
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic)
} }
} }
} }

View file

@ -0,0 +1,58 @@
package io.heckel.ntfy.ui
import android.text.Editable
import android.text.TextWatcher
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.R
fun initBaseUrlDropdown(baseUrls: List<String>, textView: AutoCompleteTextView, layout: TextInputLayout) {
// Base URL dropdown behavior; Oh my, why is this so complicated?!
val toggleEndIcon = {
if (textView.text.isNotEmpty()) {
layout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
} else if (baseUrls.isEmpty()) {
layout.setEndIconDrawable(0)
} else {
layout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
}
}
layout.setEndIconOnClickListener {
if (textView.text.isNotEmpty()) {
textView.text.clear()
if (baseUrls.isEmpty()) {
layout.setEndIconDrawable(0)
} else {
layout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
}
} else if (textView.text.isEmpty() && baseUrls.isNotEmpty()) {
layout.setEndIconDrawable(R.drawable.ic_drop_up_gray_24dp)
textView.showDropDown()
}
}
textView.setOnDismissListener { toggleEndIcon() }
textView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
toggleEndIcon()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
})
val adapter = ArrayAdapter(textView.context, R.layout.fragment_add_dialog_dropdown_item, baseUrls)
textView.threshold = 1
textView.setAdapter(adapter)
if (baseUrls.count() == 1) {
layout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
textView.setText(baseUrls.first())
} else if (baseUrls.count() > 1) {
layout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
} else {
layout.setEndIconDrawable(0)
}
}

View file

@ -10,12 +10,10 @@ import android.text.TextWatcher
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.*
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
@ -30,6 +28,9 @@ class ShareActivity : AppCompatActivity() {
// File to share // File to share
private var fileUri: Uri? = null private var fileUri: Uri? = null
// List of base URLs used, excluding app_base_url
private lateinit var baseUrls: List<String>
// UI elements // UI elements
private lateinit var menu: Menu private lateinit var menu: Menu
private lateinit var sendItem: MenuItem private lateinit var sendItem: MenuItem
@ -39,6 +40,9 @@ class ShareActivity : AppCompatActivity() {
private lateinit var contentFileIcon: ImageView private lateinit var contentFileIcon: ImageView
private lateinit var contentText: TextView private lateinit var contentText: TextView
private lateinit var topicText: TextView private lateinit var topicText: TextView
private lateinit var baseUrlLayout: TextInputLayout
private lateinit var baseUrlText: AutoCompleteTextView
private lateinit var useAnotherServerCheckbox: CheckBox
private lateinit var progress: ProgressBar private lateinit var progress: ProgressBar
private lateinit var errorText: TextView private lateinit var errorText: TextView
private lateinit var errorImage: ImageView private lateinit var errorImage: ImageView
@ -48,7 +52,7 @@ class ShareActivity : AppCompatActivity() {
setContentView(R.layout.activity_share) setContentView(R.layout.activity_share)
Log.init(this) // Init logs in all entry points Log.init(this) // Init logs in all entry points
Log.d(TAG, "Create $this") Log.d(TAG, "Create $this with intent $intent")
// Action bar // Action bar
title = getString(R.string.share_title) title = getString(R.string.share_title)
@ -63,6 +67,12 @@ class ShareActivity : AppCompatActivity() {
contentFileInfo = findViewById(R.id.share_content_file_info) contentFileInfo = findViewById(R.id.share_content_file_info)
contentFileIcon = findViewById(R.id.share_content_file_icon) contentFileIcon = findViewById(R.id.share_content_file_icon)
topicText = findViewById(R.id.share_topic_text) topicText = findViewById(R.id.share_topic_text)
baseUrlLayout = findViewById(R.id.share_base_url_layout)
//baseUrlLayout.background = window.background
baseUrlLayout.makeEndIconSmaller(resources) // Hack!
baseUrlText = findViewById(R.id.share_base_url_text)
//baseUrlText.background = topicText.background
useAnotherServerCheckbox = findViewById(R.id.share_use_another_server_checkbox)
progress = findViewById(R.id.share_progress) progress = findViewById(R.id.share_progress)
progress.visibility = View.GONE progress.visibility = View.GONE
errorText = findViewById(R.id.share_error_text) errorText = findViewById(R.id.share_error_text)
@ -84,10 +94,31 @@ class ShareActivity : AppCompatActivity() {
contentText.addTextChangedListener(textWatcher) contentText.addTextChangedListener(textWatcher)
topicText.addTextChangedListener(textWatcher) topicText.addTextChangedListener(textWatcher)
// Add behavior to "use another" checkbox
useAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked ->
baseUrlLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
validateInput()
}
// Add baseUrl auto-complete behavior
lifecycleScope.launch(Dispatchers.IO) {
val appBaseUrl = getString(R.string.app_base_url)
baseUrls = repository.getSubscriptions()
.groupBy { it.baseUrl }
.map { it.key }
.filterNot { it == appBaseUrl }
.sorted()
val activity = this@ShareActivity
activity.runOnUiThread {
initBaseUrlDropdown(baseUrls, baseUrlText, baseUrlLayout)
useAnotherServerCheckbox.isChecked = baseUrls.count() == 1
}
}
// Incoming intent // Incoming intent
val intent = intent ?: return val intent = intent ?: return
if (intent.action != Intent.ACTION_SEND) return if (intent.action != Intent.ACTION_SEND) return
if ("text/plain" == intent.type) { if (intent.type == "text/plain") {
handleSendText(intent) handleSendText(intent)
} else if (supportedImage(intent.type)) { } else if (supportedImage(intent.type)) {
handleSendImage(intent) handleSendImage(intent)
@ -97,14 +128,19 @@ class ShareActivity : AppCompatActivity() {
} }
private fun handleSendText(intent: Intent) { private fun handleSendText(intent: Intent) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { text -> val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "(no text)"
contentText.text = text Log.d(TAG, "Shared content is text: $text")
show() contentText.text = text
} show()
} }
private fun handleSendImage(intent: Intent) { private fun handleSendImage(intent: Intent) {
fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
Log.d(TAG, "Shared content is an image with URI $fileUri")
if (fileUri == null) {
Log.w(TAG, "Null URI is not allowed. Aborting.")
return
}
try { try {
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
val bitmapStream = resolver.openInputStream(fileUri!!) val bitmapStream = resolver.openInputStream(fileUri!!)
@ -121,7 +157,12 @@ class ShareActivity : AppCompatActivity() {
} }
private fun handleSendFile(intent: Intent) { private fun handleSendFile(intent: Intent) {
fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
Log.d(TAG, "Shared content is a file with URI $fileUri")
if (fileUri == null) {
Log.w(TAG, "Null URI is not allowed. Aborting.")
return
}
try { try {
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
val info = fileStat(this, fileUri) val info = fileStat(this, fileUri)
@ -130,7 +171,6 @@ class ShareActivity : AppCompatActivity() {
contentFileInfo.text = "${info.filename}\n${formatBytes(info.size)}" contentFileInfo.text = "${info.filename}\n${formatBytes(info.size)}"
contentFileIcon.setImageResource(mimeTypeToIconResource(mimeType)) contentFileIcon.setImageResource(mimeTypeToIconResource(mimeType))
show(file = true) show(file = true)
} catch (e: Exception) { } catch (e: Exception) {
fileUri = null fileUri = null
contentText.text = "" contentText.text = ""
@ -170,7 +210,7 @@ class ShareActivity : AppCompatActivity() {
} }
private fun onShareClick() { private fun onShareClick() {
val baseUrl = "https://ntfy.sh" // FIXME val baseUrl = getBaseUrl()
val topic = topicText.text.toString() val topic = topicText.text.toString()
val message = contentText.text.toString() val message = contentText.text.toString()
progress.visibility = View.VISIBLE progress.visibility = View.VISIBLE
@ -226,8 +266,21 @@ class ShareActivity : AppCompatActivity() {
private fun validateInput() { private fun validateInput() {
if (!this::sendItem.isInitialized) return // Initialized late in onCreateOptionsMenu if (!this::sendItem.isInitialized) return // Initialized late in onCreateOptionsMenu
sendItem.isEnabled = contentText.text.isNotEmpty() && topicText.text.isNotEmpty() val enabled = if (useAnotherServerCheckbox.isChecked) {
sendItem.icon.alpha = if (sendItem.isEnabled) 255 else 130 contentText.text.isNotEmpty() && validTopic(topicText.text.toString()) && validUrl(baseUrlText.text.toString())
} else {
contentText.text.isNotEmpty() && topicText.text.isNotEmpty()
}
sendItem.isEnabled = enabled
sendItem.icon.alpha = if (enabled) 255 else 130
}
private fun getBaseUrl(): String {
return if (useAnotherServerCheckbox.isChecked) {
baseUrlText.text.toString()
} else {
getString(R.string.app_base_url)
}
} }
companion object { companion object {

View file

@ -6,11 +6,15 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.TypedValue
import android.view.View
import android.view.Window import android.view.Window
import android.widget.ImageView
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Notification
@ -40,6 +44,14 @@ fun shortUrl(url: String) = url
.replace("http://", "") .replace("http://", "")
.replace("https://", "") .replace("https://", "")
fun validTopic(topic: String): Boolean {
return "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic) // Must match server side!
}
fun validUrl(url: String): Boolean {
return "^https?://.+".toRegex().matches(url)
}
fun formatDateShort(timestampSecs: Long): String { fun formatDateShort(timestampSecs: Long): String {
val date = Date(timestampSecs*1000) val date = Date(timestampSecs*1000)
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date) return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date)
@ -271,3 +283,12 @@ class ContentUriRequestBody(
} }
} }
} }
// Hack: Make end icon for drop down smaller, see https://stackoverflow.com/a/57098715/1440785
fun View.makeEndIconSmaller(resources: Resources) {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30f, resources.displayMetrics)
val endIconImageView = findViewById<ImageView>(R.id.text_input_end_icon)
endIconImageView.minimumHeight = dimension.toInt()
endIconImageView.minimumWidth = dimension.toInt()
requestLayout()
}

View file

@ -21,9 +21,9 @@
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:text="@string/share_content_title" android:text="@string/share_content_title"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent" android:paddingStart="2dp"/>
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
@ -75,9 +75,9 @@
android:paddingBottom="3dp" android:paddingBottom="3dp"
android:text="Share to topic" android:text="Share to topic"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_content_file_box" android:layout_marginTop="10dp"/> app:layout_constraintTop_toBottomOf="@id/share_content_file_box" android:layout_marginTop="15dp" android:paddingStart="2dp"/>
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/share_topic_text" android:id="@+id/share_topic_text"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -86,6 +86,44 @@
android:maxLines="1" android:inputType="text" android:maxLength="64" android:maxLines="1" android:inputType="text" android:maxLength="64"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_topic_title"/> app:layout_constraintTop_toBottomOf="@id/share_topic_title"/>
<CheckBox
android:text="@string/add_dialog_use_another_server"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/share_use_another_server_checkbox"
android:layout_marginStart="-3dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_topic_text"/>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
android:id="@+id/share_base_url_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:padding="0dp"
android:visibility="gone"
app:endIconMode="custom"
app:hintEnabled="false"
app:boxBackgroundColor="@null" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_use_another_server_checkbox">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/share_base_url_text"
android:hint="@string/app_base_url"
android:maxLines="1"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:inputType="textNoSuggestions"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
</com.google.android.material.textfield.TextInputLayout>
<TextView <TextView
android:text="Unable to resolve host example.com" android:text="Unable to resolve host example.com"
android:layout_width="0dp" android:layout_width="0dp"
@ -95,7 +133,7 @@
android:paddingEnd="4dp" android:paddingEnd="4dp"
android:textAppearance="@style/DangerText" android:textAppearance="@style/DangerText"
app:layout_constraintStart_toEndOf="@id/share_error_image" app:layout_constraintStart_toEndOf="@id/share_error_image"
android:layout_marginTop="5dp" app:layout_constraintTop_toBottomOf="@+id/share_topic_text"/> android:layout_marginTop="5dp" app:layout_constraintTop_toBottomOf="@id/share_base_url_layout"/>
<ImageView <ImageView
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp" android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"