diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml
index 3f141f1cf..73132caf4 100644
--- a/app/lint-baseline.xml
+++ b/app/lint-baseline.xml
@@ -716,6 +716,17 @@
column="51"/>
+
+
+
+
@@ -1196,7 +1207,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt b/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt
index 3538d469f..bcced8ac1 100644
--- a/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt
+++ b/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt
@@ -740,7 +740,7 @@ abstract class StatusBaseViewHolder protected constructor(i
)
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.visibility)
setSpoilerAndContent(viewData, statusDisplayOptions, listener)
- setDescriptionForStatus(viewData, statusDisplayOptions)
+ setContentDescriptionForStatus(viewData, statusDisplayOptions)
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
// RecyclerView tries to set AccessibilityDelegateCompat to null
@@ -759,41 +759,86 @@ abstract class StatusBaseViewHolder protected constructor(i
}
}
- private fun setDescriptionForStatus(
- viewData: T,
- statusDisplayOptions: StatusDisplayOptions,
- ) {
+ /** Creates and sets the content description for the status. */
+ private fun setContentDescriptionForStatus(viewData: T, statusDisplayOptions: StatusDisplayOptions) {
val (_, _, account, _, _, _, _, createdAt, editedAt, _, reblogsCount, favouritesCount, _, reblogged, favourited, bookmarked, sensitive, _, visibility) = viewData.actionable
- val description = context.getString(
- R.string.description_status,
- account.displayName,
- getContentWarningDescription(context, viewData),
- if (TextUtils.isEmpty(viewData.spoilerText) || !sensitive || viewData.isExpanded) viewData.content else "",
- getCreatedAtDescription(createdAt, statusDisplayOptions),
- editedAt?.let { context.getString(R.string.description_post_edited) } ?: "",
- getReblogDescription(context, viewData),
- viewData.username,
- if (reblogged) context.getString(R.string.description_post_reblogged) else "",
- if (favourited) context.getString(R.string.description_post_favourited) else "",
- if (bookmarked) context.getString(R.string.description_post_bookmarked) else "",
- getMediaDescription(context, viewData),
- visibility.description(context),
- getFavsText(favouritesCount),
- getReblogsText(reblogsCount),
+
+ // Build the content description using a string builder instead of a string resource
+ // as there are many places where optional ";" and "," are needed.
+ //
+ // The full string will look like (content in parentheses is optional),
+ //
+ // account.name
+ // (; contentWarning)
+ // ; (content)
+ // (, poll)
+ // , relativeDate
+ // (, editedAt)
+ // (, reblogDescription)
+ // , username
+ // (, "Reblogged")
+ // (, "Favourited")
+ // (, "Bookmarked")
+ // (, "n Favorite")
+ // (, "n Boost")
+ val description = StringBuilder().apply {
+ append(account.name)
+
+ getContentWarningDescription(context, viewData)?.let { append("; ", it) }
+
+ append("; ")
+
+ // Content is optional, and hidden if there are spoilers or the status is
+ // marked sensitive, and it has not been expanded.
+ if (TextUtils.isEmpty(viewData.spoilerText) || !sensitive || viewData.isExpanded) {
+ append(viewData.content, ", ")
+ }
+
viewData.actionable.poll?.let {
- pollView.getPollDescription(
- from(it),
- statusDisplayOptions,
- numberFormat,
- absoluteTimeFormatter,
+ append(
+ pollView.getPollDescription(
+ from(it),
+ statusDisplayOptions,
+ numberFormat,
+ absoluteTimeFormatter,
+ ),
+ ", ",
)
- } ?: "",
- )
+ }
+
+ append(getCreatedAtDescription(createdAt, statusDisplayOptions))
+
+ editedAt?.let { append(", ", context.getString(R.string.description_post_edited)) }
+
+ getReblogDescription(context, viewData)?.let { append(", ", it) }
+
+ append(", ", viewData.username)
+
+ if (reblogged) {
+ append(", ", context.getString(R.string.description_post_reblogged))
+ }
+
+ if (favourited) {
+ append(", ", context.getString(R.string.description_post_favourited))
+ }
+ if (bookmarked) {
+ append(", ", context.getString(R.string.description_post_bookmarked))
+ }
+ getMediaDescription(context, viewData)?.let { append(", ", it) }
+
+ append("; ")
+
+ visibility.description(context)?.let { append(it) }
+ getFavouritesCountDescription(favouritesCount)?.let { append(", ", it) }
+ getReblogsCountDescription(reblogsCount)?.let { append(", ", it) }
+ }
+
itemView.contentDescription = description
}
- protected fun getFavsText(count: Int): CharSequence {
- if (count <= 0) return ""
+ /** @return "n Favourite", for use in a content description. */
+ protected fun getFavouritesCountDescription(count: Int): CharSequence? {
+ if (count <= 0) return null
val countString = numberFormat.format(count.toLong())
return HtmlCompat.fromHtml(
@@ -802,8 +847,9 @@ abstract class StatusBaseViewHolder protected constructor(i
)
}
- protected fun getReblogsText(count: Int): CharSequence {
- if (count <= 0) return ""
+ /** @return "n Boost", for use in a content description. */
+ protected fun getReblogsCountDescription(count: Int): CharSequence? {
+ if (count <= 0) return null
val countString = numberFormat.format(count.toLong())
return HtmlCompat.fromHtml(
@@ -826,7 +872,10 @@ abstract class StatusBaseViewHolder protected constructor(i
cardView ?: return
val (_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, sensitive, _, _, attachments, _, _, _, _, _, poll, card) = viewData.actionable
- if (cardViewMode !== CardViewMode.NONE && attachments.isEmpty() && poll == null && card != null &&
+ if (cardViewMode !== CardViewMode.NONE &&
+ attachments.isEmpty() &&
+ poll == null &&
+ card != null &&
!TextUtils.isEmpty(card.url) &&
(!sensitive || expanded) &&
(!viewData.isCollapsible || !viewData.isCollapsed)
@@ -883,34 +932,32 @@ abstract class StatusBaseViewHolder protected constructor(i
return attachments.all { it.isPreviewable() }
}
- private fun getReblogDescription(context: Context, status: IStatusViewData): CharSequence {
+ /** @return "{account.username} boosted", for use in a content description. */
+ private fun getReblogDescription(context: Context, status: IStatusViewData): String? {
return status.rebloggingStatus?.let {
context.getString(R.string.post_boosted_format, it.account.username)
- } ?: ""
+ }
}
- private fun getMediaDescription(context: Context, status: IStatusViewData): CharSequence {
- if (status.actionable.attachments.isEmpty()) return ""
+ /** @return "Media: {0-n descriptions}", for use in a content description. */
+ private fun getMediaDescription(context: Context, status: IStatusViewData): String? {
+ if (status.actionable.attachments.isEmpty()) return null
- val mediaDescriptions =
- status.actionable.attachments.fold(StringBuilder()) { builder: StringBuilder, (_, _, _, _, _, description): Attachment ->
- if (description == null) {
- val placeholder =
- context.getString(R.string.description_post_media_no_description_placeholder)
- return@fold builder.append(placeholder)
- } else {
- builder.append("; ")
- return@fold builder.append(description)
- }
- }
- return context.getString(R.string.description_post_media, mediaDescriptions)
+ val missingDescription = context.getString(R.string.description_post_media_no_description_placeholder)
+
+ val mediaDescriptions = status.actionable.attachments.map {
+ it.description ?: missingDescription
+ }
+
+ return context.getString(R.string.description_post_media, mediaDescriptions.joinToString(", "))
}
- private fun getContentWarningDescription(context: Context, status: IStatusViewData): CharSequence {
+ /** @return "Content warning: {spoilerText}", for use in a content description. */
+ private fun getContentWarningDescription(context: Context, status: IStatusViewData): String? {
return if (!TextUtils.isEmpty(status.spoilerText)) {
context.getString(R.string.description_post_cw, status.spoilerText)
} else {
- ""
+ null
}
}
}
diff --git a/app/src/main/java/app/pachli/adapter/StatusDetailedViewHolder.kt b/app/src/main/java/app/pachli/adapter/StatusDetailedViewHolder.kt
index 12b502f5c..b1e6cf7ad 100644
--- a/app/src/main/java/app/pachli/adapter/StatusDetailedViewHolder.kt
+++ b/app/src/main/java/app/pachli/adapter/StatusDetailedViewHolder.kt
@@ -38,7 +38,7 @@ class StatusDetailedViewHolder(
visibilityIcon?.also {
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) DynamicDrawableSpan.ALIGN_CENTER else DynamicDrawableSpan.ALIGN_BASELINE
val visibilityIconSpan = ImageSpan(it, alignment)
- sb.setSpan(visibilityIconSpan, 0, visibilityString.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ sb.setSpan(visibilityIconSpan, 0, visibilityString?.length ?: 0, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
val metadataJoiner = context.getString(R.string.metadata_joiner)
sb.append(" ")
@@ -82,13 +82,13 @@ class StatusDetailedViewHolder(
listener: StatusActionListener,
) {
if (reblogCount > 0) {
- binding.statusReblogs.text = getReblogsText(reblogCount)
+ binding.statusReblogs.text = getReblogsCountDescription(reblogCount)
binding.statusReblogs.show()
} else {
binding.statusReblogs.hide()
}
if (favCount > 0) {
- binding.statusFavourites.text = getFavsText(favCount)
+ binding.statusFavourites.text = getFavouritesCountDescription(favCount)
binding.statusFavourites.show()
} else {
binding.statusFavourites.hide()
diff --git a/app/src/main/java/app/pachli/util/StatusExtensions.kt b/app/src/main/java/app/pachli/util/StatusExtensions.kt
index b8a5e49c6..f5fb563b5 100644
--- a/app/src/main/java/app/pachli/util/StatusExtensions.kt
+++ b/app/src/main/java/app/pachli/util/StatusExtensions.kt
@@ -30,17 +30,17 @@ import app.pachli.core.network.model.Status
// store resources (yet).
/**
- * @return A description for this visibility, or "" if it's null or [Status.Visibility.UNKNOWN].
+ * @return A description for this visibility, or null if it's null or [Status.Visibility.UNKNOWN].
*/
-fun Status.Visibility?.description(context: Context): CharSequence {
- this ?: return ""
+fun Status.Visibility?.description(context: Context): CharSequence? {
+ this ?: return null
val resource: Int = when (this) {
Status.Visibility.PUBLIC -> R.string.description_visibility_public
Status.Visibility.UNLISTED -> R.string.description_visibility_unlisted
Status.Visibility.PRIVATE -> R.string.description_visibility_private
Status.Visibility.DIRECT -> R.string.description_visibility_direct
- Status.Visibility.UNKNOWN -> return ""
+ Status.Visibility.UNKNOWN -> return null
}
return context.getString(resource)
}
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index 53b424675..833ab2cf1 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -192,11 +192,6 @@
- never
-
-
- %1$s; %2$s; %3$s, %15$s %4$s, %5$s, %6$s; %7$s, %8$s, %9$s, %10$s, %11$s; %12$s, %13$s, %14$s
-
-
- @string/duration_5_min
- @string/duration_30_min
diff --git a/core/network/src/main/kotlin/app/pachli/core/network/model/TimelineAccount.kt b/core/network/src/main/kotlin/app/pachli/core/network/model/TimelineAccount.kt
index d0c571752..ec03ce19f 100644
--- a/core/network/src/main/kotlin/app/pachli/core/network/model/TimelineAccount.kt
+++ b/core/network/src/main/kotlin/app/pachli/core/network/model/TimelineAccount.kt
@@ -40,6 +40,10 @@ data class TimelineAccount(
val emojis: List? = emptyList(),
) {
+ /**
+ * The account's [displayName], falling back to [localUsername] if
+ * [displayName] is null or empty.
+ */
@Suppress("DEPRECATION")
val name: String
get() = if (displayName.isNullOrEmpty()) {