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) {
binding.statusView.show()
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 -> {

View file

@ -1,10 +1,19 @@
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.SpannableStringBuilder
import android.text.Spanned
import android.text.style.CharacterStyle
import android.text.style.DynamicDrawableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
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 kotlin.math.max
@ -61,6 +70,66 @@ private class PatternFinder(
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>) {
for (span in text.getSpans(0, text.length, spanClass)) {
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 {
return (codePoint in 0x30..0x39) || // [0-9]
(codePoint in 0x41..0x5a) || // [A-Z]

View file

@ -6,15 +6,16 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding
import com.keylesspalace.tusky.util.addDrawables
import com.keylesspalace.tusky.util.visible
/**
* This view is used for screens with downloadable content which may fail.
* Can show an image, text and button below them.
* This view is used for screens with content which may be empty or might have failed to download.
*/
class BackgroundMessageView @JvmOverloads constructor(
context: Context,
@ -47,4 +48,15 @@ class BackgroundMessageView @JvmOverloads constructor(
binding.button.setOnClickListener(clickListener)
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: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
android:id="@+id/progressBar"
android:layout_width="wrap_content"
@ -35,8 +23,9 @@
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/statusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
android:src="@android:color/transparent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
@ -46,6 +35,18 @@
tools:src="@drawable/elephant_error"
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
android:id="@+id/topProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"

View file

@ -6,6 +6,28 @@
android:layout_height="match_parent"
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
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
@ -18,27 +40,6 @@
</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
android:id="@+id/topProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"

View file

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

View file

@ -660,4 +660,9 @@
<string name="filter_edit_keyword_title">Schlagwort bearbeiten</string>
<string name="filter_description_format">%s: %s</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>

View file

@ -802,4 +802,9 @@
<string name="filter_keyword_addition_title">Add keyword</string>
<string name="filter_edit_keyword_title">Edit keyword</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>