3408 home help message (#3415)

* 3408: First draft of help message on empty home timeline

* 3408: Move image spanning to utils; tweak gui a bit (looks like status)

* 3408: Use proper R again; appease linter

* 3408: Add doc; remove narrow comment

* 3408: null is default

* 3408: Add German text

* 3408: Stack refresh animation on top of help message (reorder)
This commit is contained in:
UlrichKu 2023-03-21 19:44:35 +01:00 committed by GitHub
parent 9484a8b2b9
commit 182df2bfae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 200 additions and 93 deletions

View file

@ -236,6 +236,9 @@ class TimelineFragment :
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show() binding.statusView.show()
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
if (kind == TimelineViewModel.Kind.HOME) {
binding.statusView.showHelp(R.string.help_empty_home)
}
} }
} }
is LoadState.Error -> { is LoadState.Error -> {

View file

@ -1,10 +1,19 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.CharacterStyle import android.text.style.CharacterStyle
import android.text.style.DynamicDrawableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.URLSpan import android.text.style.URLSpan
import androidx.appcompat.content.res.AppCompatResources
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.max import kotlin.math.max
@ -61,6 +70,66 @@ private class PatternFinder(
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE) val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
} }
/**
* Takes text containing mentions and hashtags and urls and makes them the given colour.
*/
fun highlightSpans(text: Spannable, colour: Int) {
// Strip all existing colour spans.
for (spanClass in spanClasses) {
clearSpans(text, spanClass)
}
// Colour the mentions and hashtags.
val string = text.toString()
val length = text.length
var start = 0
var end = 0
while (end in 0 until length && start >= 0) {
// Search for url first because it can contain the other characters
val found = findPattern(string, end)
start = found.start
end = found.end
if (start in 0 until end) {
text.setSpan(getSpan(found.matchType, string, colour, start, end), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
start += finders[found.matchType]!!.searchPrefixWidth
}
}
}
/**
* Replaces text of the form [drawabale name] or [iconics name] with their spanned counterparts (ImageSpan).
*/
fun addDrawables(text: CharSequence, color: Int, size: Int, context: Context): Spannable {
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) DynamicDrawableSpan.ALIGN_CENTER else DynamicDrawableSpan.ALIGN_BASELINE
val builder = SpannableStringBuilder(text)
val pattern = Pattern.compile("\\[(drawable|iconics) ([0-9a-z_]+)\\]")
val matcher = pattern.matcher(builder)
while (matcher.find()) {
val resourceType = matcher.group(1)
val resourceName = matcher.group(2)
?: continue
val drawable: Drawable? = when (resourceType) {
"iconics" -> IconicsDrawable(context, GoogleMaterial.getIcon(resourceName))
else -> {
val drawableResourceId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
if (drawableResourceId != 0) AppCompatResources.getDrawable(context, drawableResourceId) else null
}
}
if (drawable != null) {
drawable.setBounds(0, 0, size, size)
drawable.setTint(color)
builder.setSpan(ImageSpan(drawable, alignment), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
return builder
}
private fun <T> clearSpans(text: Spannable, spanClass: Class<T>) { private fun <T> clearSpans(text: Spannable, spanClass: Class<T>) {
for (span in text.getSpans(0, text.length, spanClass)) { for (span in text.getSpans(0, text.length, spanClass)) {
text.removeSpan(span) text.removeSpan(span)
@ -136,30 +205,6 @@ private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, star
} }
} }
/** Takes text containing mentions and hashtags and urls and makes them the given colour. */
fun highlightSpans(text: Spannable, colour: Int) {
// Strip all existing colour spans.
for (spanClass in spanClasses) {
clearSpans(text, spanClass)
}
// Colour the mentions and hashtags.
val string = text.toString()
val length = text.length
var start = 0
var end = 0
while (end in 0 until length && start >= 0) {
// Search for url first because it can contain the other characters
val found = findPattern(string, end)
start = found.start
end = found.end
if (start in 0 until end) {
text.setSpan(getSpan(found.matchType, string, colour, start, end), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
start += finders[found.matchType]!!.searchPrefixWidth
}
}
}
private fun isWordCharacters(codePoint: Int): Boolean { private fun isWordCharacters(codePoint: Int): Boolean {
return (codePoint in 0x30..0x39) || // [0-9] return (codePoint in 0x30..0x39) || // [0-9]
(codePoint in 0x41..0x5a) || // [A-Z] (codePoint in 0x41..0x5a) || // [A-Z]

View file

@ -6,15 +6,16 @@ import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding
import com.keylesspalace.tusky.util.addDrawables
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
/** /**
* This view is used for screens with downloadable content which may fail. * This view is used for screens with content which may be empty or might have failed to download.
* Can show an image, text and button below them.
*/ */
class BackgroundMessageView @JvmOverloads constructor( class BackgroundMessageView @JvmOverloads constructor(
context: Context, context: Context,
@ -47,4 +48,15 @@ class BackgroundMessageView @JvmOverloads constructor(
binding.button.setOnClickListener(clickListener) binding.button.setOnClickListener(clickListener)
binding.button.visible(clickListener != null) binding.button.visible(clickListener != null)
} }
fun showHelp(@StringRes helpRes: Int) {
val size: Int = binding.helpText.textSize.toInt() + 2
val color = binding.helpText.currentTextColor
val text = context.getText(helpRes)
val textWithDrawables = addDrawables(text, color, size, context)
binding.helpText.setText(textWithDrawables, TextView.BufferType.SPANNABLE)
binding.helpText.visible(true)
}
} }

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="1dp"
android:left="-1dp"
android:right="-1dp"
android:top="1dp">
<shape android:shape="rectangle">
<solid android:color="?attr/colorBackgroundAccent"/>
<stroke android:width="1dp" android:color="?attr/dividerColor"/>
</shape>
</item>
</layer-list>

View file

@ -12,18 +12,6 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:background="?android:attr/colorBackground"> android:background="?android:attr/colorBackground">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -35,8 +23,9 @@
<com.keylesspalace.tusky.view.BackgroundMessageView <com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/statusView" android:id="@+id/statusView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
android:src="@android:color/transparent" android:src="@android:color/transparent"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -46,6 +35,18 @@
tools:src="@drawable/elephant_error" tools:src="@drawable/elephant_error"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar" android:id="@+id/topProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="@style/Widget.AppCompat.ProgressBar.Horizontal"

View file

@ -6,6 +6,28 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/colorBackground"> android:background="?android:attr/colorBackground">
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/statusView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
android:visibility="gone"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -18,27 +40,6 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/statusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar" android:id="@+id/topProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="@style/Widget.AppCompat.ProgressBar.Horizontal"

View file

@ -1,39 +1,60 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:gravity="center_horizontal" android:layout_width="match_parent"
tools:orientation="vertical" android:layout_height="match_parent"
android:orientation="vertical"
tools:parentTag="android.widget.LinearLayout"> tools:parentTag="android.widget.LinearLayout">
<TextView
android:id="@+id/helpText"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.1"
android:textColor="@color/textColorPrimary"
android:background="@drawable/help_message_background"
android:layout_marginTop="16dp"
android:padding="16dp"
android:textAlignment="viewStart"
android:textSize="?attr/status_text_medium" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/imageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_weight="1"
android:contentDescription="@null" android:contentDescription="@null"
android:scaleType="centerInside" android:scaleType="centerInside"
tools:src="@drawable/elephant_offline" /> android:src="@drawable/elephant_offline" />
<TextView <TextView
android:id="@+id/messageTextView" android:id="@+id/messageTextView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="16dp" android:drawablePadding="16dp"
android:lineSpacingMultiplier="1.1" android:lineSpacingMultiplier="1.1"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp" android:paddingRight="16dp"
android:paddingTop="16dp"
android:text="@string/error_network"
android:textAlignment="center" android:textAlignment="center"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium" />
tools:text="@string/error_network" />
<Button <Button
android:id="@+id/button" android:id="@+id/button"
style="@style/TuskyButton.Outlined" style="@style/TuskyButton.Outlined"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:layout_marginTop="8dp"
android:text="@string/action_retry" /> android:text="@string/action_retry" />
</LinearLayout>
</merge> </merge>

View file

@ -660,4 +660,9 @@
<string name="filter_edit_keyword_title">Schlagwort bearbeiten</string> <string name="filter_edit_keyword_title">Schlagwort bearbeiten</string>
<string name="filter_description_format">%s: %s</string> <string name="filter_description_format">%s: %s</string>
<string name="status_filtered_show_anyway">Trotzdem anzeigen</string> <string name="status_filtered_show_anyway">Trotzdem anzeigen</string>
<string name="help_empty_home">Dies ist deine <b>Startseite</b>. Sie zeigt die neuesten Beiträge der Accounts,
denen du folgst.\n\nUm andere Accounts zu finden, kannst du entweder andere Timelines lesen.
Zum Beispiel die Lokale Timeline deiner Instanz [drawable ic_local_24dp]. Oder du kannst nach ihrem Namen suchen
[iconics gmd_search]; suche z. B. nach Tusky, um unseren Mastodon-Account zu finden.</string>
</resources> </resources>

View file

@ -802,4 +802,9 @@
<string name="filter_keyword_addition_title">Add keyword</string> <string name="filter_keyword_addition_title">Add keyword</string>
<string name="filter_edit_keyword_title">Edit keyword</string> <string name="filter_edit_keyword_title">Edit keyword</string>
<string name="filter_description_format">%s: %s</string> <string name="filter_description_format">%s: %s</string>
<string name="help_empty_home">This is your <b>home timeline</b>. It shows the recent posts of the accounts
you follow.\n\nTo explore accounts you can either discover them in one of the other timelines.
For example the local timeline of your instance [drawable ic_local_24dp]. Or you can search them
by name [iconics gmd_search]; for example search for Tusky to find our Mastodon account.</string>
</resources> </resources>