Turns out that the crash I thought will not occur with #4153 did occur
again. 😒
But this time I managed to reproduce it (faking a slow network for that
one request only and then quickly switching activities to get it
destroyed) so I'm sure it is fixed, and I did a check for possible side
effects but it seems Glide is clever enough to cancel all requests by
itself.
<details>
<summary>stacktrace</summary>
```
Exception java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity
at com.bumptech.glide.manager.RequestManagerRetriever.assertNotDestroyed (RequestManagerRetriever.java:236)
at com.bumptech.glide.manager.RequestManagerRetriever.get (RequestManagerRetriever.java:110)
at com.bumptech.glide.manager.RequestManagerRetriever.get (RequestManagerRetriever.java:176)
at com.bumptech.glide.Glide.with (Glide.java:634)
at com.keylesspalace.tusky.MainActivity$setupDrawer$2.cancel (MainActivity.kt:573)
at com.mikepenz.materialdrawer.util.DrawerImageLoader.cancelImage (DrawerImageLoader.java:56)
at com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem.unbindView (BaseDescribeableDrawerItem.kt:95)
at com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem.unbindView (BaseDescribeableDrawerItem.kt:20)
at com.mikepenz.fastadapter.listeners.OnBindViewHolderListenerImpl.unBindViewHolder (OnBindViewHolderListenerImpl.java:36)
at com.mikepenz.fastadapter.FastAdapter.onViewRecycled (FastAdapter.kt:410)
at androidx.recyclerview.widget.RecyclerView$Recycler.dispatchViewRecycled (RecyclerView.java:7346)
at androidx.recyclerview.widget.RecyclerView$Recycler.addViewHolderToRecycledViewPool (RecyclerView.java:7108)
at androidx.recyclerview.widget.RecyclerView$Recycler.recycleViewHolderInternal (RecyclerView.java:7059)
at androidx.recyclerview.widget.RecyclerView.removeAnimatingView (RecyclerView.java:1556)
at androidx.recyclerview.widget.RecyclerView$ItemAnimatorRestoreListener.onAnimationFinished (RecyclerView.java:13564)
at androidx.recyclerview.widget.RecyclerView$ItemAnimator.dispatchAnimationFinished (RecyclerView.java:14066)
at androidx.recyclerview.widget.SimpleItemAnimator.dispatchChangeFinished (SimpleItemAnimator.java:328)
at androidx.recyclerview.widget.DefaultItemAnimator.endChangeAnimationIfNecessary (DefaultItemAnimator.java:437)
at androidx.recyclerview.widget.DefaultItemAnimator.endChangeAnimationIfNecessary (DefaultItemAnimator.java:418)
at androidx.recyclerview.widget.DefaultItemAnimator.endAnimations (DefaultItemAnimator.java:588)
at androidx.recyclerview.widget.RecyclerView.onDetachedFromWindow (RecyclerView.java:3383)
at android.view.View.dispatchDetachedFromWindow (View.java:20898)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3956)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
at android.view.ViewRootImpl.dispatchDetachedFromWindow (ViewRootImpl.java:5114)
at android.view.ViewRootImpl.doDie (ViewRootImpl.java:8309)
at android.view.ViewRootImpl.die (ViewRootImpl.java:8286)
at android.view.WindowManagerGlobal.removeViewLocked (WindowManagerGlobal.java:538)
at android.view.WindowManagerGlobal.removeView (WindowManagerGlobal.java:479)
at android.view.WindowManagerImpl.removeViewImmediate (WindowManagerImpl.java:162)
at android.app.ActivityThread.handleDestroyActivity (ActivityThread.java:5564)
at android.app.ActivityThread.handleRelaunchActivityInner (ActivityThread.java:5842)
at android.app.ActivityThread.handleRelaunchActivity (ActivityThread.java:5758)
at android.app.servertransaction.ActivityRelaunchItem.execute (ActivityRelaunchItem.java:71)
at android.app.servertransaction.ActivityTransactionItem.execute (ActivityTransactionItem.java:45)
at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2293)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loopOnce (Looper.java:226)
at android.os.Looper.loop (Looper.java:329)
at android.app.ActivityThread.main (ActivityThread.java:8058)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1026)
```
</details>
<details>
<summary>Stacktrace</summary>
```
java.lang.ClassCastException: java.lang.String cannot be cast to
android.text.Spannable
at
com.keylesspalace.tusky.view.ClickableSpanTextView.dispatchTouchEvent(ClickableSpanTextView.kt:208)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:448)
at
com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1829)
at android.app.Activity.dispatchTouchEvent(Activity.java:3307)
at
androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:70)
at
com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:410)
at android.view.View.dispatchPointerEvent(View.java:12015)
at
android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4795)
at
android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4609)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at
android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4200)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4166)
at
android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4293)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4174)
at
android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4350)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at
android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4200)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4166)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4174)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6661)
```
</details>
Could not reproduce with a regular build because I couldn't find the
place where we set a string into a ClickableSpanTextView, so I created
that scenario manually. It crashed instantly when trying to select text,
and with this fix it behaved as expected.
Saw an ANR (app not responding) error being reported in the Play console
and then found this. Sorry but `runBlocking` in production code is an
absolute no go.
Currently translated at 100.0% (634 of 634 strings)
Translated using Weblate (Russian)
Currently translated at 100.0% (634 of 634 strings)
Co-authored-by: Vladyslav Stepanov <mittwerk@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/
Translation: Tusky/Tusky
```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.keylesspalace.tusky.test/com.keylesspalace.tusky.ViewMediaActivity}: java.lang.NullPointerException
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2778)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
Caused by: java.lang.NullPointerException
at com.keylesspalace.tusky.ViewMediaActivity.adjustScreenWakefulness(ViewMediaActivity.kt:351)
at com.keylesspalace.tusky.ViewMediaActivity.onCreate(ViewMediaActivity.kt:161)
at android.app.Activity.performCreate(Activity.java:7009)
at android.app.Activity.performCreate(Activity.java:7000)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
```
For reasons not totally clear to me, Github marked #4160 as "merged" and
will not let me reopen it.
I believe this should be included for 24.1 because it fixes a 24.0
regression in the media player.
I have tested this newest commit in a number of ways, and in my testing
I find (1) when viewing an image, it sleeps after about a minute (2)
when viewing video, it stays awake indefinitely (3) this is true whether
the image/video was opened directly, or reached by swiping from another
attachment. I have not tested swiping to/from audio but I am confident
it will work the same.
I hate these workarounds for Android bugs, I'm always afraid the will
introduce other problems. I tested this on multiple Android versions, it
definitely fixes the problem and otherwise seems fine though.
closes#4164
Currently translated at 100.0% (634 of 634 strings)
Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
Another fix for a memory leak. This one is not as big as #4150, but
still worth fixing for memory constrained devices imo.
The `DrawerImageLoader` implementation (a global singleton) references a
member of the `MainActivity`, causing the whole activity to leak.
This weird construct was introduced in #1989 to fix a crash, but I think
since we migrated to coroutines it is no longer necessary because all
calls get correctly cancelled. I tried reproducing the crash but could
not, so I'm pretty sure it is fine. I would appreciate it if someone
else could try it as well though.
(The crash could be reproduced on slow internet, when
`onFetchUserInfoSuccess` was called while the activity was being
destroyed, causing Glide to crash the app because it can't use destroyed
activities. `onFetchUserInfoSuccess` is now no longer called in this
case because it is inside a `lifecycleScope.launch` block.)
Found with Leak canary: The transformation ends up in Glide's memory
cache and leaks whole Activities through the view -> context reference.
This fixes the problem by removing the background detection logic (so
the view reference is no longer needed) and setting the background
directly instead. Looks exactly as before.
Currently translated at 100.0% (634 of 634 strings)
Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
Currently translated at 100.0% (634 of 634 strings)
Translated using Weblate (Icelandic)
Currently translated at 100.0% (634 of 634 strings)
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
This changes one word in the string `pref_failed_to_sync` in the file
`values/strings.xml`.
See my reasoning here #4133
"Failed to sync settings" changes to "Failed to sync preferences".
Steps to reproduce: Open the dialog to set a catption on an image.
Rotate the screen.
<details>
<summary>Stacktrace</summary>
```
Exception java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
at com.keylesspalace.tusky.components.compose.dialog.CaptionDialog.onCreateView (CaptionDialog.kt:61)
at androidx.fragment.app.Fragment.performCreateView (Fragment.java:3114)
at androidx.fragment.app.DialogFragment.performCreateView (DialogFragment.java:775)
at androidx.fragment.app.FragmentStateManager.createView (FragmentStateManager.java:557)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState (FragmentStateManager.java:272)
at androidx.fragment.app.FragmentStore.moveToExpectedState (FragmentStore.java:114)
at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:1455)
at androidx.fragment.app.FragmentManager.dispatchStateChange (FragmentManager.java:3034)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated (FragmentManager.java:2952)
at androidx.fragment.app.FragmentController.dispatchActivityCreated (FragmentController.java:263)
at androidx.fragment.app.FragmentActivity.onStart (FragmentActivity.java:350)
at androidx.appcompat.app.AppCompatActivity.onStart (AppCompatActivity.java:251)
at android.app.Instrumentation.callActivityOnStart (Instrumentation.java:1543)
at android.app.Activity.performStart (Activity.java:8682)
at android.app.ActivityThread.handleStartActivity (ActivityThread.java:4219)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence (TransactionExecutor.java:221)
at android.app.servertransaction.TransactionExecutor.cycleToPath (TransactionExecutor.java:201)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:173)
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2584)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loopOnce (Looper.java:226)
at android.os.Looper.loop (Looper.java:313)
at android.app.ActivityThread.main (ActivityThread.java:8810)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:604)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
```
</details>
Restoring the saved caption after the view was created fixes the
problem.
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
Currently translated at 100.0% (634 of 634 strings)
Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)
Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
I [posted our new video player layout] on Mastodon for comments and
multiple people said the buttons were too close together. I agree. I
added some space (I eyeballed it, I made it bigger until it felt too big
and then I narrowed it), I think we have now increased the space from
10dp to 25dp. I added the space by wrapping the buttons in
LinearLayouts, because they are <include>s and could theoretically
insert more than one button.
Concerns: If the "next"/"prev" buttons ever become active, the space
will not be correctly applied to those. We can fix that if it ever comes
up (we don't display those buttons). If people think the buttons should
be placed even further apart we can do this by just increasing the
number in styles.xml.
This is what it looks like now. See previous look and comparison with
23.0 in #4071
<img width=400
src="https://files.mastodon.social/media_attachments/files/111/293/547/524/867/101/original/91b83e1717111444.png">
Steps to reproduce:
1. install Gboard
(https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin)
2. open a direct link to any image in Firefox
3. long-press the image to get a "Copy Image" dialogue (and copy the
image)
4. compose a new post in Tusky
5. Gboard will suggest to paste the image from clipboard
6. paste image, see that when opening alt text editor, it is filled out
with this garbage string
Why is this bad? It's not when I just fix the alt text. But it breaks
every mechanism that is supposed to remind me of adding alt text.
It's hard to argue that this is within scope of Tusky but I also don't
see it getting fixed in Gboard, so here we go.
Fixes: #4063
Switching from an AlertDialog to only a DialogFragment.
I didn't get the AlertDialog to be sized correctly.
It also opens now directly with the right (full screen) size. When the
imageView fails to load (i.e. with an audio file) it will be hidden.
This changes the button layout somewhat.
One observation: The placeholder text "... visually impaired..." is not
quite right as a description for an audio file is not intended for the
visually impaired. But I couldn't think of a better text just yet.
![grafik](https://github.com/tuskyapp/Tusky/assets/1618905/fd49d5bd-b86c-4659-abb9-f1776cbb2a55)
Fixes#2512
![grafik](https://github.com/tuskyapp/Tusky/assets/1618905/f8199d10-e26a-4f14-93c3-95cb890ea663)
Can add an arbitrary number of tabs.
Graphical behavior is unchanged for small numbers: the whole space if
filled with the tabs - they are enlarged if needed.
If there are more the mode switches to "scrollable".
This does not, however, look very differently (see screenshot with the
current tab scrolled out).
---------
Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
## Issue
Close#3967
# What I did
- Displayed the date of each announcement.
- Date is placed in the lower left corner of the Announcement
- Supported date format internationalization using
getBestDateTimePattern
# Screenshot
<image
src="https://github.com/tuskyapp/Tusky/assets/62137820/7c124183-1a13-4cae-8667-ff82ca99b60c"
width="500"/>
## Note
I am not good at English so I use machine translation a bit. So, you may
find my writing style a little strange...