From dc4fd8f1d0f057db54bcd299c7bfac8d459905af Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 10 May 2024 12:21:37 +0200 Subject: [PATCH 1/6] add default value to Translation.mediaAttachments to fix translation on some servers (#4422) see https://github.com/tuskyapp/Tusky/pull/4307#issuecomment-2093827027 --- app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt index 6728b232e..a9b79494c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt @@ -21,7 +21,7 @@ data class Translation( val spoilerText: String? = null, val poll: TranslatedPoll? = null, @Json(name = "media_attachments") - val mediaAttachments: List, + val mediaAttachments: List = emptyList(), @Json(name = "detected_source_language") val detectedSourceLanguage: String, val provider: String, From ba575dbfc69f8eaf44bb4837aa4038a638aa3bc4 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 10 May 2024 12:21:48 +0200 Subject: [PATCH 2/6] make Status.filtered nullable to make some weird api implementations work again (#4426) closes https://github.com/tuskyapp/Tusky/issues/4424 --- app/src/main/java/com/keylesspalace/tusky/entity/Status.kt | 5 +++-- .../main/java/com/keylesspalace/tusky/network/FilterModel.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 1176752d4..db72bf44f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -56,8 +56,9 @@ data class Status( val card: Card? = null, /** ISO 639 language code for this status. */ val language: String? = null, - /** If the current token has an authorized user: The filter and keywords that matched this status. */ - val filtered: List = emptyList() + /** If the current token has an authorized user: The filter and keywords that matched this status. + * Iceshrimp and maybe other implementations explicitly send filtered=null so we can't default to empty list. */ + val filtered: List? = null ) { val actionableId: String diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt index d70e4fa3f..3763fa548 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -47,7 +47,7 @@ class FilterModel @Inject constructor() { } } - val matchingKind = status.filtered.filter { result -> + val matchingKind = status.filtered.orEmpty().filter { result -> result.filter.kinds.contains(kind) } From f483cf7f29e56101af77e5af468054a238a639c8 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 10 May 2024 12:22:07 +0200 Subject: [PATCH 3/6] don't load custom emojis in their full size (#4429) This should save quite some memory, but most importantly it gets rid of this crash: ``` java.lang.RuntimeException: Canvas: trying to draw too large(121969936bytes) bitmap. at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:266) at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:94) at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:549) at com.keylesspalace.tusky.util.EmojiSpan.draw(CustomEmojiHelper.kt:131) ... ``` --- .../tusky/adapter/FollowRequestViewHolder.kt | 8 +++---- .../adapter/ReportNotificationViewHolder.kt | 13 +++-------- .../announcements/AnnouncementAdapter.kt | 3 +-- .../tusky/util/CustomEmojiHelper.kt | 22 +++++++++++++++---- .../keylesspalace/tusky/util/LinkHelper.kt | 3 +-- app/src/main/res/values/dimens.xml | 2 ++ 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index d4953514e..902d36bcb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -16,7 +16,7 @@ package com.keylesspalace.tusky.adapter import android.graphics.Typeface -import android.text.SpannableStringBuilder +import android.text.SpannableString import android.text.Spanned import android.text.style.StyleSpan import androidx.recyclerview.widget.RecyclerView @@ -49,7 +49,7 @@ class FollowRequestViewHolder( val wrappedName = account.name.unicodeWrap() val emojifiedName: CharSequence = wrappedName.emojify( account.emojis, - itemView, + binding.displayNameTextView, animateEmojis ) binding.displayNameTextView.text = emojifiedName @@ -58,9 +58,9 @@ class FollowRequestViewHolder( R.string.notification_follow_request_format, wrappedName ) - binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply { + binding.notificationTextView.text = SpannableString(wholeMessage).apply { setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - }.emojify(account.emojis, itemView, animateEmojis) + }.emojify(account.emojis, binding.notificationTextView, animateEmojis) } binding.notificationTextView.visible(showHeader) val formattedUsername = itemView.context.getString( diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt index d4a20821f..b894c372d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt @@ -39,16 +39,9 @@ class ReportNotificationViewHolder( animateAvatar: Boolean, animateEmojis: Boolean ) { - val reporterName = reporter.name.unicodeWrap().emojify( - reporter.emojis, - itemView, - animateEmojis - ) - val reporteeName = report.targetAccount.name.unicodeWrap().emojify( - report.targetAccount.emojis, - itemView, - animateEmojis - ) + val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, binding.notificationTopText, animateEmojis) + val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, binding.notificationTopText, animateEmojis) + val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp) binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt index 4e835dbcb..1ce538927 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -37,7 +37,6 @@ import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.visible -import java.lang.ref.WeakReference interface AnnouncementActionListener : LinkListener { fun openReactionPicker(announcementId: String, target: View) @@ -111,7 +110,7 @@ class AnnouncementAdapter( // we set the EmojiSpan on a space, because otherwise the Chip won't have the right size // https://github.com/tuskyapp/Tusky/issues/2308 val spanBuilder = SpannableStringBuilder(" ${reaction.count}") - val span = EmojiSpan(WeakReference(this)) + val span = EmojiSpan(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { span.contentDescription = reaction.name } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index 2c462caa8..0ef40b312 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -24,10 +24,12 @@ import android.graphics.drawable.Drawable import android.text.SpannableStringBuilder import android.text.style.ReplacementSpan import android.view.View +import android.widget.TextView import com.bumptech.glide.Glide import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition +import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Emoji import java.lang.ref.WeakReference import java.util.regex.Pattern @@ -51,7 +53,7 @@ fun CharSequence.emojify(emojis: List, view: View, animate: Boolean): Cha .matcher(this) while (matcher.find()) { - val span = EmojiSpan(WeakReference(view)) + val span = EmojiSpan(view) builder.setSpan(span, matcher.start(), matcher.end(), 0) Glide.with(view) @@ -69,7 +71,19 @@ fun CharSequence.emojify(emojis: List, view: View, animate: Boolean): Cha return builder } -class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() { +class EmojiSpan(view: View) : ReplacementSpan() { + + private val viewWeakReference = WeakReference(view) + + private val emojiSize: Int = if (view is TextView) { + view.paint.textSize + } else { + // sometimes it is not possible to determine the TextView the emoji will be shown in, + // e.g. because it is passed to a library, so we fallback to a size that should be large + // enough in most cases + view.context.resources.getDimension(R.dimen.fallback_emoji_size) + }.times(1.2).toInt() + var imageDrawable: Drawable? = null override fun getSize( @@ -89,7 +103,7 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() fm.bottom = metrics.bottom } - return (paint.textSize * 1.2).toInt() + return emojiSize } override fun draw( @@ -134,7 +148,7 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() } fun getTarget(animate: Boolean): Target { - return object : CustomTarget() { + return object : CustomTarget(emojiSize, emojiSize) { override fun onResourceReady(resource: Drawable, transition: Transition?) { viewWeakReference.get()?.let { view -> if (animate && resource is Animatable) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt index 7770d54dc..50d3ccfa7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt @@ -48,7 +48,6 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.interfaces.LinkListener -import java.lang.ref.WeakReference import java.net.URI import java.net.URISyntaxException @@ -128,7 +127,7 @@ fun markupHiddenUrls(view: TextView, content: CharSequence): SpannableStringBuil val linkDrawable = AppCompatResources.getDrawable(view.context, R.drawable.ic_link)!! // ImageSpan does not always align the icon correctly in the line, let's use our custom emoji span for this - val linkDrawableSpan = EmojiSpan(WeakReference(view)) + val linkDrawableSpan = EmojiSpan(view) linkDrawableSpan.imageDrawable = linkDrawable val placeholderIndex = replacementText.indexOf("🔗") diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index c6f26d481..7136914bf 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -75,4 +75,6 @@ 64dp + 16sp + From 5137bbfade4b67a2273c28949f25fe109ce87d4f Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 10 May 2024 12:26:03 +0200 Subject: [PATCH 4/6] fix another crash in ShareShortcutHelper (#4431) Glide sometimes calls the callback more than once (for the placeholder, then for the actual image), but a coroutine can only resume once. ``` Exception java.lang.IllegalStateException: at kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError (CancellableContinuationImpl.kt:555) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl (CancellableContinuationImpl.kt:520) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default (CancellableContinuationImpl.kt:493) at kotlinx.coroutines.CancellableContinuationImpl.resumeWith (CancellableContinuationImpl.kt:364) at com.keylesspalace.tusky.util.GlideExtensionsKt$submitAsync$2$target$1.onResourceReady (GlideExtensions.kt:39) at com.bumptech.glide.request.SingleRequest.onResourceReady (SingleRequest.java:650) at com.bumptech.glide.request.SingleRequest.onResourceReady (SingleRequest.java:596) at com.bumptech.glide.request.SingleRequest.begin (SingleRequest.java:243) at com.bumptech.glide.manager.RequestTracker.resumeRequests (RequestTracker.java:115) at com.bumptech.glide.RequestManager.resumeRequests (RequestManager.java:339) at com.bumptech.glide.RequestManager.onStart (RequestManager.java:364) at com.bumptech.glide.manager.ApplicationLifecycle.addListener (ApplicationLifecycle.java:15) at com.bumptech.glide.RequestManager$1.run (RequestManager.java:84) at android.os.Handler.handleCallback (Handler.java:958) at android.os.Handler.dispatchMessage (Handler.java:99) at android.os.Looper.loopOnce (Looper.java:230) at android.os.Looper.loop (Looper.java:319) at android.app.ActivityThread.main (ActivityThread.java:8893) at java.lang.reflect.Method.invoke at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:608) at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103) ``` While removing the placeholder fixes the problem here, we should probably put some safeguards into `submitAsync` so that this can't happen again elsewhere. Any ideas how to do that, @cbeyls? --- .../java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt index 1715e2ba5..504cb47f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt @@ -59,8 +59,6 @@ class ShareShortcutHelper @Inject constructor( Glide.with(context) .asBitmap() .load(account.profilePictureUrl) - .placeholder(R.drawable.avatar_default) - .error(R.drawable.avatar_default) .submitAsync(innerSize, innerSize) } catch (e: GlideException) { // https://github.com/bumptech/glide/issues/4672 :/ From f2d7de0144193faec03f4dcd5e1da0b6665fe9fa Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 10 May 2024 12:33:55 +0200 Subject: [PATCH 5/6] fix crash in AccountListFragment (#4430) Steps to reproduce: When viewing an account list, very quickly rotate the screen multiple times. I found this easier to do with an emulator, just spam the rotation buttons. ``` java.lang.IllegalStateException: Fragment AccountListFragment{50bbc6c} (8a3e6922-e855-45a7-8a49-c9d16e21e438) did not return a View from onCreateView() or this was called before onCreateView(). at androidx.fragment.app.Fragment.requireView(Fragment.java:2063) at com.keylesspalace.tusky.util.ViewLifecycleLazy.getValue(ViewBindingExtensions.kt:32) at com.keylesspalace.tusky.components.accountlist.AccountListFragment.getBinding(AccountListFragment.kt:75) at com.keylesspalace.tusky.components.accountlist.AccountListFragment.onFetchAccountsFailure(AccountListFragment.kt:390) at com.keylesspalace.tusky.components.accountlist.AccountListFragment.access$onFetchAccountsFailure(AccountListFragment.kt:63) at com.keylesspalace.tusky.components.accountlist.AccountListFragment$fetchAccounts$2.invokeSuspend(AccountListFragment.kt:333) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:230) at android.os.Handler.handleCallback(Handler.java:942) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:201) at android.os.Looper.loop(Looper.java:288) at android.app.ActivityThread.main(ActivityThread.java:7872) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@260dcb1, Dispatchers.Main.immediate] ``` --- .../tusky/components/accountlist/AccountListFragment.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt index 729933114..95a611090 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt @@ -56,6 +56,7 @@ import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener import javax.inject.Inject +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import retrofit2.Response @@ -329,6 +330,12 @@ class AccountListFragment : val linkHeader = response.headers()["Link"] onFetchAccountsSuccess(accountList, linkHeader) } catch (exception: Exception) { + if (exception is CancellationException) { + // Scope is cancelled, probably because the fragment is destroyed. + // We must not touch any views anymore, so rethrow the exception. + // (CancellationException in a cancelled scope is normal and will be ignored) + throw exception + } onFetchAccountsFailure(exception) } } From bf9be47f0f5a1382ad9cbda1f927929c22a3fcd8 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 10 May 2024 13:31:40 +0200 Subject: [PATCH 6/6] Release 121 --- CHANGELOG.md | 10 ++++++++++ app/build.gradle | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 431397483..ab13d8e7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ ### Significant bug fixes +## v25.2 + +### Significant bug fixes + +- Fixes a bug that could sometimes crash Tusky when rotating the screen while viewing an account list [PR#4430](https://github.com/tuskyapp/Tusky/pull/4430) +- Fixes a bug that could crash Tusky at startup under certain conditions [PR#4431](https://github.com/tuskyapp/Tusky/pull/4431) +- Fixes a bug that caused Tusky to crash when custom emojis with too large dimensions were loaded [PR#4429](https://github.com/tuskyapp/Tusky/pull/4429) +- Makes Tusky work again with Iceshrimp by working around a quirk in their API implementation [PR#4426](https://github.com/tuskyapp/Tusky/pull/4426) +- Fixes a bug that made translations not work on some servers [PR#4422](https://github.com/tuskyapp/Tusky/pull/4422) + ## v25.1 ### Significant bug fixes diff --git a/app/build.gradle b/app/build.gradle index b6c224555..fbe7b8233 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { namespace "com.keylesspalace.tusky" minSdk 24 targetSdk 34 - versionCode 120 - versionName "25.1" + versionCode 121 + versionName "25.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true