mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
A hashtag picker dialog was implemented twice, with slight differences. Now there is only one - with hashtag validation - no more api errors when following an invalid one - The dialog can now be closed with the keyboard, for extra fast hashtag selection - with autocomplete I also added a new snackbar when following a hashtag was succesfull. Although I'm not sure about the auto complete, it can be very annoying as the drop down covers the buttons. I found no way to make it size to its content: https://chaos.social/@ConnyDuck/113803457147888844 Should we get rid of it?pull/4846/merge
54 changed files with 234 additions and 211 deletions
@ -0,0 +1,118 @@ |
|||||||
|
/* Copyright 2025 Tusky Contributors |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||||
|
* License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||||
|
* Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
@file:JvmName("HashTagPickerDialog") |
||||||
|
|
||||||
|
package com.keylesspalace.tusky.view |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.util.Log |
||||||
|
import android.view.KeyEvent |
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.WindowManager |
||||||
|
import android.view.inputmethod.EditorInfo |
||||||
|
import android.widget.TextView |
||||||
|
import android.widget.TextView.OnEditorActionListener |
||||||
|
import androidx.annotation.StringRes |
||||||
|
import androidx.appcompat.app.AlertDialog |
||||||
|
import androidx.core.widget.doOnTextChanged |
||||||
|
import at.connyduck.calladapter.networkresult.fold |
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter |
||||||
|
import com.keylesspalace.tusky.components.search.SearchType |
||||||
|
import com.keylesspalace.tusky.databinding.DialogPickHashtagBinding |
||||||
|
import com.keylesspalace.tusky.network.MastodonApi |
||||||
|
import com.keylesspalace.tusky.util.hashtagPattern |
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
import kotlinx.coroutines.Dispatchers |
||||||
|
import kotlinx.coroutines.cancel |
||||||
|
import kotlinx.coroutines.runBlocking |
||||||
|
|
||||||
|
fun Context.showHashtagPickerDialog( |
||||||
|
api: MastodonApi, |
||||||
|
@StringRes title: Int, |
||||||
|
onHashtagSelected: (String) -> Unit |
||||||
|
) { |
||||||
|
val dialogScope = CoroutineScope(Dispatchers.Main) |
||||||
|
val dialogBinding = DialogPickHashtagBinding.inflate(LayoutInflater.from(this)) |
||||||
|
val autocompleteTextView = dialogBinding.pickHashtagEditText |
||||||
|
|
||||||
|
val autoCompleteProvider = object : ComposeAutoCompleteAdapter.AutocompletionProvider { |
||||||
|
override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> { |
||||||
|
return runBlocking { |
||||||
|
api.search(query = token, type = SearchType.Hashtag.apiParameter, limit = 5) |
||||||
|
.fold({ searchResult -> |
||||||
|
searchResult.hashtags.map { |
||||||
|
ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult( |
||||||
|
it.name |
||||||
|
) |
||||||
|
} |
||||||
|
}, { e -> |
||||||
|
Log.e("HashtagPickerDialog", "Autocomplete search for $token failed", e) |
||||||
|
emptyList() |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
autocompleteTextView.setAdapter( |
||||||
|
ComposeAutoCompleteAdapter( |
||||||
|
autoCompleteProvider, |
||||||
|
animateAvatar = false, |
||||||
|
animateEmojis = false, |
||||||
|
showBotBadge = false, |
||||||
|
withDecoration = false |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
autocompleteTextView.setSelection(autocompleteTextView.length()) |
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(this) |
||||||
|
.setTitle(title) |
||||||
|
.setView(dialogBinding.root) |
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> |
||||||
|
onHashtagSelected(autocompleteTextView.text.toString()) |
||||||
|
} |
||||||
|
.setNegativeButton(android.R.string.cancel, null) |
||||||
|
.setOnDismissListener { |
||||||
|
dialogScope.cancel() |
||||||
|
} |
||||||
|
.create() |
||||||
|
|
||||||
|
autocompleteTextView.doOnTextChanged { s, _, _, _ -> |
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(s) |
||||||
|
} |
||||||
|
|
||||||
|
autocompleteTextView.setOnEditorActionListener(object : OnEditorActionListener { |
||||||
|
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { |
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE && validateHashtag(autocompleteTextView.text)) { |
||||||
|
onHashtagSelected(autocompleteTextView.text.toString()) |
||||||
|
dialog.dismiss() |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) |
||||||
|
dialog.show() |
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(autocompleteTextView.text) |
||||||
|
autocompleteTextView.requestFocus() |
||||||
|
} |
||||||
|
|
||||||
|
private fun validateHashtag(input: CharSequence?): Boolean { |
||||||
|
val trimmedInput = input?.trim() ?: "" |
||||||
|
return trimmedInput.isNotEmpty() && hashtagPattern.matcher(trimmedInput).matches() |
||||||
|
} |
||||||
@ -1,27 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="wrap_content" |
|
||||||
android:paddingLeft="20dp" |
|
||||||
android:paddingTop="16dp" |
|
||||||
android:paddingRight="20dp"> |
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout |
|
||||||
android:id="@+id/addHashtagInputLayout" |
|
||||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="wrap_content" |
|
||||||
android:hint="@string/edit_hashtag_hint" |
|
||||||
app:endIconMode="none"> |
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText |
|
||||||
android:id="@+id/addHashtagEditText" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="wrap_content" |
|
||||||
android:inputType="textNoSuggestions" |
|
||||||
android:lines="1" /> |
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout> |
|
||||||
|
|
||||||
</FrameLayout> |
|
||||||
Loading…
Reference in new issue