Browse Source

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)
pull/3485/head
UlrichKu 3 years ago committed by GitHub
parent
commit
182df2bfae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt
  2. 93
      app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt
  3. 16
      app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt
  4. 14
      app/src/main/res/drawable/help_message_background.xml
  5. 31
      app/src/main/res/layout-sw640dp/fragment_timeline.xml
  6. 45
      app/src/main/res/layout/fragment_timeline.xml
  7. 77
      app/src/main/res/layout/view_background_message.xml
  8. 9
      app/src/main/res/values-de/strings.xml
  9. 5
      app/src/main/res/values/strings.xml

3
app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt

@ -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 -> {

93
app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt

@ -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]

16
app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt

@ -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)
}
} }

14
app/src/main/res/drawable/help_message_background.xml

@ -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>

31
app/src/main/res/layout-sw640dp/fragment_timeline.xml

@ -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"
@ -60,4 +61,4 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </FrameLayout>

45
app/src/main/res/layout/fragment_timeline.xml

@ -6,17 +6,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/colorBackground"> android:background="?android:attr/colorBackground">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/swipeRefreshLayout" android:id="@+id/statusView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
<androidx.recyclerview.widget.RecyclerView android:visibility="gone"
android:id="@+id/recyclerView" app:layout_constrainedHeight="true"
android:layout_width="match_parent" app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="match_parent" /> app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
@ -27,17 +28,17 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.keylesspalace.tusky.view.BackgroundMessageView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/statusView" android:id="@+id/swipeRefreshLayout"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:visibility="gone"
app:layout_constrainedHeight="true" <androidx.recyclerview.widget.RecyclerView
app:layout_constraintBottom_toBottomOf="parent" android:id="@+id/recyclerView"
app:layout_constraintLeft_toLeftOf="parent" android:layout_width="match_parent"
app:layout_constraintRight_toRightOf="parent" android:layout_height="match_parent" />
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar" android:id="@+id/topProgressBar"
@ -51,4 +52,4 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

77
app/src/main/res/layout/view_background_message.xml

@ -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">
<ImageView <TextView
android:id="@+id/imageView" 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_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="4dp"
android:layout_weight="1" android:layout_weight="1"
android:contentDescription="@null" android:gravity="center"
android:scaleType="centerInside" android:orientation="vertical">
tools:src="@drawable/elephant_offline" />
<TextView <ImageView
android:id="@+id/messageTextView" android:id="@+id/imageView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="16dp" android:layout_marginTop="4dp"
android:lineSpacingMultiplier="1.1" android:contentDescription="@null"
android:paddingLeft="16dp" android:scaleType="centerInside"
android:paddingTop="16dp" android:src="@drawable/elephant_offline" />
android:paddingRight="16dp"
android:textAlignment="center"
android:textSize="?attr/status_text_medium"
tools:text="@string/error_network" />
<Button <TextView
android:id="@+id/button" android:id="@+id/messageTextView"
style="@style/TuskyButton.Outlined" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:drawablePadding="16dp"
android:layout_marginTop="8dp" android:lineSpacingMultiplier="1.1"
android:layout_marginBottom="4dp" android:paddingLeft="16dp"
android:text="@string/action_retry" /> android:paddingRight="16dp"
android:paddingTop="16dp"
android:text="@string/error_network"
android:textAlignment="center"
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_marginBottom="4dp"
android:layout_marginTop="8dp"
android:text="@string/action_retry" />
</LinearLayout>
</merge> </merge>

9
app/src/main/res/values-de/strings.xml

@ -228,7 +228,7 @@
to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html
* the url can be changed to link to the localized version of the license. * the url can be changed to link to the localized version of the license.
--> -->
<string name="about_project_site">Website des Projekts: <string name="about_project_site">Website des Projekts:
\n https://tusky.app</string> \n https://tusky.app</string>
<string name="about_bug_feature_request_site"> Fehlermeldungen &amp; Verbesserungsvorschläge:\n <string name="about_bug_feature_request_site"> Fehlermeldungen &amp; Verbesserungsvorschläge:\n
https://github.com/tuskyapp/Tusky/issues https://github.com/tuskyapp/Tusky/issues
@ -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>
</resources> <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>

5
app/src/main/res/values/strings.xml

@ -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>

Loading…
Cancel
Save