Compare commits
242 Commits
v1.1.1
...
v1.1.3+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aeda56fc8 | ||
|
|
f531a90b41 | ||
|
|
ff52c37868 | ||
|
|
8fb2b454dd | ||
|
|
265b2ad32c | ||
|
|
ba3219d9fc | ||
|
|
b44e3424e3 | ||
|
|
f073eba538 | ||
|
|
7f78431eff | ||
|
|
24c1ac042c | ||
|
|
105fe68438 | ||
|
|
46057af093 | ||
|
|
750fa4c112 | ||
|
|
8b40643e63 | ||
|
|
5968dcd05b | ||
|
|
0c382bdbf6 | ||
|
|
74f03026cf | ||
|
|
8ad6bd52ef | ||
|
|
04da21edf3 | ||
|
|
ee709b6db7 | ||
|
|
b7432fe422 | ||
|
|
8a0991d533 | ||
|
|
6fffe778d3 | ||
|
|
b67b61dfe4 | ||
|
|
65d86d9238 | ||
|
|
c1e67c4f73 | ||
|
|
e0e48f87eb | ||
|
|
b2db64022f | ||
|
|
8336bfdf5c | ||
|
|
0ec14fe8fa | ||
|
|
01a2f1d95c | ||
|
|
b38bf5e431 | ||
|
|
67b3e85837 | ||
|
|
310fb7db42 | ||
|
|
9f4d330ab1 | ||
|
|
25092fbcfb | ||
|
|
705e98729d | ||
|
|
2f24977996 | ||
|
|
6c336ba89e | ||
|
|
108d16a157 | ||
|
|
e55ca6cc05 | ||
|
|
b8be1f184d | ||
|
|
aa96ec54a3 | ||
|
|
e8b43c7179 | ||
|
|
b51b4a10ee | ||
|
|
f2b9ede27c | ||
|
|
a8c7d891f1 | ||
|
|
195c4d7b6d | ||
|
|
d280dc31e8 | ||
|
|
eb0925c524 | ||
|
|
968de3664d | ||
|
|
12f7336392 | ||
|
|
3a4d13b1c6 | ||
|
|
273c841d9a | ||
|
|
d1b1cb2082 | ||
|
|
5bbe99be51 | ||
|
|
556d1e7433 | ||
|
|
293d7032ce | ||
|
|
0186b7f8da | ||
|
|
d33654c793 | ||
|
|
86d2312615 | ||
|
|
d1083c331b | ||
|
|
ed7242217a | ||
|
|
8fddaa8c82 | ||
|
|
00affe6e3e | ||
|
|
f21b647ee0 | ||
|
|
2a628a3791 | ||
|
|
ecd568503d | ||
|
|
f9d0632a85 | ||
|
|
11905513b7 | ||
|
|
9c89abf1c4 | ||
|
|
4d950e43ac | ||
|
|
99405f307d | ||
|
|
f1bfe05263 | ||
|
|
0f223159c0 | ||
|
|
ad9518e87c | ||
|
|
1c16cfb09e | ||
|
|
d4a4b10017 | ||
|
|
74ae5bd04e | ||
|
|
9638cf079f | ||
|
|
a6d161c1b4 | ||
|
|
1136e40eb4 | ||
|
|
98de3a2984 | ||
|
|
48e7071450 | ||
|
|
bcc8d55c7b | ||
|
|
8d477efc28 | ||
|
|
4ae21862a5 | ||
|
|
6e6aebef35 | ||
|
|
cd138032da | ||
|
|
864e6fb9d0 | ||
|
|
9d356b0635 | ||
|
|
6c5d720a40 | ||
|
|
d733d76ccf | ||
|
|
3483d8c3c0 | ||
|
|
0f326c1362 | ||
|
|
c6eda38400 | ||
|
|
3c59c8cc0f | ||
|
|
8f1b9ec092 | ||
|
|
5a42136395 | ||
|
|
e9b347d130 | ||
|
|
d86e203127 | ||
|
|
c80417e671 | ||
|
|
55a55fbb1c | ||
|
|
b42236999b | ||
|
|
8ca8bd765b | ||
|
|
7f4cf77283 | ||
|
|
950d413bd1 | ||
|
|
475827b1c1 | ||
|
|
1c9164e559 | ||
|
|
7311a394d8 | ||
|
|
aa09bc7ab2 | ||
|
|
267a6a75ef | ||
|
|
37660b4c73 | ||
|
|
15b4d46ea1 | ||
|
|
0b99e76b25 | ||
|
|
c851f666b3 | ||
|
|
7662c81754 | ||
|
|
5c1b583448 | ||
|
|
4071c1552d | ||
|
|
89856a81a3 | ||
|
|
005ddfb651 | ||
|
|
92d10d59c6 | ||
|
|
150f70edd8 | ||
|
|
73019eaade | ||
|
|
46bac59ff5 | ||
|
|
9b162cb63b | ||
|
|
7d216314c9 | ||
|
|
080a320e12 | ||
|
|
28787b4068 | ||
|
|
a73ea62a9c | ||
|
|
69b399e397 | ||
|
|
fc2c033e93 | ||
|
|
1d81abca5b | ||
|
|
0f3cd5d8d0 | ||
|
|
f0476f3187 | ||
|
|
b4677d14e5 | ||
|
|
a8837bd4f8 | ||
|
|
c6991a7067 | ||
|
|
0723e942f0 | ||
|
|
b08415ca8f | ||
|
|
3639c69d36 | ||
|
|
52fd300d1e | ||
|
|
37cefcaf6d | ||
|
|
68c9f7a861 | ||
|
|
8eb0b12a09 | ||
|
|
558adc6936 | ||
|
|
68ecd7bc28 | ||
|
|
5c7d4e389f | ||
|
|
31e3a8592f | ||
|
|
39655d5278 | ||
|
|
55fd74c227 | ||
|
|
b65b7c53bc | ||
|
|
68d0862008 | ||
|
|
c9e13eefa5 | ||
|
|
349fbce5af | ||
|
|
7a23c9b348 | ||
|
|
95c66654aa | ||
|
|
a8407571a4 | ||
|
|
75538deb9b | ||
|
|
601eec4607 | ||
|
|
9b87d0bece | ||
|
|
cb25632691 | ||
|
|
63957250c5 | ||
|
|
d844a77e65 | ||
|
|
32cc760272 | ||
|
|
e105764aa8 | ||
|
|
5a9a352e56 | ||
|
|
7deb2d452e | ||
|
|
c3d2df88e8 | ||
|
|
9943d19c31 | ||
|
|
51c1e115c5 | ||
|
|
f83ff93c68 | ||
|
|
7deb5d44c2 | ||
|
|
3140ae8046 | ||
|
|
772f79219b | ||
|
|
8830d67af0 | ||
|
|
89c02be41c | ||
|
|
1d092c660b | ||
|
|
a34084da5a | ||
|
|
212f5a9beb | ||
|
|
f6333de4e6 | ||
|
|
5af22f1bab | ||
|
|
02d866d7d6 | ||
|
|
fa7aa6240b | ||
|
|
bde2e398a8 | ||
|
|
8d443b2051 | ||
|
|
33d4b678ed | ||
|
|
3becad1468 | ||
|
|
fad3ba3eae | ||
|
|
cb16f95878 | ||
|
|
4e833490ff | ||
|
|
04a973f7b0 | ||
|
|
0318169b74 | ||
|
|
972fb1e241 | ||
|
|
9beb04b01d | ||
|
|
a3bea6ad24 | ||
|
|
cb38e0d367 | ||
|
|
a133a1d01f | ||
|
|
7996e4ee4a | ||
|
|
79bfc43431 | ||
|
|
69c4bf4213 | ||
|
|
7cd5ca77f5 | ||
|
|
7e736d3cd3 | ||
|
|
13c2adba56 | ||
|
|
010095a50e | ||
|
|
f0cef2103f | ||
|
|
8ed731a48b | ||
|
|
8660d43cb1 | ||
|
|
0f495f620a | ||
|
|
ac81f10ea8 | ||
|
|
9aa95413e6 | ||
|
|
72f3a51af7 | ||
|
|
a0a28a0cb7 | ||
|
|
11d88aed27 | ||
|
|
ee73b487ae | ||
|
|
e580d2e890 | ||
|
|
4f6f53061f | ||
|
|
899c9cdf21 | ||
|
|
be23ec4176 | ||
|
|
186636c2ef | ||
|
|
f0396ff418 | ||
|
|
919d5cffb5 | ||
|
|
67e793b56a | ||
|
|
d84f011d27 | ||
|
|
f685d9ccdd | ||
|
|
380c742f54 | ||
|
|
ed84ea6162 | ||
|
|
e99ffc0d4c | ||
|
|
ca015db188 | ||
|
|
af3a3761f2 | ||
|
|
95085b6306 | ||
|
|
12599db0ff | ||
|
|
c751c85c1c | ||
|
|
f1331a0f6d | ||
|
|
c75c9b60f9 | ||
|
|
eb3adf1dfd | ||
|
|
6533163fd0 | ||
|
|
1becad6016 | ||
|
|
d34653750e | ||
|
|
705592aefd | ||
|
|
583325d6e8 | ||
|
|
318d271127 |
34
README.md
@@ -1,11 +1,33 @@
|
|||||||
# Mastodon for Android
|

|
||||||
[](https://crowdin.com/project/mastodon-for-android)
|
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
|
# Mastodon for Android Fork
|
||||||
|
|
||||||
This is the repository for the official Android app for Mastodon.
|
## Changes
|
||||||
|
|
||||||
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
|
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
|
||||||
|
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
|
||||||
|
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
|
||||||
|
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/8))
|
||||||
|
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/tree/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
|
||||||
|
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodon-android-fork/tree/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
|
||||||
|
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/tree/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
|
||||||
|
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/tree/feature/always-preserve-cw) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||||
|
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/tree/feature/back-returns-home) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||||
|
* [Implement a bookmark button and list](https://github.com/sk22/mastodon-android-fork/tree/feature/bookmarks) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||||
|
* [Implement deleting and re-drafting](https://github.com/sk22/mastodon-android-fork/tree/feature/delete-redraft) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/21))
|
||||||
|
|
||||||
|
## Fork-specific changes
|
||||||
|
|
||||||
|
* Custom app name
|
||||||
|
* Custom icon: Modulate upstream icon using ImageMagick
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mogrify -modulate 90,100,140 mastodon/src/main/res/mipmap-*/ic_launcher*.png
|
||||||
|
```
|
||||||
|
|
||||||
|
* Custom primary color: Hue of all `primary` colors in `colors.xml` is rotated, on basis of upstream Mastodon's [old branding](https://github.com/mastodon/mastodon-android/commit/74f03026cfcfcfd23237c38ff47d2b2a98a6f92a#diff-59134ec2a1cf3761f80b0ecccbbf8b9e433d9780d2f5c5d6ac3ac8cc254e808f)
|
||||||
|
by `109.8°` (equivalent of `161%`, done by hand using
|
||||||
|
[PineTools](https://pinetools.com/shift-hue-color))
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -17,4 +39,4 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is released under the [GPL-3 License](./LICENSE).
|
This project is released under the [GPL-3 License](./LICENSE).
|
||||||
|
|||||||
16
fastlane/metadata/android/cs-CZ/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||||
|
|
||||||
|
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||||
|
|
||||||
|
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||||
|
|
||||||
|
More features:
|
||||||
|
|
||||||
|
• Dark Mode: Read posts in light, dark, or true black mode
|
||||||
|
• Polls: Ask followers for their opinion and tally the votes
|
||||||
|
• Explore: Trending hashtags and accounts are a tap away
|
||||||
|
• Notifications: Get notified about new follows, replies, and reblogs
|
||||||
|
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||||
|
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||||
|
|
||||||
|
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||||
1
fastlane/metadata/android/cs-CZ/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Decentralized social network
|
||||||
1
fastlane/metadata/android/cs-CZ/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Mastodon
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Website ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal was dich interessiert, auf Mastodon kannst du interessierte Leute treffen, die darüber schreiben!
|
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Website ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal was dich interessiert, auf Mastodon kannst du interessierte Leute treffen, die darüber schreiben!
|
||||||
|
|
||||||
Trete einer Gemeinschaft bei und erstelle dein Profil. Finde und verfolge faszinierende Leute und lese ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bilderns, GIFs, Videos und Audio in 500-Zeichen Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bilderns, GIFs, Videos und Audio in 500-Zeichen-Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen, um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||||
|
|
||||||
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge um Missbrauch vorzubeugen.
|
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
|
||||||
|
|
||||||
Weitere Funktionen:
|
Weitere Funktionen:
|
||||||
|
|
||||||
@@ -11,6 +11,6 @@ Weitere Funktionen:
|
|||||||
• Entdecken: Trending Hashtags und Accounts sind nur einen Fingertipp entfernt
|
• Entdecken: Trending Hashtags und Accounts sind nur einen Fingertipp entfernt
|
||||||
• Benachrichtigungen: Erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
|
• Benachrichtigungen: Erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
|
||||||
• Teilen: Veröffentliche auf Mastodon aus jeder beliebigen anderen App
|
• Teilen: Veröffentliche auf Mastodon aus jeder beliebigen anderen App
|
||||||
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant, und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
||||||
|
|
||||||
Mastodon ist eine eingetragene gemeinnützige Organisation und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen es so zu erhalten.
|
Mastodon ist eine eingetragene gemeinnützige Organisation, und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen es so zu erhalten.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Mastodon es la red social descentralizada más grande de internet. En lugar de ser una sola web, es una red de millones de usuarios en comunidades independientes que pueden interactuar entre ellas de forma transparente. No importa qué es lo que hagas, podrás encontrar gente apasionada escribiendo sobre ello en Mastodon!
|
Mastodon es la red social descentralizada más grande de internet. En lugar de ser una sola web, es una red de millones de usuarios en comunidades independientes que pueden interactuar entre ellas de forma transparente. No importa qué es lo que hagas, podrás encontrar gente apasionada escribiendo sobre ello en Mastodon!
|
||||||
|
|
||||||
Únete a una comunidad y crea tu perfil. Encuentra y sigue a gente fascinante y lee sus publicaciones sin anuncios y de forma cronológica. Exprésate con emoticonos personalizados, imágenes, GIFs, vídeos y audio en publicaciónes de 500 caracteres. Responde a hilos e impulsa publicaciones de cualquiera para compartir contenido genial. Encuentra nuevas cuentas para seguir y los hashtags de actualidad para expandir tu red.
|
Únete a una comunidad y crea tu perfil. Encuentra y sigue a gente fascinante y lee sus publicaciones sin anuncios y de forma cronológica. Exprésate con emoticonos personalizados, imágenes, GIFs, vídeos y audio en publicaciónes de 500 caracteres. Responde a hilos e rebloguea publicaciones de cualquiera para compartir contenido genial. Encuentra nuevas cuentas para seguir y los hashtags de actualidad para expandir tu red.
|
||||||
|
|
||||||
Mastodon está construída con un enfoque en la privacidad y la seguridad. Decide si tus publicaciones se comparten con tus seguidores, solo a la gente que menciones, o a todo el mundo. Las advertencias de contenido te permiten esconder publicaciones con contenido sensible o limitarlas de tu visión hasta que estés listo para interactuar con ellas. Cada comunidad tiene sus propias reglas y moderadores para mantener a salvo a sus miembros, además de herramientas robustas para bloquear y reportar contenido para prevenir el abuso.
|
Mastodon está construída con un enfoque en la privacidad y la seguridad. Decide si tus publicaciones se comparten con tus seguidores, solo a la gente que menciones, o a todo el mundo. Las advertencias de contenido te permiten esconder publicaciones con contenido sensible o limitarlas de tu visión hasta que estés listo para interactuar con ellas. Cada comunidad tiene sus propias reglas y moderadores para mantener a salvo a sus miembros, además de herramientas robustas para bloquear y reportar contenido para prevenir el abuso.
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ Más características:
|
|||||||
• Modo oscuro: Lee las publicaciones en modo claro, oscuro o negro real
|
• Modo oscuro: Lee las publicaciones en modo claro, oscuro o negro real
|
||||||
• Encuestas: Pide opinión a tus seguidores y cuenta los votos
|
• Encuestas: Pide opinión a tus seguidores y cuenta los votos
|
||||||
• Explora: Hashtags y cuentas en tendencia a un solo toque
|
• Explora: Hashtags y cuentas en tendencia a un solo toque
|
||||||
• Notificaciones: Recibe notificaciones sobre nuevos seguidores, respuestas e impulsos
|
• Notificaciones: Recibe notificaciones sobre nuevos seguidores, respuestas y reblogueos
|
||||||
• Compartir: Publica directamente a Mastodon desde cualquier hoja de acción en cualquier aplicación
|
• Compartir: Publica directamente a Mastodon desde cualquier hoja de acción en cualquier aplicación
|
||||||
• Preciosidad: Nuestra mascota es un elefante adorable, y verás que aparece de vez en cuando
|
• Preciosidad: Nuestra mascota es un elefante adorable, y verás que aparece de vez en cuando
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata intohimoisia ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
|
||||||
|
|
||||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
Liity yhteisöön ja luo itsellesi tili. Löydä ja seuraa kiehtovia ihmisiä ja lue heidän julkaisunsa ilman mainoksia, kronologisella aikajanalla. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja trendaavia hashtageja laajentaaksesi verkostoasi.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
Mastodon on rakennettu keskittyen yksityisyyteen ja turvallisuuteen. Päätä, jaetaanko julkaisusi seuraajille, vain mainitsemillesi ihmisille vai koko maailmalle. Sisältövaroitusten avulla, voit piilottaa julkaisut, jotka sisältävät arkaluontoista tai laukaisevaa materiaalia, kunnes olet valmis käsittelemään niitä. Jokaisella yhteisöllä on omat ohjeistonsa ja valvojansa, jotka pitävät jäsenensä turvassa, ja tehokkaat esto- ja ilmiantotyökalut auttavat torjumaan väärinkäytöksiä.
|
||||||
|
|
||||||
More features:
|
Lisää ominaisuuksia:
|
||||||
|
|
||||||
• Dark Mode: Read posts in light, dark, or true black mode
|
• Tumma tila: Lue julkaisut vaaleassa, tummassa tai mustan tummassa tilassa
|
||||||
• Polls: Ask followers for their opinion and tally the votes
|
• Kyselyt: Kysy seuraajilta heidän mielipidettään ja laske äänet
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• Tutustu: Trendaavat hashtagit ja tilit ovat vain napsautuksen päässä
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja edelleen jaoista
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• Jakaminen: Julkaise suoraan Mastodoniin minkä tahansa sovelluksen jakovalikon kautta
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• Suloisuus: Maskottimme on ihastuttava mastodontti ja näet sen ajoittain
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
Mastodon on rekisteröity voittoa tavoittelematon organisaatio ja kehitystä tuetaan suoraan lahjoituksillasi. Ei mainontaa, kaupallistamista eikä riskipääomaa, ja aiomme pitää sen sellaisena.
|
||||||
@@ -1 +1 @@
|
|||||||
Decentralized social network
|
Hajautettu sosiaalinen verkosto
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
마스토돈은 인터넷에서 가장 큰 분산 소셜 네트워크입니다. 단 하나의 통일된 웹사이트 대신, 수백만 명의 사용자들이, 서로 경계 없이 소통할 수 있는 독립적인 커뮤니티의 네트워크입니다. 당신이 어디에 속하든간에, 마스토돈에 열정적으로 게시물을 남기는 사람들을 만날 수 있습니다!
|
||||||
|
|
||||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
커뮤니티에 가입하고 프로필을 생성하세요. 매력적인 사람들을 찾아 팔로우하고 그들의 글을 광고가 없고, 시간순으로 정렬된 타임라인에서 읽으세요. 커스텀 에모지, 그림, 움짤, 동영상, 소리와 함께 500자의 글로 당신을 표현하세요. 아무에게나 글타래에 답장을 하고 게시물을 리블로그하여 멋진 것들을 공유하세요. 새로 팔로우 할 계정이나 유행 중인 해시태그를 찾아 당신의 인맥을 넓히세요.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
마스토돈은 개인정보 보호와 안전에 중점을 두고 만들어졌습니다. 당신의 게시물을 팔로워들에게만 공개할지, 언급한 사람들에게만 공유할지, 아니면 전세계에 공유할 지 선택하세요. 열람주의는 민감하거나 남들에게 껄끄러울 수 있는 게시물을 마음의 준비가 된 사람들만 열람하도록 숨길 수 있도록 해줍니다. 각각의 커뮤니티는 구성원들을 안전하게 지키기 위한 각자의 규정과 중재자들을 가지고 있으며, 남용을 방지하기 위한 강력한 차단 도구와 신고 도구를 가지고 있습니다.
|
||||||
|
|
||||||
더 많은 기능:
|
더 많은 기능:
|
||||||
|
|
||||||
• 다크모드: 게시물을 밝음, 어두움, 진정한 검정 모드에서 읽으세요
|
• 다크모드: 게시물을 밝음, 어두움, 진정한 검정 모드에서 읽으세요
|
||||||
• Polls: Ask followers for their opinion and tally the votes
|
• 투표: 팔로워들에게 의견을 물어보고 투표를 집계하세요
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• 탐색: 유행 중인 해시태그와 계정을 탭 한 번에 볼 수 있습니다
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• 알림: 새로운 팔로우, 답글, 리블로그에 대한 알림을 받으세요
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• 공유: 어떤 앱에서든 공유 기능으로 바로 마스토돈에 게시하세요
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• 귀여움: 우리의 마스코트는 귀여운 코끼리입니다, 때때로 이들을 볼 수 있습니다
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
마스토돈은 등록된 상호이며 여러분들의 직접적인 기부를 통해 개발되고 있습니다. 광고도, 유료화도, 벤처 캐피탈도 없습니다, 그리고 계속 그렇게 할 생각입니다.
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon to największa zdecentralizowana sieć społecznościowa w Internecie. Zamiast jednej strony internetowej, jest to sieć milionów użytkowników w niezależnych społecznościach, które mogą ze sobą wchodzić w interakcje. Niezależnie od swoich zainteresowań, momżesz poznać interesujących ludzi piszących o nich na Mastodonie!
|
Mastodon to największa zdecentralizowana sieć społecznościowa w Internecie. Zamiast jednej strony internetowej, jest to sieć milionów użytkowników w niezależnych społecznościach, które mogą ze sobą wchodzić w interakcje. Niezależnie od swoich zainteresowań, momżesz poznać interesujących ludzi piszących o nich na Mastodonie!
|
||||||
|
|
||||||
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Find new accounts to follow and trending hashtags to expand your network.
|
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Odnajduj nowe konta do śledzenia i zyskujące popularność hashtagi, by poszerzać swoją sieć.
|
||||||
|
|
||||||
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decyduj, czy Twoje wpisy są udostępniane osobom śledzącym Cię, tylko wzmiankowanym, czy całemu światu. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Każda społeczność ma własne wytyczne i moderatorów, aby zapewniać swoim członkom bezpieczeństwo, a także solidne narzędzia blokowania i raportowania pomagające zapobiegać nadużyciom.
|
||||||
|
|
||||||
Więcej funkcji:
|
Więcej funkcji:
|
||||||
|
|
||||||
• Tryb ciemny: Czytaj wpisy w jasnym, ciemnym lub czarnym trybie
|
• Tryb ciemny: Czytaj wpisy w jasnym, ciemnym lub czarnym trybie
|
||||||
• Ankiety: Poproś obserwujących o ich opinię i poznaj ich głosy
|
• Ankiety: Poproś obserwujących o ich opinię i poznaj ich głosy
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• Odkrywaj: Najpopularniejsze hashtagi i konta są dostępne za jednym dotknięciem
|
||||||
• Powiadomienia: Otrzymuj powiadomienia o nowych obserwacjach, odpowiedziach i udostępnieniach
|
• Powiadomienia: Otrzymuj powiadomienia o nowych obserwacjach, odpowiedziach i udostępnieniach
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• Udostępnianie: Publikuj bezpośrednio na Mastodonie z menu udostępniania w dowolnej aplikacji
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• Słodycz: Nasza maskotka to uroczy słoń i zobaczysz go pojawiającego się od czasu do czasu
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
Mastodon to zarejestrowana organizacja non-profit i rozwój jest wspierany bezpośrednio przez darowizny. Nie ma reklam, monetyzacji, ani kapitału inwestycyjnego i planujemy, by tak pozostało.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
O Mastodon é a maior rede social descentralizada da Internet. Em vez de ser um único site, é uma rede de milhões de utilizadores em comunidades independentes que podem facilmente interagir uns com os outros. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
O Mastodon é a maior rede social descentralizada da Internet. Em vez de ser um único site, é uma rede de milhões de utilizadores em comunidades independentes que podem facilmente interagir uns com os outros. Independemente dos teus gostos, consegues encontrar pessoas que os partilhem no Mastodon!
|
||||||
|
|
||||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
Junta-te a uma comunidade e cria o teu perfil. Encontra, segue gente fascinante e lê as suas publicações numa cronologia sem anúncios. Expressa-te com emojis personalizados imagens, GIFs, vídeos e áudio em publicações com até 500 caracteres. Responde a tópicos e promove publicações de qualquer pessoa para partilhares ótimas coisas. Encontra novas contas e tendências a seguir para expandires a tua rede.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
O Mastodon é construído com foco na privacidade e segurança. Decide se as tuas publicações são partilhadas com os teus seguidores, apenas com as pessoas mencionadas, ou com o mundo inteiro. Avisos de conteúdo permitem-te esconder publicações que contenham material sensível ou provocatório até estares pronto(a) para o veres. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||||
|
|
||||||
More features:
|
More features:
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
Mastodon är det största decentraliserade sociala nätverket på internet. I stället för en enda webbplats är det ett nätverk av miljontals användare på oberoende servrar som alla kan interagera med varandra, sömlöst. Oavsett vad du är intresserad av kan du träffa passionerade personer som diskuterar ämnet på Mastodon!
|
||||||
|
|
||||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
Gå med på en server och skapa din profil. Hitta och följ fascinerande människor och läsa deras inlägg i en annonsfri, kronologisk tidslinje. Uttryck dig med anpassade emoji, bilder, GIF:ar, videor och ljud i 500-teckensinlägg. Svara på trådar och ompostningar från vem som helst för att dela bra saker. Hitta nya konton att följa och trendande hashtaggar för att utöka ditt nätverk.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
Mastodon är byggt med fokus på integritet och trygghet. Bestäm om dina inlägg delas med dina följare, bara personer du omnämner, eller hela världen. Innehållsvarningar låter dig dölja inlägg som innehåller känsligt eller triggande material tills du är redo att interagera med dem. Varje server har sina egna riktlinjer och moderatorer för att hålla sina medlemmar trygga, och robusta blockerings- och rapporteringsverktyg för att förhindra missbruk.
|
||||||
|
|
||||||
More features:
|
Fler funktioner:
|
||||||
|
|
||||||
• Dark Mode: Read posts in light, dark, or true black mode
|
• Mörkt läge: Läs inlägg i ljust, mörkt eller helsvart läge
|
||||||
• Polls: Ask followers for their opinion and tally the votes
|
• Omröstningar: Fråga följare om deras åsikt och sammanställ deras röster
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• Utforska: Trendande hashtaggar och konton är ett tryck bort
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• Notiser: Bli meddelad om nya följare, svar och ompostningar
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• Delning: Posta direkt till Mastodon från delningsbladet i alla appar
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• Gullighet: Vår maskot är en bedårande elefant, och du kommer att se dem dyka upp då och då
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
Mastodon är en registrerad ideell förening och utvecklingen stöds direkt av dina donationer. Det finns ingen reklam, ingen monetarisering, och inget riskkapital, och vi planerar att behålla det på det sättet.
|
||||||
@@ -1 +1 @@
|
|||||||
Decentralized social network
|
Decentraliserat socialt nätverk
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
|
||||||
|
|
||||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในไทม์ไลน์ที่ไร้โฆษณาและเรียงตามลำดับเวลาล้วน ๆ แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้ควบคุมเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
|
||||||
|
|
||||||
More features:
|
คุณสมบัติอื่น ๆ:
|
||||||
|
|
||||||
• Dark Mode: Read posts in light, dark, or true black mode
|
• โหมดมืด: อ่านโพสต์ในโหมดสว่าง มืด หรือโหมดมืดดำสนิท
|
||||||
• Polls: Ask followers for their opinion and tally the votes
|
• การสำรวจความคิดเห็น: สำรวจความคิดเห็นของผู้ติดตามและนับจำนวนการลงคะแนน
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• สำรวจ: แตะปุ่มเดียวเพื่อดูแฮชแท็กและบัญชีที่เป็นที่นิยม
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• การแจ้งเตือน: รับการแจ้งเตือนเกี่ยวกับผู้ติดตามใหม่ การตอบกลับ และการดันโพสต์
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
|
||||||
@@ -6,11 +6,11 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
compileSdk 31
|
compileSdk 31
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.joinmastodon.android"
|
applicationId "org.joinmastodon.android.sk"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 31
|
targetSdk 31
|
||||||
versionCode 37
|
versionCode 21
|
||||||
versionName "1.1.1"
|
versionName '1.1.3+fork.21'
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +47,10 @@ android {
|
|||||||
setRoot "src/appcenter"
|
setRoot "src/appcenter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lintOptions{
|
||||||
|
checkReleaseBuilds false
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
7
mastodon/proguard-rules.pro
vendored
@@ -46,4 +46,9 @@
|
|||||||
|
|
||||||
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
||||||
|
|
||||||
-keepattributes LineNumberTable
|
-keepattributes LineNumberTable
|
||||||
|
|
||||||
|
# Parceler library
|
||||||
|
-keep interface org.parceler.Parcel
|
||||||
|
-keep @org.parceler.Parcel class * { *; }
|
||||||
|
-keep class **$$Parcelable { *; }
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 46 KiB |
@@ -9,6 +9,7 @@ import android.util.Log;
|
|||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
import org.joinmastodon.android.fragments.HomeFragment;
|
import org.joinmastodon.android.fragments.HomeFragment;
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.SplashFragment;
|
import org.joinmastodon.android.fragments.SplashFragment;
|
||||||
@@ -56,6 +57,8 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
||||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||||
showFragmentForNotification(notification, session.getID());
|
showFragmentForNotification(notification, session.getID());
|
||||||
|
}else if(intent.getBooleanExtra("compose", false)){
|
||||||
|
showCompose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +94,8 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
showFragmentClearingBackStack(fragment);
|
showFragmentClearingBackStack(fragment);
|
||||||
}
|
}
|
||||||
|
}else if(intent.getBooleanExtra("compose", false)){
|
||||||
|
showCompose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,4 +120,15 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
showFragment(fragment);
|
showFragment(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showCompose(){
|
||||||
|
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||||
|
if(session==null || !session.activated)
|
||||||
|
return;
|
||||||
|
ComposeFragment compose=new ComposeFragment();
|
||||||
|
Bundle composeArgs=new Bundle();
|
||||||
|
composeArgs.putString("account", session.getID());
|
||||||
|
compose.setArguments(composeArgs);
|
||||||
|
showFragment(compose);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.joinmastodon.android.api;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class AvatarResizedImageRequestBody extends ResizedImageRequestBody{
|
||||||
|
public AvatarResizedImageRequestBody(Uri uri, ProgressListener progressListener) throws IOException{
|
||||||
|
super(uri, 0, progressListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int[] getTargetSize(int srcWidth, int srcHeight){
|
||||||
|
float factor=400f/Math.min(srcWidth, srcHeight);
|
||||||
|
return new int[]{Math.round(srcWidth*factor), Math.round(srcHeight*factor)};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean needResize(int srcWidth, int srcHeight){
|
||||||
|
return srcHeight>400 || srcWidth!=srcHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean needCrop(int srcWidth, int srcHeight){
|
||||||
|
return srcWidth!=srcHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Rect getCropBounds(int srcWidth, int srcHeight){
|
||||||
|
Rect rect=new Rect();
|
||||||
|
if(srcWidth>srcHeight){
|
||||||
|
rect.set(srcWidth/2-srcHeight/2, 0, srcWidth/2-srcHeight/2+srcHeight, srcHeight);
|
||||||
|
}else{
|
||||||
|
rect.set(0, srcHeight/2-srcWidth/2, srcWidth, srcHeight/2-srcWidth/2+srcWidth);
|
||||||
|
}
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,6 +233,12 @@ public class CacheController{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteStatus(String id){
|
||||||
|
runOnDbThread((db)->{
|
||||||
|
db.delete("home_timeline", "`id`=?", new String[]{id});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void clearRecentSearches(){
|
public void clearRecentSearches(){
|
||||||
runOnDbThread((db)->db.delete("recent_searches", null, null));
|
runOnDbThread((db)->db.delete("recent_searches", null, null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import android.os.Build;
|
|||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
|
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@@ -30,62 +31,105 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
|||||||
private File tempFile;
|
private File tempFile;
|
||||||
private Uri uri;
|
private Uri uri;
|
||||||
private String contentType;
|
private String contentType;
|
||||||
|
private int maxSize;
|
||||||
|
|
||||||
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
|
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
|
||||||
super(progressListener);
|
super(progressListener);
|
||||||
this.uri=uri;
|
this.uri=uri;
|
||||||
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
this.maxSize=maxSize;
|
||||||
BitmapFactory.Options opts=new BitmapFactory.Options();
|
BitmapFactory.Options opts=new BitmapFactory.Options();
|
||||||
opts.inJustDecodeBounds=true;
|
opts.inJustDecodeBounds=true;
|
||||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
if("file".equals(uri.getScheme())){
|
||||||
BitmapFactory.decodeStream(in, null, opts);
|
BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||||
|
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
|
||||||
|
}else{
|
||||||
|
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||||
|
BitmapFactory.decodeStream(in, null, opts);
|
||||||
|
}
|
||||||
|
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
||||||
}
|
}
|
||||||
if(opts.outWidth*opts.outHeight>maxSize){
|
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
|
||||||
Bitmap bitmap;
|
Bitmap bitmap;
|
||||||
if(Build.VERSION.SDK_INT>=29){
|
if(Build.VERSION.SDK_INT>=28){
|
||||||
bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{
|
ImageDecoder.Source source;
|
||||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight())));
|
if("file".equals(uri.getScheme())){
|
||||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth())));
|
source=ImageDecoder.createSource(new File(uri.getPath()));
|
||||||
|
}else{
|
||||||
|
source=ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri);
|
||||||
|
}
|
||||||
|
bitmap=ImageDecoder.decodeBitmap(source, (decoder, info, _source)->{
|
||||||
|
int[] size=getTargetSize(info.getSize().getWidth(), info.getSize().getHeight());
|
||||||
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
|
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
|
||||||
decoder.setTargetSize(targetWidth, targetHeight);
|
decoder.setTargetSize(size[0], size[1]);
|
||||||
|
// Breaks images in mysterious ways
|
||||||
|
// if(needCrop(size[0], size[1]))
|
||||||
|
// decoder.setCrop(getCropBounds(size[0], size[1]));
|
||||||
});
|
});
|
||||||
|
if(needCrop(bitmap.getWidth(), bitmap.getHeight())){
|
||||||
|
Rect crop=getCropBounds(bitmap.getWidth(), bitmap.getHeight());
|
||||||
|
bitmap=Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height());
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight)));
|
int[] size=getTargetSize(opts.outWidth, opts.outHeight);
|
||||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth)));
|
int targetWidth=size[0];
|
||||||
|
int targetHeight=size[1];
|
||||||
float factor=opts.outWidth/(float)targetWidth;
|
float factor=opts.outWidth/(float)targetWidth;
|
||||||
opts=new BitmapFactory.Options();
|
opts=new BitmapFactory.Options();
|
||||||
opts.inSampleSize=(int)factor;
|
opts.inSampleSize=(int)factor;
|
||||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
if("file".equals(uri.getScheme())){
|
||||||
bitmap=BitmapFactory.decodeStream(in, null, opts);
|
bitmap=BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||||
|
}else{
|
||||||
|
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||||
|
bitmap=BitmapFactory.decodeStream(in, null, opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(factor%1f!=0f){
|
boolean needCrop=needCrop(targetWidth, targetHeight);
|
||||||
Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
|
if(factor%1f!=0f || needCrop){
|
||||||
new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, targetWidth, targetHeight), new Paint(Paint.FILTER_BITMAP_FLAG));
|
Rect srcBounds=null;
|
||||||
|
Rect dstBounds;
|
||||||
|
if(needCrop){
|
||||||
|
Rect crop=getCropBounds(targetWidth, targetHeight);
|
||||||
|
dstBounds=new Rect(0, 0, crop.width(), crop.height());
|
||||||
|
srcBounds=new Rect(
|
||||||
|
Math.round(crop.left/(float)targetWidth*bitmap.getWidth()),
|
||||||
|
Math.round(crop.top/(float)targetHeight*bitmap.getHeight()),
|
||||||
|
Math.round(crop.right/(float)targetWidth*bitmap.getWidth()),
|
||||||
|
Math.round(crop.bottom/(float)targetHeight*bitmap.getHeight())
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
dstBounds=new Rect(0, 0, targetWidth, targetHeight);
|
||||||
|
}
|
||||||
|
Bitmap scaled=Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888);
|
||||||
|
new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG));
|
||||||
bitmap=scaled;
|
bitmap=scaled;
|
||||||
}
|
}
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
int orientation=0;
|
||||||
int rotation;
|
if("file".equals(uri.getScheme())){
|
||||||
|
ExifInterface exif=new ExifInterface(uri.getPath());
|
||||||
|
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||||
ExifInterface exif=new ExifInterface(in);
|
ExifInterface exif=new ExifInterface(in);
|
||||||
int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
rotation=switch(orientation){
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
|
|
||||||
default -> 0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if(rotation!=0){
|
|
||||||
Matrix matrix=new Matrix();
|
|
||||||
matrix.setRotate(rotation);
|
|
||||||
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
int rotation=switch(orientation){
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
|
||||||
|
default -> 0;
|
||||||
|
};
|
||||||
|
if(rotation!=0){
|
||||||
|
Matrix matrix=new Matrix();
|
||||||
|
matrix.setRotate(rotation);
|
||||||
|
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image");
|
boolean isPNG="image/png".equals(contentType);
|
||||||
|
tempFile=File.createTempFile("mastodon_tmp_resized", null);
|
||||||
try(FileOutputStream out=new FileOutputStream(tempFile)){
|
try(FileOutputStream out=new FileOutputStream(tempFile)){
|
||||||
if("image/png".equals(contentType)){
|
if(isPNG){
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||||
}else{
|
}else{
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
|
||||||
@@ -94,9 +138,13 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
|||||||
}
|
}
|
||||||
length=tempFile.length();
|
length=tempFile.length();
|
||||||
}else{
|
}else{
|
||||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
if("file".equals(uri.getScheme())){
|
||||||
cursor.moveToFirst();
|
length=new File(uri.getPath()).length();
|
||||||
length=cursor.getInt(0);
|
}else{
|
||||||
|
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||||
|
cursor.moveToFirst();
|
||||||
|
length=cursor.getInt(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,4 +173,22 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int[] getTargetSize(int srcWidth, int srcHeight){
|
||||||
|
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)srcWidth/srcHeight)));
|
||||||
|
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)srcHeight/srcWidth)));
|
||||||
|
return new int[]{targetWidth, targetHeight};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean needResize(int srcWidth, int srcHeight){
|
||||||
|
return srcWidth*srcHeight>maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean needCrop(int srcWidth, int srcHeight){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Rect getCropBounds(int srcWidth, int srcHeight){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Looper;
|
|||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
||||||
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
@@ -18,6 +19,7 @@ public class StatusInteractionController{
|
|||||||
private final String accountID;
|
private final String accountID;
|
||||||
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
||||||
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
||||||
|
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
|
||||||
|
|
||||||
public StatusInteractionController(String accountID){
|
public StatusInteractionController(String accountID){
|
||||||
this.accountID=accountID;
|
this.accountID=accountID;
|
||||||
@@ -61,6 +63,36 @@ public class StatusInteractionController{
|
|||||||
E.post(new StatusCountersUpdatedEvent(status));
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBookmarked(Status status, boolean bookmarked){
|
||||||
|
if(!Looper.getMainLooper().isCurrentThread())
|
||||||
|
throw new IllegalStateException("Can only be called from main thread");
|
||||||
|
|
||||||
|
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
|
||||||
|
if(current!=null){
|
||||||
|
current.cancel();
|
||||||
|
}
|
||||||
|
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Status result){
|
||||||
|
runningBookmarkRequests.remove(status.id);
|
||||||
|
E.post(new StatusCountersUpdatedEvent(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
runningBookmarkRequests.remove(status.id);
|
||||||
|
error.showToast(MastodonApp.context);
|
||||||
|
status.bookmarked=!bookmarked;
|
||||||
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
runningBookmarkRequests.put(status.id, req);
|
||||||
|
status.bookmarked=bookmarked;
|
||||||
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
|
}
|
||||||
|
|
||||||
public void setReblogged(Status status, boolean reblogged){
|
public void setReblogged(Status status, boolean reblogged){
|
||||||
if(!Looper.getMainLooper().isCurrentThread())
|
if(!Looper.getMainLooper().isCurrentThread())
|
||||||
throw new IllegalStateException("Can only be called from main thread");
|
throw new IllegalStateException("Can only be called from main thread");
|
||||||
|
|||||||
@@ -21,18 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
|||||||
switch(filter){
|
switch(filter){
|
||||||
case DEFAULT -> addQueryParameter("exclude_replies", "true");
|
case DEFAULT -> addQueryParameter("exclude_replies", "true");
|
||||||
case INCLUDE_REPLIES -> {}
|
case INCLUDE_REPLIES -> {}
|
||||||
|
case PINNED -> addQueryParameter("pinned", "true");
|
||||||
case MEDIA -> addQueryParameter("only_media", "true");
|
case MEDIA -> addQueryParameter("only_media", "true");
|
||||||
case NO_REBLOGS -> {
|
case NO_REBLOGS -> {
|
||||||
addQueryParameter("exclude_replies", "true");
|
addQueryParameter("exclude_replies", "true");
|
||||||
addQueryParameter("exclude_reblogs", "true");
|
addQueryParameter("exclude_reblogs", "true");
|
||||||
}
|
}
|
||||||
|
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Filter{
|
public enum Filter{
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
INCLUDE_REPLIES,
|
INCLUDE_REPLIES,
|
||||||
|
PINNED,
|
||||||
MEDIA,
|
MEDIA,
|
||||||
NO_REBLOGS
|
NO_REBLOGS,
|
||||||
|
OWN_POSTS_AND_REPLIES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
|
||||||
|
private String maxId;
|
||||||
|
|
||||||
|
public GetBookmarks(String maxID, String minID, int limit){
|
||||||
|
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(minID!=null)
|
||||||
|
addQueryParameter("min_id", minID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", ""+limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
|
||||||
|
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||||
|
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
|
||||||
|
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
|
||||||
|
String link=httpResponse.header("link");
|
||||||
|
// parsing link header by hand; using a library would be cleaner
|
||||||
|
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
||||||
|
if(link==null) return;
|
||||||
|
String maxIdEq="max_id=";
|
||||||
|
for(String s : link.split(",")) {
|
||||||
|
if(s.contains("rel=\"next\"")) {
|
||||||
|
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
||||||
|
int end=s.indexOf('>');
|
||||||
|
if(start<0 || start>end) return;
|
||||||
|
this.maxId=s.substring(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMaxId() {
|
||||||
|
return maxId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,16 @@ package org.joinmastodon.android.api.requests.accounts;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.AvatarResizedImageRequestBody;
|
||||||
import org.joinmastodon.android.api.ContentUriRequestBody;
|
import org.joinmastodon.android.api.ContentUriRequestBody;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.ResizedImageRequestBody;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.AccountField;
|
import org.joinmastodon.android.model.AccountField;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
@@ -39,21 +42,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBody getRequestBody(){
|
public RequestBody getRequestBody() throws IOException{
|
||||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart("display_name", displayName)
|
.addFormDataPart("display_name", displayName)
|
||||||
.addFormDataPart("note", bio);
|
.addFormDataPart("note", bio);
|
||||||
|
|
||||||
if(avatar!=null){
|
if(avatar!=null){
|
||||||
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null));
|
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new AvatarResizedImageRequestBody(avatar, null));
|
||||||
}else if(avatarFile!=null){
|
}else if(avatarFile!=null){
|
||||||
bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile));
|
bldr.addFormDataPart("avatar", avatarFile.getName(), new AvatarResizedImageRequestBody(Uri.fromFile(avatarFile), null));
|
||||||
}
|
}
|
||||||
if(cover!=null){
|
if(cover!=null){
|
||||||
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null));
|
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ResizedImageRequestBody(cover, 1500*500, null));
|
||||||
}else if(coverFile!=null){
|
}else if(coverFile!=null){
|
||||||
bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile));
|
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||||
}
|
}
|
||||||
if(fields.isEmpty()){
|
if(fields.isEmpty()){
|
||||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class Request{
|
private static class Request{
|
||||||
public String clientName="Mastodon for Android";
|
public String clientName="Mastodon for Android Fork";
|
||||||
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
||||||
public String scopes=AccountSessionManager.SCOPE;
|
public String scopes=AccountSessionManager.SCOPE;
|
||||||
public String website="https://app.joinmastodon.org/android";
|
public String website="https://github.com/sk22/mastodon-android-fork";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
public class EditStatus extends MastodonAPIRequest<Status>{
|
||||||
|
public EditStatus(CreateStatus.Request req, String id){
|
||||||
|
super(HttpMethod.PUT, "/statuses/"+id, Status.class);
|
||||||
|
setRequestBody(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
||||||
|
public GetStatusEditHistory(String id){
|
||||||
|
super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException{
|
||||||
|
int i=0;
|
||||||
|
for(Status s:respObj){
|
||||||
|
s.uri="";
|
||||||
|
s.id="fakeID"+i;
|
||||||
|
s.visibility=StatusPrivacy.PUBLIC;
|
||||||
|
s.mentions=Collections.emptyList();
|
||||||
|
s.tags=Collections.emptyList();
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.BaseModel;
|
||||||
|
|
||||||
|
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
|
||||||
|
public GetStatusSourceText(String id){
|
||||||
|
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllFieldsAreRequired
|
||||||
|
public static class Response extends BaseModel{
|
||||||
|
public String id;
|
||||||
|
public String text;
|
||||||
|
public String spoilerText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
public class SetStatusBookmarked extends MastodonAPIRequest<Status>{
|
||||||
|
public SetStatusBookmarked(String id, boolean bookmarked){
|
||||||
|
super(HttpMethod.POST, "/statuses/"+id+"/"+(bookmarked ? "bookmark" : "unbookmark"), Status.class);
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
public class SetStatusPinned extends MastodonAPIRequest<Status>{
|
||||||
|
public SetStatusPinned(String id, boolean pinned){
|
||||||
|
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,22 @@ package org.joinmastodon.android.api.session;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.ShortcutInfo;
|
||||||
|
import android.content.pm.ShortcutManager;
|
||||||
|
import android.graphics.drawable.Icon;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.MainActivity;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
@@ -85,11 +92,12 @@ public class AccountSessionManager{
|
|||||||
domains.add(session.domain.toLowerCase());
|
domains.add(session.domain.toLowerCase());
|
||||||
sessions.put(session.getID(), session);
|
sessions.put(session.getID(), session);
|
||||||
}
|
}
|
||||||
}catch(IOException|JsonParseException x){
|
}catch(Exception x){
|
||||||
Log.e(TAG, "Error loading accounts", x);
|
Log.e(TAG, "Error loading accounts", x);
|
||||||
}
|
}
|
||||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||||
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
|
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
|
||||||
|
maybeUpdateShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
|
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
|
||||||
@@ -102,6 +110,7 @@ public class AccountSessionManager{
|
|||||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||||
}
|
}
|
||||||
|
maybeUpdateShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void writeAccountsFile(){
|
public synchronized void writeAccountsFile(){
|
||||||
@@ -181,6 +190,7 @@ public class AccountSessionManager{
|
|||||||
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
|
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
|
||||||
nm.deleteNotificationChannelGroup(id);
|
nm.deleteNotificationChannelGroup(id);
|
||||||
}
|
}
|
||||||
|
maybeUpdateShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -358,7 +368,7 @@ public class AccountSessionManager{
|
|||||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||||
instances.put(domain, emojis.instance);
|
instances.put(domain, emojis.instance);
|
||||||
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
||||||
}catch(IOException|JsonParseException x){
|
}catch(Exception x){
|
||||||
Log.w(TAG, "Error reading instance info file for "+domain, x);
|
Log.w(TAG, "Error reading instance info file for "+domain, x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,6 +405,29 @@ public class AccountSessionManager{
|
|||||||
writeAccountsFile();
|
writeAccountsFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void maybeUpdateShortcuts(){
|
||||||
|
if(Build.VERSION.SDK_INT<26)
|
||||||
|
return;
|
||||||
|
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
|
||||||
|
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
|
||||||
|
// There are no shortcuts, but there are accounts. Add a compose shortcut.
|
||||||
|
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
||||||
|
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
||||||
|
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
|
||||||
|
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
|
||||||
|
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
|
||||||
|
.setAction(Intent.ACTION_MAIN)
|
||||||
|
.putExtra("compose", true))
|
||||||
|
.build();
|
||||||
|
sm.setDynamicShortcuts(Collections.singletonList(info));
|
||||||
|
}else if(sessions.isEmpty()){
|
||||||
|
// There are shortcuts, but no accounts. Disable existing shortcuts.
|
||||||
|
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
|
||||||
|
}else{
|
||||||
|
sm.enableShortcuts(Collections.singletonList("compose"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class SessionsStorageWrapper{
|
private static class SessionsStorageWrapper{
|
||||||
public List<AccountSession> accounts;
|
public List<AccountSession> accounts;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.joinmastodon.android.model.Status;
|
|||||||
public class StatusCountersUpdatedEvent{
|
public class StatusCountersUpdatedEvent{
|
||||||
public String id;
|
public String id;
|
||||||
public int favorites, reblogs, replies;
|
public int favorites, reblogs, replies;
|
||||||
public boolean favorited, reblogged;
|
public boolean favorited, reblogged, pinned;
|
||||||
|
|
||||||
public StatusCountersUpdatedEvent(Status s){
|
public StatusCountersUpdatedEvent(Status s){
|
||||||
id=s.id;
|
id=s.id;
|
||||||
@@ -14,5 +14,6 @@ public class StatusCountersUpdatedEvent{
|
|||||||
replies=s.repliesCount;
|
replies=s.repliesCount;
|
||||||
favorited=s.favourited;
|
favorited=s.favourited;
|
||||||
reblogged=s.reblogged;
|
reblogged=s.reblogged;
|
||||||
|
pinned=s.pinned;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class StatusUnpinnedEvent {
|
||||||
|
public final String id;
|
||||||
|
public final String accountID;
|
||||||
|
|
||||||
|
public StatusUnpinnedEvent(String id, String accountID){
|
||||||
|
this.id=id;
|
||||||
|
this.accountID=accountID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
public class StatusUpdatedEvent{
|
||||||
|
public Status status;
|
||||||
|
|
||||||
|
public StatusUpdatedEvent(Status status){
|
||||||
|
this.status=status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,10 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -76,6 +78,7 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
||||||
return;
|
return;
|
||||||
|
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||||
// Keep replies to self, discard all other replies
|
// Keep replies to self, discard all other replies
|
||||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||||
@@ -86,4 +89,24 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
}
|
}
|
||||||
prependItems(Collections.singletonList(ev.status), true);
|
prependItems(Collections.singletonList(ev.status), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Status status=getStatusByID(ev.id);
|
||||||
|
data.remove(status);
|
||||||
|
preloadedData.remove(status);
|
||||||
|
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
|
||||||
|
if(item==null)
|
||||||
|
return;
|
||||||
|
int index=displayItems.indexOf(item);
|
||||||
|
int lastIndex;
|
||||||
|
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||||
|
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
displayItems.subList(index, lastIndex).clear();
|
||||||
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -457,13 +457,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
status.spoilerRevealed=true;
|
status.spoilerRevealed=true;
|
||||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||||
if(text!=null)
|
if(text!=null)
|
||||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
|
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||||
if(header!=null)
|
if(header!=null)
|
||||||
header.rebind();
|
header.rebind();
|
||||||
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
|
updateImagesSpoilerState(status, itemID);
|
||||||
photo.setRevealed(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
||||||
@@ -472,12 +470,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||||
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
|
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
|
||||||
if(text!=null){
|
if(text!=null){
|
||||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
|
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.rebind();
|
holder.rebind();
|
||||||
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){
|
updateImagesSpoilerState(status, holder.getItemID());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateImagesSpoilerState(Status status, String itemID){
|
||||||
|
ArrayList<Integer> updatedPositions=new ArrayList<>();
|
||||||
|
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
|
||||||
photo.setRevealed(status.spoilerRevealed);
|
photo.setRevealed(status.spoilerRevealed);
|
||||||
|
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||||
|
}
|
||||||
|
int i=0;
|
||||||
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
|
||||||
|
adapter.notifyItemChanged(i);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,6 +579,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ArrayList<StatusDisplayItem> getDisplayItems(){
|
||||||
|
return displayItems;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplyWindowInsets(WindowInsets insets){
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
|
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class BookmarksListFragment extends StatusListFragment{
|
||||||
|
|
||||||
|
private String accountID;
|
||||||
|
private Account self;
|
||||||
|
private String lastMaxId=null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
accountID=getArguments().getString("account");
|
||||||
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
|
self=session.self;
|
||||||
|
setTitle(R.string.bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onShown(){
|
||||||
|
super.onShown();
|
||||||
|
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count) {
|
||||||
|
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
|
||||||
|
currentRequest=b.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Status> result){
|
||||||
|
onDataLoaded(result, b.getMaxId()!=null);
|
||||||
|
lastMaxId=b.getMaxId();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import android.text.Layout;
|
|||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -53,17 +54,20 @@ import org.joinmastodon.android.MastodonApp;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.ProgressListener;
|
import org.joinmastodon.android.api.ProgressListener;
|
||||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
||||||
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
import org.joinmastodon.android.model.EmojiCategory;
|
import org.joinmastodon.android.model.EmojiCategory;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Mention;
|
import org.joinmastodon.android.model.Mention;
|
||||||
|
import org.joinmastodon.android.model.Poll;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.StatusPrivacy;
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
|
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
|
||||||
@@ -167,13 +171,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private ImageView sendError;
|
private ImageView sendError;
|
||||||
private View sendingOverlay;
|
private View sendingOverlay;
|
||||||
private WindowManager wm;
|
private WindowManager wm;
|
||||||
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
|
private StatusPrivacy statusVisibility=StatusPrivacy.UNLISTED;
|
||||||
private ComposeAutocompleteSpan currentAutocompleteSpan;
|
private ComposeAutocompleteSpan currentAutocompleteSpan;
|
||||||
private FrameLayout mainEditTextWrap;
|
private FrameLayout mainEditTextWrap;
|
||||||
private ComposeAutocompleteViewController autocompleteViewController;
|
private ComposeAutocompleteViewController autocompleteViewController;
|
||||||
private Instance instance;
|
private Instance instance;
|
||||||
private boolean attachmentsErrorShowing;
|
private boolean attachmentsErrorShowing;
|
||||||
|
|
||||||
|
private Status editingStatus;
|
||||||
|
private boolean pollChanged;
|
||||||
|
private boolean creatingView;
|
||||||
|
|
||||||
|
public static DraftMediaAttachment redraftAttachment(Attachment att) {
|
||||||
|
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||||
|
draft.serverAttachment=att;
|
||||||
|
draft.description=att.description;
|
||||||
|
draft.uri=Uri.parse(att.url);
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -185,6 +201,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
instanceDomain=session.domain;
|
instanceDomain=session.domain;
|
||||||
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
|
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
|
||||||
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
|
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
|
||||||
|
if(getArguments().containsKey("editStatus")){
|
||||||
|
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
|
||||||
|
}
|
||||||
if(instance==null){
|
if(instance==null){
|
||||||
Nav.finish(this);
|
Nav.finish(this);
|
||||||
return;
|
return;
|
||||||
@@ -204,6 +223,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||||
statusVisibility=replyTo.visibility;
|
statusVisibility=replyTo.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(getArguments().containsKey("visibility")){
|
||||||
|
statusVisibility=(StatusPrivacy) getArguments().getSerializable("visibility");
|
||||||
|
}
|
||||||
|
|
||||||
if(savedInstanceState!=null){
|
if(savedInstanceState!=null){
|
||||||
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||||
}
|
}
|
||||||
@@ -225,6 +249,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||||
|
creatingView=true;
|
||||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain);
|
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain);
|
||||||
emojiKeyboard.setListener(this::onCustomEmojiClick);
|
emojiKeyboard.setListener(this::onCustomEmojiClick);
|
||||||
|
|
||||||
@@ -286,16 +311,33 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
pollDurationView.setOnClickListener(v->showPollDurationMenu());
|
pollDurationView.setOnClickListener(v->showPollDurationMenu());
|
||||||
|
|
||||||
pollOptions.clear();
|
pollOptions.clear();
|
||||||
if(savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){
|
ArrayList<String> restoredPollOptions=(savedInstanceState!=null ? savedInstanceState : getArguments())
|
||||||
|
.getStringArrayList("pollOptions");
|
||||||
|
if(restoredPollOptions!=null){
|
||||||
|
if(savedInstanceState==null){
|
||||||
|
// restoring from arguments
|
||||||
|
pollDuration=getArguments().getInt("pollDuration");
|
||||||
|
pollDurationStr=DateUtils.formatElapsedTime(pollDuration); // getResources().getQuantityString(R.plurals.x_hours, pollDuration/3600);
|
||||||
|
}
|
||||||
pollBtn.setSelected(true);
|
pollBtn.setSelected(true);
|
||||||
mediaBtn.setEnabled(false);
|
mediaBtn.setEnabled(false);
|
||||||
pollWrap.setVisibility(View.VISIBLE);
|
pollWrap.setVisibility(View.VISIBLE);
|
||||||
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
|
for(String oldText:restoredPollOptions){
|
||||||
DraftPollOption opt=createDraftPollOption();
|
DraftPollOption opt=createDraftPollOption();
|
||||||
opt.edit.setText(oldText);
|
opt.edit.setText(oldText);
|
||||||
}
|
}
|
||||||
updatePollOptionHints();
|
updatePollOptionHints();
|
||||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
|
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
|
||||||
|
}else if(savedInstanceState==null && editingStatus!=null && editingStatus.poll!=null){
|
||||||
|
pollBtn.setSelected(true);
|
||||||
|
mediaBtn.setEnabled(false);
|
||||||
|
pollWrap.setVisibility(View.VISIBLE);
|
||||||
|
for(Poll.Option eopt:editingStatus.poll.options){
|
||||||
|
DraftPollOption opt=createDraftPollOption();
|
||||||
|
opt.edit.setText(eopt.title);
|
||||||
|
}
|
||||||
|
updatePollOptionHints();
|
||||||
|
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
|
||||||
}else{
|
}else{
|
||||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=getResources().getQuantityString(R.plurals.x_days, 1, 1)));
|
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=getResources().getQuantityString(R.plurals.x_days, 1, 1)));
|
||||||
}
|
}
|
||||||
@@ -308,10 +350,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
|
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
|
||||||
spoilerEdit.setVisibility(View.VISIBLE);
|
spoilerEdit.setVisibility(View.VISIBLE);
|
||||||
spoilerBtn.setSelected(true);
|
spoilerBtn.setSelected(true);
|
||||||
|
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
|
||||||
|
spoilerEdit.setVisibility(View.VISIBLE);
|
||||||
|
spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText));
|
||||||
|
spoilerBtn.setSelected(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
|
ArrayList<Parcelable> serializedAttachments=(savedInstanceState!=null ? savedInstanceState : getArguments())
|
||||||
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
|
.getParcelableArrayList("attachments");
|
||||||
|
if(serializedAttachments!=null){
|
||||||
for(Parcelable a:serializedAttachments){
|
for(Parcelable a:serializedAttachments){
|
||||||
DraftMediaAttachment att=Parcels.unwrap(a);
|
DraftMediaAttachment att=Parcels.unwrap(a);
|
||||||
attachmentsView.addView(createMediaAttachmentView(att));
|
attachmentsView.addView(createMediaAttachmentView(att));
|
||||||
@@ -332,6 +379,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
autocompleteView.setVisibility(View.GONE);
|
autocompleteView.setVisibility(View.GONE);
|
||||||
mainEditTextWrap.addView(autocompleteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(178), Gravity.TOP));
|
mainEditTextWrap.addView(autocompleteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(178), Gravity.TOP));
|
||||||
|
|
||||||
|
creatingView=false;
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,9 +482,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable s){
|
public void afterTextChanged(Editable s){
|
||||||
updateCharCounter(s);
|
updateCharCounter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
|
||||||
if(replyTo!=null){
|
if(replyTo!=null){
|
||||||
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
|
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
|
||||||
ArrayList<String> mentions=new ArrayList<>();
|
ArrayList<String> mentions=new ArrayList<>();
|
||||||
@@ -453,7 +503,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
mainEditText.setText(initialText);
|
mainEditText.setText(initialText);
|
||||||
mainEditText.setSelection(mainEditText.length());
|
mainEditText.setSelection(mainEditText.length());
|
||||||
if(!TextUtils.isEmpty(replyTo.spoilerText) && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)){
|
if(!TextUtils.isEmpty(replyTo.spoilerText)){
|
||||||
hasSpoiler=true;
|
hasSpoiler=true;
|
||||||
spoilerEdit.setVisibility(View.VISIBLE);
|
spoilerEdit.setVisibility(View.VISIBLE);
|
||||||
spoilerEdit.setText(replyTo.spoilerText);
|
spoilerEdit.setText(replyTo.spoilerText);
|
||||||
@@ -464,25 +514,47 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
replyText.setVisibility(View.GONE);
|
replyText.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
String prefilledText=getArguments().getString("prefilledText");
|
if(editingStatus!=null){
|
||||||
if(!TextUtils.isEmpty(prefilledText)){
|
initialText=getArguments().getString("sourceText", "");
|
||||||
mainEditText.setText(prefilledText);
|
mainEditText.setText(initialText);
|
||||||
mainEditText.setSelection(mainEditText.length());
|
mainEditText.setSelection(mainEditText.length());
|
||||||
initialText=prefilledText;
|
if(!editingStatus.mediaAttachments.isEmpty()){
|
||||||
}
|
attachmentsView.setVisibility(View.VISIBLE);
|
||||||
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
|
for(Attachment att:editingStatus.mediaAttachments){
|
||||||
if(mediaUris!=null && !mediaUris.isEmpty()){
|
DraftMediaAttachment da=new DraftMediaAttachment();
|
||||||
for(Uri uri:mediaUris){
|
da.serverAttachment=att;
|
||||||
addMediaAttachment(uri, null);
|
da.description=att.description;
|
||||||
|
da.uri=Uri.parse(att.previewUrl);
|
||||||
|
attachmentsView.addView(createMediaAttachmentView(da));
|
||||||
|
attachments.add(da);
|
||||||
|
}
|
||||||
|
pollBtn.setEnabled(false);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
String prefilledText=getArguments().getString("prefilledText");
|
||||||
|
if(!TextUtils.isEmpty(prefilledText)){
|
||||||
|
mainEditText.setText(prefilledText);
|
||||||
|
mainEditText.setSelection(mainEditText.length());
|
||||||
|
initialText=prefilledText;
|
||||||
|
}
|
||||||
|
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
|
||||||
|
if(mediaUris!=null && !mediaUris.isEmpty()){
|
||||||
|
for(Uri uri:mediaUris){
|
||||||
|
addMediaAttachment(uri, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(editingStatus!=null){
|
||||||
|
updateCharCounter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
publishButton=new Button(getActivity());
|
publishButton=new Button(getActivity());
|
||||||
publishButton.setText(R.string.publish);
|
publishButton.setText(editingStatus==null ? R.string.publish : R.string.save);
|
||||||
publishButton.setOnClickListener(this::onPublishClick);
|
publishButton.setOnClickListener(this::onPublishClick);
|
||||||
LinearLayout wrap=new LinearLayout(getActivity());
|
LinearLayout wrap=new LinearLayout(getActivity());
|
||||||
wrap.setOrientation(LinearLayout.HORIZONTAL);
|
wrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
@@ -505,7 +577,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||||
wrap.setClipToPadding(false);
|
wrap.setClipToPadding(false);
|
||||||
MenuItem item=menu.add(R.string.publish);
|
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
|
||||||
item.setActionView(wrap);
|
item.setActionView(wrap);
|
||||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
updatePublishButtonState();
|
updatePublishButtonState();
|
||||||
@@ -523,7 +595,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private void updateCharCounter(CharSequence text){
|
private void updateCharCounter(){
|
||||||
|
CharSequence text=mainEditText.getText();
|
||||||
|
|
||||||
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
||||||
MENTION_PATTERN.matcher(
|
MENTION_PATTERN.matcher(
|
||||||
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
||||||
@@ -535,6 +609,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
charCount++;
|
charCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(hasSpoiler){
|
||||||
|
charCount+=spoilerEdit.length();
|
||||||
|
}
|
||||||
charCounter.setText(String.valueOf(charLimit-charCount));
|
charCounter.setText(String.valueOf(charLimit-charCount));
|
||||||
trimmedCharCount=text.toString().trim().length();
|
trimmedCharCount=text.toString().trim().length();
|
||||||
updatePublishButtonState();
|
updatePublishButtonState();
|
||||||
@@ -604,39 +681,61 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
sendProgress.setVisibility(View.VISIBLE);
|
sendProgress.setVisibility(View.VISIBLE);
|
||||||
sendError.setVisibility(View.GONE);
|
sendError.setVisibility(View.GONE);
|
||||||
|
|
||||||
new CreateStatus(req, uuid)
|
Callback<Status> resCallback=new Callback<>(){
|
||||||
.setCallback(new Callback<>(){
|
@Override
|
||||||
@Override
|
public void onSuccess(Status result){
|
||||||
public void onSuccess(Status result){
|
wm.removeView(sendingOverlay);
|
||||||
wm.removeView(sendingOverlay);
|
sendingOverlay=null;
|
||||||
sendingOverlay=null;
|
if(editingStatus==null){
|
||||||
Nav.finish(ComposeFragment.this);
|
E.post(new StatusCreatedEvent(result));
|
||||||
E.post(new StatusCreatedEvent(result));
|
if(replyTo!=null){
|
||||||
if(replyTo!=null){
|
replyTo.repliesCount++;
|
||||||
replyTo.repliesCount++;
|
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}else{
|
||||||
|
E.post(new StatusUpdatedEvent(result));
|
||||||
|
}
|
||||||
|
Nav.finish(ComposeFragment.this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
wm.removeView(sendingOverlay);
|
wm.removeView(sendingOverlay);
|
||||||
sendingOverlay=null;
|
sendingOverlay=null;
|
||||||
sendProgress.setVisibility(View.GONE);
|
sendProgress.setVisibility(View.GONE);
|
||||||
sendError.setVisibility(View.VISIBLE);
|
sendError.setVisibility(View.VISIBLE);
|
||||||
publishButton.setEnabled(true);
|
publishButton.setEnabled(true);
|
||||||
error.showToast(getActivity());
|
error.showToast(getActivity());
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
.exec(accountID);
|
|
||||||
|
if(editingStatus!=null){
|
||||||
|
new EditStatus(req, editingStatus.id)
|
||||||
|
.setCallback(resCallback)
|
||||||
|
.exec(accountID);
|
||||||
|
}else{
|
||||||
|
new CreateStatus(req, uuid)
|
||||||
|
.setCallback(resCallback)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasDraft(){
|
private boolean hasDraft(){
|
||||||
|
if(editingStatus!=null){
|
||||||
|
if(!mainEditText.getText().toString().equals(initialText))
|
||||||
|
return true;
|
||||||
|
List<String> existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList());
|
||||||
|
if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList())))
|
||||||
|
return true;
|
||||||
|
return pollChanged;
|
||||||
|
}
|
||||||
boolean pollFieldsHaveContent=false;
|
boolean pollFieldsHaveContent=false;
|
||||||
for(DraftPollOption opt:pollOptions)
|
for(DraftPollOption opt:pollOptions)
|
||||||
pollFieldsHaveContent|=opt.edit.length()>0;
|
pollFieldsHaveContent|=opt.edit.length()>0;
|
||||||
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty()
|
return getArguments().getBoolean("hasDraft", false)
|
||||||
|| uploadingAttachment!=null || !queuedAttachments.isEmpty() || !failedAttachments.isEmpty() || pollFieldsHaveContent;
|
|| (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText))
|
||||||
|
|| !attachments.isEmpty() || uploadingAttachment!=null || !queuedAttachments.isEmpty()
|
||||||
|
|| !failedAttachments.isEmpty() || pollFieldsHaveContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -680,7 +779,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
private void confirmDiscardDraftAndFinish(){
|
private void confirmDiscardDraftAndFinish(){
|
||||||
new M3AlertDialogBuilder(getActivity())
|
new M3AlertDialogBuilder(getActivity())
|
||||||
.setTitle(R.string.discard_draft)
|
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
|
||||||
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
|
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
@@ -960,7 +1059,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
pollOptionsView.startDragging(option.view);
|
pollOptionsView.startDragging(option.view);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
option.edit.addTextChangedListener(new SimpleTextWatcher(e->updatePublishButtonState()));
|
option.edit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||||
|
if(!creatingView)
|
||||||
|
pollChanged=true;
|
||||||
|
updatePublishButtonState();
|
||||||
|
}));
|
||||||
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
|
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
|
||||||
|
|
||||||
pollOptionsView.addView(option.view);
|
pollOptionsView.addView(option.view);
|
||||||
@@ -980,6 +1083,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private void onSwapPollOptions(int oldIndex, int newIndex){
|
private void onSwapPollOptions(int oldIndex, int newIndex){
|
||||||
pollOptions.add(newIndex, pollOptions.remove(oldIndex));
|
pollOptions.add(newIndex, pollOptions.remove(oldIndex));
|
||||||
updatePollOptionHints();
|
updatePollOptionHints();
|
||||||
|
pollChanged=true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPollDurationMenu(){
|
private void showPollDurationMenu(){
|
||||||
@@ -1003,6 +1107,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
default -> throw new IllegalStateException("Unexpected value: "+item.getItemId());
|
default -> throw new IllegalStateException("Unexpected value: "+item.getItemId());
|
||||||
};
|
};
|
||||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString()));
|
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString()));
|
||||||
|
pollChanged=true;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
menu.show();
|
menu.show();
|
||||||
@@ -1019,6 +1124,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
spoilerEdit.setText("");
|
spoilerEdit.setText("");
|
||||||
spoilerBtn.setSelected(false);
|
spoilerBtn.setSelected(false);
|
||||||
mainEditText.requestFocus();
|
mainEditText.requestFocus();
|
||||||
|
updateCharCounter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,7 +1139,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
UiUtils.enablePopupMenuIcons(getActivity(), menu);
|
UiUtils.enablePopupMenuIcons(getActivity(), menu);
|
||||||
m.setGroupCheckable(0, true, true);
|
m.setGroupCheckable(0, true, true);
|
||||||
m.findItem(switch(statusVisibility){
|
m.findItem(switch(statusVisibility){
|
||||||
case PUBLIC, UNLISTED -> R.id.vis_public;
|
case PUBLIC -> R.id.vis_public;
|
||||||
|
case UNLISTED -> R.id.vis_unlisted;
|
||||||
case PRIVATE -> R.id.vis_followers;
|
case PRIVATE -> R.id.vis_followers;
|
||||||
case DIRECT -> R.id.vis_private;
|
case DIRECT -> R.id.vis_private;
|
||||||
}).setChecked(true);
|
}).setChecked(true);
|
||||||
@@ -1043,6 +1150,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
int id=item.getItemId();
|
int id=item.getItemId();
|
||||||
if(id==R.id.vis_public){
|
if(id==R.id.vis_public){
|
||||||
statusVisibility=StatusPrivacy.PUBLIC;
|
statusVisibility=StatusPrivacy.PUBLIC;
|
||||||
|
}else if(id==R.id.vis_unlisted){
|
||||||
|
statusVisibility=StatusPrivacy.UNLISTED;
|
||||||
}else if(id==R.id.vis_followers){
|
}else if(id==R.id.vis_followers){
|
||||||
statusVisibility=StatusPrivacy.PRIVATE;
|
statusVisibility=StatusPrivacy.PRIVATE;
|
||||||
}else if(id==R.id.vis_private){
|
}else if(id==R.id.vis_private){
|
||||||
|
|||||||
@@ -255,9 +255,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
@Override
|
@Override
|
||||||
public boolean onBackPressed(){
|
public boolean onBackPressed(){
|
||||||
if(currentTab==R.id.tab_profile)
|
if(currentTab==R.id.tab_profile)
|
||||||
return profileFragment.onBackPressed();
|
if (profileFragment.onBackPressed()) return true;
|
||||||
if(currentTab==R.id.tab_search)
|
if(currentTab==R.id.tab_search)
|
||||||
return searchFragment.onBackPressed();
|
if (searchFragment.onBackPressed()) return true;
|
||||||
|
if (currentTab!=R.id.tab_home) {
|
||||||
|
tabBar.selectTab(R.id.tab_home);
|
||||||
|
onTabSelected(R.id.tab_home);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -16,15 +12,12 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
|||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Poll;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
|
||||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -33,7 +26,6 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
@@ -160,91 +152,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
||||||
private int bgColor=UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground);
|
|
||||||
private int borderColor=UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted);
|
|
||||||
private RectF rect=new RectF();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
|
||||||
int pos=0;
|
|
||||||
for(int i=0;i<parent.getChildCount();i++){
|
|
||||||
View child=parent.getChildAt(i);
|
|
||||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
|
||||||
pos=holder.getAbsoluteAdapterPosition();
|
|
||||||
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
|
||||||
if(inset){
|
|
||||||
if(rect.isEmpty()){
|
|
||||||
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
|
||||||
}else{
|
|
||||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
|
||||||
}
|
|
||||||
}else if(!rect.isEmpty()){
|
|
||||||
drawInsetBackground(c);
|
|
||||||
rect.setEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!rect.isEmpty()){
|
|
||||||
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
|
|
||||||
rect.bottom=parent.getHeight()+V.dp(10);
|
|
||||||
}
|
|
||||||
drawInsetBackground(c);
|
|
||||||
rect.setEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawInsetBackground(Canvas c){
|
|
||||||
paint.setStyle(Paint.Style.FILL);
|
|
||||||
paint.setColor(bgColor);
|
|
||||||
rect.left=V.dp(12);
|
|
||||||
rect.right=list.getWidth()-V.dp(12);
|
|
||||||
rect.inset(V.dp(4), V.dp(4));
|
|
||||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setStrokeWidth(V.dp(1));
|
|
||||||
paint.setColor(borderColor);
|
|
||||||
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
|
|
||||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
|
||||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
|
||||||
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
|
||||||
boolean inset=sdi.getItem().inset;
|
|
||||||
int pos=holder.getAbsoluteAdapterPosition();
|
|
||||||
if(inset){
|
|
||||||
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
|
||||||
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
|
||||||
int pad;
|
|
||||||
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
|
||||||
pad=V.dp(16);
|
|
||||||
else
|
|
||||||
pad=V.dp(12);
|
|
||||||
boolean insetLeft=true, insetRight=true;
|
|
||||||
if(holder instanceof ImageStatusDisplayItem.Holder<?> img){
|
|
||||||
PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout;
|
|
||||||
PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile;
|
|
||||||
// only inset those items that are on the edges of the layout
|
|
||||||
insetLeft=tile.startCol==0;
|
|
||||||
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
|
|
||||||
// inset all items in the bottom row
|
|
||||||
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
|
|
||||||
bottomSiblingInset=false;
|
|
||||||
}
|
|
||||||
if(insetLeft)
|
|
||||||
outRect.left=pad;
|
|
||||||
if(insetRight)
|
|
||||||
outRect.right=pad;
|
|
||||||
if(!topSiblingInset)
|
|
||||||
outRect.top=pad;
|
|
||||||
if(!bottomSiblingInset)
|
|
||||||
outRect.bottom=pad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Notification getNotificationByID(String id){
|
private Notification getNotificationByID(String id){
|
||||||
@@ -268,4 +176,5 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private ProgressBarButton actionButton;
|
private ProgressBarButton actionButton;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
private NestedRecyclerScrollView scrollView;
|
private NestedRecyclerScrollView scrollView;
|
||||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
|
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
|
||||||
private ProfileAboutFragment aboutFragment;
|
private ProfileAboutFragment aboutFragment;
|
||||||
private TabLayout tabbar;
|
private TabLayout tabbar;
|
||||||
private SwipeRefreshLayout refreshLayout;
|
private SwipeRefreshLayout refreshLayout;
|
||||||
@@ -209,14 +209,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tabViews=new FrameLayout[4];
|
tabViews=new FrameLayout[5];
|
||||||
for(int i=0;i<tabViews.length;i++){
|
for(int i=0;i<tabViews.length;i++){
|
||||||
FrameLayout tabView=new FrameLayout(getActivity());
|
FrameLayout tabView=new FrameLayout(getActivity());
|
||||||
tabView.setId(switch(i){
|
tabView.setId(switch(i){
|
||||||
case 0 -> R.id.profile_posts;
|
case 0 -> R.id.profile_posts;
|
||||||
case 1 -> R.id.profile_posts_with_replies;
|
case 1 -> R.id.profile_posts_with_replies;
|
||||||
case 2 -> R.id.profile_media;
|
case 2 -> R.id.profile_pinned_posts;
|
||||||
case 3 -> R.id.profile_about;
|
case 3 -> R.id.profile_media;
|
||||||
|
case 4 -> R.id.profile_about;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||||
});
|
});
|
||||||
tabView.setVisibility(View.GONE);
|
tabView.setVisibility(View.GONE);
|
||||||
@@ -224,7 +225,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
tabViews[i]=tabView;
|
tabViews[i]=tabView;
|
||||||
}
|
}
|
||||||
|
|
||||||
pager.setOffscreenPageLimit(4);
|
pager.setOffscreenPageLimit(5);
|
||||||
pager.setAdapter(new ProfilePagerAdapter());
|
pager.setAdapter(new ProfilePagerAdapter());
|
||||||
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
||||||
|
|
||||||
@@ -240,8 +241,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
tab.setText(switch(position){
|
tab.setText(switch(position){
|
||||||
case 0 -> R.string.posts;
|
case 0 -> R.string.posts;
|
||||||
case 1 -> R.string.posts_and_replies;
|
case 1 -> R.string.posts_and_replies;
|
||||||
case 2 -> R.string.media;
|
case 2 -> R.string.pinned_posts;
|
||||||
case 3 -> R.string.profile_about;
|
case 3 -> R.string.media;
|
||||||
|
case 4 -> R.string.profile_about;
|
||||||
default -> throw new IllegalStateException();
|
default -> throw new IllegalStateException();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -298,6 +300,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
postsFragment.onRefresh();
|
postsFragment.onRefresh();
|
||||||
if(postsWithRepliesFragment.loaded)
|
if(postsWithRepliesFragment.loaded)
|
||||||
postsWithRepliesFragment.onRefresh();
|
postsWithRepliesFragment.onRefresh();
|
||||||
|
if(pinnedPostsFragment.loaded)
|
||||||
|
pinnedPostsFragment.onRefresh();
|
||||||
if(mediaFragment.loaded)
|
if(mediaFragment.loaded)
|
||||||
mediaFragment.onRefresh();
|
mediaFragment.onRefresh();
|
||||||
}
|
}
|
||||||
@@ -322,6 +326,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if(postsFragment==null){
|
if(postsFragment==null){
|
||||||
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
||||||
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
||||||
|
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
|
||||||
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
||||||
aboutFragment=new ProfileAboutFragment();
|
aboutFragment=new ProfileAboutFragment();
|
||||||
aboutFragment.setFields(fields);
|
aboutFragment.setFields(fields);
|
||||||
@@ -402,6 +407,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
|
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
|
||||||
postsFragment.onApplyWindowInsets(childInsets);
|
postsFragment.onApplyWindowInsets(childInsets);
|
||||||
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
|
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
|
||||||
|
pinnedPostsFragment.onApplyWindowInsets(childInsets);
|
||||||
mediaFragment.onApplyWindowInsets(childInsets);
|
mediaFragment.onApplyWindowInsets(childInsets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,10 +515,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(relationship==null)
|
if(relationship==null && !isOwnProfile)
|
||||||
return;
|
return;
|
||||||
inflater.inflate(R.menu.profile, menu);
|
inflater.inflate(R.menu.profile, menu);
|
||||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||||
|
if(isOwnProfile){
|
||||||
|
for(int i=0;i<menu.size();i++){
|
||||||
|
MenuItem item=menu.getItem(i);
|
||||||
|
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||||
@@ -529,11 +542,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item){
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
int id=item.getItemId();
|
int id=item.getItemId();
|
||||||
if(id==R.id.share){
|
if(id==R.id.share) {
|
||||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
intent.setType("text/plain");
|
intent.setType("text/plain");
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||||
|
}else if(id==R.id.bookmarks) {
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||||
|
Nav.go(getActivity(), BookmarksListFragment.class, args);
|
||||||
}else if(id==R.id.mute){
|
}else if(id==R.id.mute){
|
||||||
confirmToggleMuted();
|
confirmToggleMuted();
|
||||||
}else if(id==R.id.block){
|
}else if(id==R.id.block){
|
||||||
@@ -637,8 +655,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return switch(page){
|
return switch(page){
|
||||||
case 0 -> postsFragment;
|
case 0 -> postsFragment;
|
||||||
case 1 -> postsWithRepliesFragment;
|
case 1 -> postsWithRepliesFragment;
|
||||||
case 2 -> mediaFragment;
|
case 2 -> pinnedPostsFragment;
|
||||||
case 3 -> aboutFragment;
|
case 3 -> mediaFragment;
|
||||||
|
case 4 -> aboutFragment;
|
||||||
default -> throw new IllegalStateException();
|
default -> throw new IllegalStateException();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -699,9 +718,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
pager.setUserInputEnabled(false);
|
pager.setUserInputEnabled(false);
|
||||||
actionButton.setText(R.string.done);
|
actionButton.setText(R.string.done);
|
||||||
pager.setCurrentItem(3);
|
pager.setCurrentItem(4);
|
||||||
ArrayList<Animator> animators=new ArrayList<>();
|
ArrayList<Animator> animators=new ArrayList<>();
|
||||||
for(int i=0;i<3;i++){
|
for(int i=0;i<tabViews.length-1;i++){
|
||||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
|
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
|
||||||
tabbar.getTabAt(i).view.setEnabled(false);
|
tabbar.getTabAt(i).view.setEnabled(false);
|
||||||
}
|
}
|
||||||
@@ -742,7 +761,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
ArrayList<Animator> animators=new ArrayList<>();
|
ArrayList<Animator> animators=new ArrayList<>();
|
||||||
actionButton.setText(R.string.edit_profile);
|
actionButton.setText(R.string.edit_profile);
|
||||||
for(int i=0;i<3;i++){
|
for(int i=0;i<tabViews.length-1;i++){
|
||||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
|
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
|
||||||
}
|
}
|
||||||
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
|
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
|
||||||
@@ -760,7 +779,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
set.addListener(new AnimatorListenerAdapter(){
|
set.addListener(new AnimatorListenerAdapter(){
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animator animation){
|
public void onAnimationEnd(Animator animation){
|
||||||
for(int i=0;i<3;i++){
|
for(int i=0;i<tabViews.length-1;i++){
|
||||||
tabbar.getTabAt(i).view.setEnabled(true);
|
tabbar.getTabAt(i).view.setEnabled(true);
|
||||||
}
|
}
|
||||||
pager.setUserInputEnabled(true);
|
pager.setUserInputEnabled(true);
|
||||||
@@ -937,7 +956,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount(){
|
public int getItemCount(){
|
||||||
return loaded ? 4 : 0;
|
return loaded ? tabViews.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
id=getArguments().getString("id");
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Activity activity){
|
||||||
|
super.onAttach(activity);
|
||||||
|
setTitle(R.string.edit_history);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
new GetStatusEditHistory(id)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Status> result){
|
||||||
|
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||||
|
onDataLoaded(result, false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
|
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
|
||||||
|
int idx=data.indexOf(s);
|
||||||
|
if(idx>=0){
|
||||||
|
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||||
|
String action="";
|
||||||
|
if(idx==data.size()-1){
|
||||||
|
action=getString(R.string.edit_original_post);
|
||||||
|
}else{
|
||||||
|
enum StatusEditChangeType{
|
||||||
|
TEXT_CHANGED,
|
||||||
|
SPOILER_ADDED,
|
||||||
|
SPOILER_REMOVED,
|
||||||
|
SPOILER_CHANGED,
|
||||||
|
POLL_ADDED,
|
||||||
|
POLL_REMOVED,
|
||||||
|
POLL_CHANGED,
|
||||||
|
MEDIA_ADDED,
|
||||||
|
MEDIA_REMOVED,
|
||||||
|
MEDIA_REORDERED,
|
||||||
|
MARKED_SENSITIVE,
|
||||||
|
MARKED_NOT_SENSITIVE
|
||||||
|
}
|
||||||
|
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
|
||||||
|
Status prev=data.get(idx+1);
|
||||||
|
|
||||||
|
if(!Objects.equals(s.content, prev.content)){
|
||||||
|
changes.add(StatusEditChangeType.TEXT_CHANGED);
|
||||||
|
}
|
||||||
|
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
|
||||||
|
if(s.spoilerText==null){
|
||||||
|
changes.add(StatusEditChangeType.SPOILER_REMOVED);
|
||||||
|
}else if(prev.spoilerText==null){
|
||||||
|
changes.add(StatusEditChangeType.SPOILER_ADDED);
|
||||||
|
}else{
|
||||||
|
changes.add(StatusEditChangeType.SPOILER_CHANGED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(s.poll!=null || prev.poll!=null){
|
||||||
|
if(s.poll==null){
|
||||||
|
changes.add(StatusEditChangeType.POLL_REMOVED);
|
||||||
|
}else if(prev.poll==null){
|
||||||
|
changes.add(StatusEditChangeType.POLL_ADDED);
|
||||||
|
}else if(!s.poll.id.equals(prev.poll.id)){
|
||||||
|
changes.add(StatusEditChangeType.POLL_CHANGED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
|
||||||
|
List<String> prevAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
|
||||||
|
boolean addedOrRemoved=false;
|
||||||
|
if(!newAttachmentIDs.containsAll(prevAttachmentIDs)){
|
||||||
|
changes.add(StatusEditChangeType.MEDIA_REMOVED);
|
||||||
|
addedOrRemoved=true;
|
||||||
|
}
|
||||||
|
if(!prevAttachmentIDs.containsAll(newAttachmentIDs)){
|
||||||
|
changes.add(StatusEditChangeType.MEDIA_ADDED);
|
||||||
|
addedOrRemoved=true;
|
||||||
|
}
|
||||||
|
if(!addedOrRemoved && !newAttachmentIDs.equals(prevAttachmentIDs)){
|
||||||
|
changes.add(StatusEditChangeType.MEDIA_REORDERED);
|
||||||
|
}
|
||||||
|
if(s.sensitive && !prev.sensitive){
|
||||||
|
changes.add(StatusEditChangeType.MARKED_SENSITIVE);
|
||||||
|
}else if(prev.sensitive && !s.sensitive){
|
||||||
|
changes.add(StatusEditChangeType.MARKED_NOT_SENSITIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(changes.size()==1){
|
||||||
|
action=getString(switch(changes.iterator().next()){
|
||||||
|
case TEXT_CHANGED -> R.string.edit_text_edited;
|
||||||
|
case SPOILER_ADDED -> R.string.edit_spoiler_added;
|
||||||
|
case SPOILER_REMOVED -> R.string.edit_spoiler_removed;
|
||||||
|
case SPOILER_CHANGED -> R.string.edit_spoiler_edited;
|
||||||
|
case POLL_ADDED -> R.string.edit_poll_added;
|
||||||
|
case POLL_REMOVED -> R.string.edit_poll_removed;
|
||||||
|
case POLL_CHANGED -> R.string.edit_poll_edited;
|
||||||
|
case MEDIA_ADDED -> R.string.edit_media_added;
|
||||||
|
case MEDIA_REMOVED -> R.string.edit_media_removed;
|
||||||
|
case MEDIA_REORDERED -> R.string.edit_media_reordered;
|
||||||
|
case MARKED_SENSITIVE -> R.string.edit_marked_sensitive;
|
||||||
|
case MARKED_NOT_SENSITIVE -> R.string.edit_marked_not_sensitive;
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
action=getString(R.string.edit_multiple_changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isItemEnabled(String id){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,15 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
|
|||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@@ -113,10 +115,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
return;
|
return;
|
||||||
data.remove(status);
|
data.remove(status);
|
||||||
preloadedData.remove(status);
|
preloadedData.remove(status);
|
||||||
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
|
int index=-1;
|
||||||
if(item==null)
|
for(int i=0;i<displayItems.size();i++){
|
||||||
|
if(ev.id.equals(displayItems.get(i).parentID)){
|
||||||
|
index=i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(index==-1)
|
||||||
return;
|
return;
|
||||||
int index=displayItems.indexOf(item);
|
|
||||||
int lastIndex;
|
int lastIndex;
|
||||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class ThreadFragment extends StatusListFragment{
|
|||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||||
if(s==mainStatus){
|
if(s.id.equals(mainStatus.id)){
|
||||||
for(StatusDisplayItem item:items){
|
for(StatusDisplayItem item:items){
|
||||||
if(item instanceof TextStatusDisplayItem text)
|
if(item instanceof TextStatusDisplayItem text)
|
||||||
text.textSelectable=true;
|
text.textSelectable=true;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
private DiscoverAccountsFragment accountsFragment;
|
private DiscoverAccountsFragment accountsFragment;
|
||||||
private SearchFragment searchFragment;
|
private SearchFragment searchFragment;
|
||||||
private LocalTimelineFragment localTimelineFragment;
|
private LocalTimelineFragment localTimelineFragment;
|
||||||
|
private FederatedTimelineFragment federatedTimelineFragment;
|
||||||
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||||
@@ -72,15 +73,16 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
tabLayout=view.findViewById(R.id.tabbar);
|
tabLayout=view.findViewById(R.id.tabbar);
|
||||||
pager=view.findViewById(R.id.pager);
|
pager=view.findViewById(R.id.pager);
|
||||||
|
|
||||||
tabViews=new FrameLayout[5];
|
tabViews=new FrameLayout[6];
|
||||||
for(int i=0;i<tabViews.length;i++){
|
for(int i=0;i<tabViews.length;i++){
|
||||||
FrameLayout tabView=new FrameLayout(getActivity());
|
FrameLayout tabView=new FrameLayout(getActivity());
|
||||||
tabView.setId(switch(i){
|
tabView.setId(switch(i){
|
||||||
case 0 -> R.id.discover_posts;
|
case 0 -> R.id.discover_local_timeline;
|
||||||
case 1 -> R.id.discover_hashtags;
|
case 1 -> R.id.discover_federated_timeline;
|
||||||
case 2 -> R.id.discover_news;
|
case 2 -> R.id.discover_hashtags;
|
||||||
case 3 -> R.id.discover_local_timeline;
|
case 3 -> R.id.discover_posts;
|
||||||
case 4 -> R.id.discover_users;
|
case 4 -> R.id.discover_news;
|
||||||
|
case 5 -> R.id.discover_users;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||||
});
|
});
|
||||||
tabView.setVisibility(View.GONE);
|
tabView.setVisibility(View.GONE);
|
||||||
@@ -106,7 +108,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(postsFragment==null){
|
if(localTimelineFragment==null){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putBoolean("__is_tab", true);
|
args.putBoolean("__is_tab", true);
|
||||||
@@ -126,9 +128,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
localTimelineFragment=new LocalTimelineFragment();
|
localTimelineFragment=new LocalTimelineFragment();
|
||||||
localTimelineFragment.setArguments(args);
|
localTimelineFragment.setArguments(args);
|
||||||
|
|
||||||
|
federatedTimelineFragment=new FederatedTimelineFragment();
|
||||||
|
federatedTimelineFragment.setArguments(args);
|
||||||
|
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.add(R.id.discover_posts, postsFragment)
|
.add(R.id.discover_posts, postsFragment)
|
||||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||||
|
.add(R.id.discover_federated_timeline, federatedTimelineFragment)
|
||||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||||
.add(R.id.discover_news, newsFragment)
|
.add(R.id.discover_news, newsFragment)
|
||||||
.add(R.id.discover_users, accountsFragment)
|
.add(R.id.discover_users, accountsFragment)
|
||||||
@@ -139,17 +145,30 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
@Override
|
@Override
|
||||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||||
tab.setText(switch(position){
|
tab.setText(switch(position){
|
||||||
case 0 -> R.string.posts;
|
case 0 -> R.string.local_timeline;
|
||||||
case 1 -> R.string.hashtags;
|
case 1 -> R.string.federated_timeline;
|
||||||
case 2 -> R.string.news;
|
case 2 -> R.string.hashtags;
|
||||||
case 3 -> R.string.local_timeline;
|
case 3 -> R.string.posts;
|
||||||
case 4 -> R.string.for_you;
|
case 4 -> R.string.news;
|
||||||
|
case 5 -> R.string.for_you;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||||
});
|
});
|
||||||
tab.view.textView.setAllCaps(true);
|
tab.view.textView.setAllCaps(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tabLayoutMediator.attach();
|
tabLayoutMediator.attach();
|
||||||
|
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||||
|
@Override
|
||||||
|
public void onTabSelected(TabLayout.Tab tab){}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTabUnselected(TabLayout.Tab tab){}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTabReselected(TabLayout.Tab tab){
|
||||||
|
scrollToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
searchEdit=view.findViewById(R.id.search_edit);
|
searchEdit=view.findViewById(R.id.search_edit);
|
||||||
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
|
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
|
||||||
@@ -217,8 +236,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void loadData(){
|
public void loadData(){
|
||||||
if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading)
|
if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
|
||||||
postsFragment.loadData();
|
localTimelineFragment.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSearchEditFocusChanged(View v, boolean hasFocus){
|
private void onSearchEditFocusChanged(View v, boolean hasFocus){
|
||||||
@@ -254,11 +273,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
|
|
||||||
private Fragment getFragmentForPage(int page){
|
private Fragment getFragmentForPage(int page){
|
||||||
return switch(page){
|
return switch(page){
|
||||||
case 0 -> postsFragment;
|
case 0 -> localTimelineFragment;
|
||||||
case 1 -> hashtagsFragment;
|
case 1 -> federatedTimelineFragment;
|
||||||
case 2 -> newsFragment;
|
case 2 -> hashtagsFragment;
|
||||||
case 3 -> localTimelineFragment;
|
case 3 -> postsFragment;
|
||||||
case 4 -> accountsFragment;
|
case 4 -> newsFragment;
|
||||||
|
case 5 -> accountsFragment;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||||
|
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||||
|
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class FederatedTimelineFragment extends StatusListFragment{
|
||||||
|
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
|
||||||
|
private String maxID;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Status> result){
|
||||||
|
if(!result.isEmpty())
|
||||||
|
maxID=result.get(result.size()-1).id;
|
||||||
|
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
bannerHelper.maybeAddBanner(contentWrap);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -205,7 +205,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabReselected(TabLayout.Tab tab){
|
public void onTabReselected(TabLayout.Tab tab){
|
||||||
|
scrollToTop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.NO_REBLOGS)
|
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||||||
public int reblogsCount;
|
public int reblogsCount;
|
||||||
public int favouritesCount;
|
public int favouritesCount;
|
||||||
public int repliesCount;
|
public int repliesCount;
|
||||||
|
public Instant editedAt;
|
||||||
|
|
||||||
public String url;
|
public String url;
|
||||||
public String inReplyToId;
|
public String inReplyToId;
|
||||||
@@ -126,6 +127,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||||||
repliesCount=ev.replies;
|
repliesCount=ev.replies;
|
||||||
favourited=ev.favorited;
|
favourited=ev.favorited;
|
||||||
reblogged=ev.reblogged;
|
reblogged=ev.reblogged;
|
||||||
|
pinned=ev.pinned;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Status getContentStatus(){
|
public Status getContentStatus(){
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ public class ComposeAutocompleteViewController{
|
|||||||
.map(WrappedEmoji::new)
|
.map(WrappedEmoji::new)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
|
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
|
||||||
|
imgLoader.updateImages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +187,7 @@ public class ComposeAutocompleteViewController{
|
|||||||
List<WrappedAccount> oldList=users;
|
List<WrappedAccount> oldList=users;
|
||||||
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
|
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||||
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
|
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
|
||||||
|
imgLoader.updateImages();
|
||||||
if(listIsHidden){
|
if(listIsHidden){
|
||||||
listIsHidden=false;
|
listIsHidden=false;
|
||||||
V.setVisibilityAnimated(list, View.VISIBLE);
|
V.setVisibilityAnimated(list, View.VISIBLE);
|
||||||
@@ -210,6 +212,7 @@ public class ComposeAutocompleteViewController{
|
|||||||
List<Hashtag> oldList=hashtags;
|
List<Hashtag> oldList=hashtags;
|
||||||
hashtags=result.hashtags;
|
hashtags=result.hashtags;
|
||||||
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
|
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
|
||||||
|
imgLoader.updateImages();
|
||||||
if(listIsHidden){
|
if(listIsHidden){
|
||||||
listIsHidden=false;
|
listIsHidden=false;
|
||||||
V.setVisibilityAnimated(list, View.VISIBLE);
|
V.setVisibilityAnimated(list, View.VISIBLE);
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package org.joinmastodon.android.ui;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.model.Attachment;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
import me.grishka.appkit.views.BottomSheet;
|
||||||
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
|
public class ImageDescriptionSheet extends BottomSheet{
|
||||||
|
private UsableRecyclerView list;
|
||||||
|
|
||||||
|
public ImageDescriptionSheet(@NonNull Activity activity, Attachment attachment){
|
||||||
|
super(activity);
|
||||||
|
|
||||||
|
View handleView=new View(activity);
|
||||||
|
handleView.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
|
||||||
|
ViewGroup handle=new FrameLayout(activity);
|
||||||
|
handle.addView(handleView);
|
||||||
|
handle.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
|
||||||
|
|
||||||
|
TextView textView = new TextView(activity);
|
||||||
|
if (attachment.description == null || attachment.description.isEmpty()) {
|
||||||
|
textView.setText(R.string.media_no_description);
|
||||||
|
textView.setTypeface(null, Typeface.ITALIC);
|
||||||
|
} else {
|
||||||
|
textView.setText(attachment.description);
|
||||||
|
textView.setTextIsSelectable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView heading=new TextView(activity);
|
||||||
|
heading.setText(R.string.image_description);
|
||||||
|
heading.setAllCaps(true);
|
||||||
|
heading.setTypeface(null, Typeface.BOLD);
|
||||||
|
heading.setPadding(0, V.dp(24), 0, V.dp(8));
|
||||||
|
|
||||||
|
LinearLayout linearLayout = new LinearLayout(activity);
|
||||||
|
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
linearLayout.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||||
|
linearLayout.addView(heading);
|
||||||
|
linearLayout.addView(textView);
|
||||||
|
|
||||||
|
FrameLayout layout=new FrameLayout(activity);
|
||||||
|
layout.addView(handle);
|
||||||
|
layout.addView(linearLayout);
|
||||||
|
|
||||||
|
list=new UsableRecyclerView(activity);
|
||||||
|
list.setLayoutManager(new LinearLayoutManager(activity));
|
||||||
|
list.setBackgroundResource(R.drawable.bg_bottom_sheet);
|
||||||
|
list.setAdapter(new SingleViewRecyclerAdapter(layout));
|
||||||
|
list.setClipToPadding(false);
|
||||||
|
|
||||||
|
setContentView(list);
|
||||||
|
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onWindowInsetsUpdated(WindowInsets insets){
|
||||||
|
if(Build.VERSION.SDK_INT>=29){
|
||||||
|
int tappableBottom=insets.getTappableElementInsets().bottom;
|
||||||
|
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||||
|
if(tappableBottom==0 && insetBottom>0){
|
||||||
|
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
|
||||||
|
}else{
|
||||||
|
list.setPadding(0, 0, 0, V.dp(24));
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
list.setPadding(0, 0, 0, V.dp(24));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,7 +92,9 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public void onBind(AudioStatusDisplayItem item){
|
public void onBind(AudioStatusDisplayItem item){
|
||||||
int seconds=(int)item.attachment.getDuration();
|
int seconds=(int)item.attachment.getDuration();
|
||||||
String duration=formatDuration(seconds);
|
String duration=formatDuration(seconds);
|
||||||
time.getLayoutParams().width=(int)Math.ceil(time.getPaint().measureText("-"+duration));
|
// Some fonts (not Roboto) have different-width digits. 0 is supposedly the widest.
|
||||||
|
time.getLayoutParams().width=(int)Math.ceil(Math.max(time.getPaint().measureText("-"+duration),
|
||||||
|
time.getPaint().measureText("-"+duration.replaceAll("\\d", "0"))));
|
||||||
time.setText(duration);
|
time.setText(duration);
|
||||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.ui.displayitems;
|
package org.joinmastodon.android.ui.displayitems;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
@@ -12,6 +13,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.fragments.StatusEditHistoryFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
|
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
|
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
|
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
|
||||||
@@ -43,39 +45,35 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<ExtendedFooterStatusDisplayItem>{
|
public static class Holder extends StatusDisplayItem.Holder<ExtendedFooterStatusDisplayItem>{
|
||||||
private final TextView reblogs, favorites, time;
|
private final TextView time, favoritesCount, reblogsCount, lastEditTime;
|
||||||
private final View buttonsView;
|
private final View favorites, reblogs, editHistory;
|
||||||
|
|
||||||
public Holder(Context context, ViewGroup parent){
|
public Holder(Context context, ViewGroup parent){
|
||||||
super(context, R.layout.display_item_extended_footer, parent);
|
super(context, R.layout.display_item_extended_footer, parent);
|
||||||
reblogs=findViewById(R.id.reblogs);
|
reblogs=findViewById(R.id.reblogs);
|
||||||
favorites=findViewById(R.id.favorites);
|
favorites=findViewById(R.id.favorites);
|
||||||
|
editHistory=findViewById(R.id.edit_history);
|
||||||
time=findViewById(R.id.timestamp);
|
time=findViewById(R.id.timestamp);
|
||||||
buttonsView=findViewById(R.id.button_bar);
|
favoritesCount=findViewById(R.id.favorites_count);
|
||||||
|
reblogsCount=findViewById(R.id.reblogs_count);
|
||||||
|
lastEditTime=findViewById(R.id.last_edited);
|
||||||
|
|
||||||
reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class));
|
reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class));
|
||||||
favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class));
|
favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class));
|
||||||
|
editHistory.setOnClickListener(v->startEditHistoryFragment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
@Override
|
@Override
|
||||||
public void onBind(ExtendedFooterStatusDisplayItem item){
|
public void onBind(ExtendedFooterStatusDisplayItem item){
|
||||||
Status s=item.status;
|
Status s=item.status;
|
||||||
if(s.favouritesCount>0){
|
favoritesCount.setText(String.format("%,d", s.favouritesCount));
|
||||||
favorites.setVisibility(View.VISIBLE);
|
reblogsCount.setText(String.format("%,d", s.reblogsCount));
|
||||||
favorites.setText(getFormattedPlural(R.plurals.x_favorites, s.favouritesCount));
|
if(s.editedAt!=null){
|
||||||
|
editHistory.setVisibility(View.VISIBLE);
|
||||||
|
lastEditTime.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt)));
|
||||||
}else{
|
}else{
|
||||||
favorites.setVisibility(View.GONE);
|
editHistory.setVisibility(View.GONE);
|
||||||
}
|
|
||||||
if(s.reblogsCount>0){
|
|
||||||
reblogs.setVisibility(View.VISIBLE);
|
|
||||||
reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, s.reblogsCount));
|
|
||||||
}else{
|
|
||||||
reblogs.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
if(s.favouritesCount==0 && s.reblogsCount==0){
|
|
||||||
buttonsView.setVisibility(View.GONE);
|
|
||||||
}else{
|
|
||||||
buttonsView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault()));
|
String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault()));
|
||||||
if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){
|
if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){
|
||||||
@@ -108,5 +106,12 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
args.putParcelable("status", Parcels.wrap(item.status));
|
args.putParcelable("status", Parcels.wrap(item.status));
|
||||||
Nav.go(item.parentFragment.getActivity(), cls, args);
|
Nav.go(item.parentFragment.getActivity(), cls, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startEditHistoryFragment(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", item.parentFragment.getAccountID());
|
||||||
|
args.putString("id", item.status.id);
|
||||||
|
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||||
private final TextView reply, boost, favorite;
|
private final TextView reply, boost, favorite, bookmark;
|
||||||
private final ImageView share;
|
private final ImageView share;
|
||||||
|
|
||||||
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
||||||
@@ -60,22 +60,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
reply=findViewById(R.id.reply);
|
reply=findViewById(R.id.reply);
|
||||||
boost=findViewById(R.id.boost);
|
boost=findViewById(R.id.boost);
|
||||||
favorite=findViewById(R.id.favorite);
|
favorite=findViewById(R.id.favorite);
|
||||||
|
bookmark=findViewById(R.id.bookmark);
|
||||||
share=findViewById(R.id.share);
|
share=findViewById(R.id.share);
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
||||||
|
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
|
||||||
}
|
}
|
||||||
View reply=findViewById(R.id.reply_btn);
|
View reply=findViewById(R.id.reply_btn);
|
||||||
View boost=findViewById(R.id.boost_btn);
|
View boost=findViewById(R.id.boost_btn);
|
||||||
View favorite=findViewById(R.id.favorite_btn);
|
View favorite=findViewById(R.id.favorite_btn);
|
||||||
View share=findViewById(R.id.share_btn);
|
View share=findViewById(R.id.share_btn);
|
||||||
|
View bookmark=findViewById(R.id.bookmark_btn);
|
||||||
reply.setOnClickListener(this::onReplyClick);
|
reply.setOnClickListener(this::onReplyClick);
|
||||||
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||||
boost.setOnClickListener(this::onBoostClick);
|
boost.setOnClickListener(this::onBoostClick);
|
||||||
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||||
favorite.setOnClickListener(this::onFavoriteClick);
|
favorite.setOnClickListener(this::onFavoriteClick);
|
||||||
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||||
|
bookmark.setOnClickListener(this::onBookmarkClick);
|
||||||
|
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||||
share.setOnClickListener(this::onShareClick);
|
share.setOnClickListener(this::onShareClick);
|
||||||
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||||
}
|
}
|
||||||
@@ -87,6 +92,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
bindButton(favorite, item.status.favouritesCount);
|
bindButton(favorite, item.status.favouritesCount);
|
||||||
boost.setSelected(item.status.reblogged);
|
boost.setSelected(item.status.reblogged);
|
||||||
favorite.setSelected(item.status.favourited);
|
favorite.setSelected(item.status.favourited);
|
||||||
|
bookmark.setSelected(item.status.bookmarked);
|
||||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|
||||||
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
|
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
|
||||||
}
|
}
|
||||||
@@ -120,6 +126,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
bindButton(favorite, item.status.favouritesCount);
|
bindButton(favorite, item.status.favouritesCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onBookmarkClick(View v){
|
||||||
|
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
|
||||||
|
bookmark.setSelected(item.status.bookmarked);
|
||||||
|
}
|
||||||
|
|
||||||
private void onShareClick(View v){
|
private void onShareClick(View v){
|
||||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||||
intent.setType("text/plain");
|
intent.setType("text/plain");
|
||||||
@@ -134,6 +145,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
return R.string.button_reblog;
|
return R.string.button_reblog;
|
||||||
if(id==R.id.favorite_btn)
|
if(id==R.id.favorite_btn)
|
||||||
return R.string.button_favorite;
|
return R.string.button_favorite;
|
||||||
|
if(id==R.id.bookmark_btn)
|
||||||
|
return R.string.button_bookmark;
|
||||||
if(id==R.id.share_btn)
|
if(id==R.id.share_btn)
|
||||||
return R.string.button_share;
|
return R.string.button_share;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import android.widget.Toast;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
@@ -135,8 +137,36 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||||
Account account=item.user;
|
Account account=item.user;
|
||||||
int id=menuItem.getItemId();
|
int id=menuItem.getItemId();
|
||||||
if(id==R.id.delete){
|
if(id==R.id.edit){
|
||||||
|
final Bundle args=new Bundle();
|
||||||
|
args.putString("account", item.parentFragment.getAccountID());
|
||||||
|
args.putParcelable("editStatus", Parcels.wrap(item.status));
|
||||||
|
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
||||||
|
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||||
|
}else{
|
||||||
|
new GetStatusSourceText(item.status.id)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(GetStatusSourceText.Response result){
|
||||||
|
args.putString("sourceText", result.text);
|
||||||
|
args.putString("sourceSpoiler", result.spoilerText);
|
||||||
|
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(item.parentFragment.getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(item.parentFragment.getActivity(), R.string.loading, true)
|
||||||
|
.exec(item.parentFragment.getAccountID());
|
||||||
|
}
|
||||||
|
}else if(id==R.id.delete){
|
||||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||||
|
}else if(id==R.id.delete_and_redraft) {
|
||||||
|
UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||||
|
}else if(id==R.id.pin || id==R.id.unpin){
|
||||||
|
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
|
||||||
}else if(id==R.id.mute){
|
}else if(id==R.id.mute){
|
||||||
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
|
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
|
||||||
}else if(id==R.id.block){
|
}else if(id==R.id.block){
|
||||||
@@ -175,7 +205,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public void onBind(HeaderStatusDisplayItem item){
|
public void onBind(HeaderStatusDisplayItem item){
|
||||||
name.setText(item.parsedName);
|
name.setText(item.parsedName);
|
||||||
username.setText('@'+item.user.acct);
|
username.setText('@'+item.user.acct);
|
||||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
if(item.status==null || item.status.editedAt==null)
|
||||||
|
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||||
|
else
|
||||||
|
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||||
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
||||||
if(item.hasVisibilityToggle){
|
if(item.hasVisibilityToggle){
|
||||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||||
@@ -249,7 +282,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
Account account=item.user;
|
Account account=item.user;
|
||||||
Menu menu=optionsMenu.getMenu();
|
Menu menu=optionsMenu.getMenu();
|
||||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||||
|
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
|
||||||
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
||||||
|
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
|
||||||
|
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
|
||||||
|
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
|
||||||
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
|
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
|
||||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||||
MenuItem mute=menu.findItem(R.id.mute);
|
MenuItem mute=menu.findItem(R.id.mute);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||||
@@ -114,7 +115,7 @@ public abstract class StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
if(addFooter){
|
if(addFooter){
|
||||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||||
if(status.hasGapAfter)
|
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
|
||||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||||
}
|
}
|
||||||
int i=1;
|
int i=1;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.widget.TextView;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||||
|
|
||||||
@@ -20,7 +21,8 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
|||||||
|
|
||||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||||
private CharSequence text;
|
private CharSequence text;
|
||||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), spoilerEmojiHelper;
|
||||||
|
private CharSequence parsedSpoilerText;
|
||||||
public boolean textSelectable;
|
public boolean textSelectable;
|
||||||
public final Status status;
|
public final Status status;
|
||||||
|
|
||||||
@@ -29,6 +31,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
this.text=text;
|
this.text=text;
|
||||||
this.status=status;
|
this.status=status;
|
||||||
emojiHelper.setText(text);
|
emojiHelper.setText(text);
|
||||||
|
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||||
|
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||||
|
spoilerEmojiHelper=new CustomEmojiHelper();
|
||||||
|
spoilerEmojiHelper.setText(parsedSpoilerText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,11 +45,15 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getImageCount(){
|
public int getImageCount(){
|
||||||
|
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||||
|
return spoilerEmojiHelper.getImageCount();
|
||||||
return emojiHelper.getImageCount();
|
return emojiHelper.getImageCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImageLoaderRequest getImageRequest(int index){
|
public ImageLoaderRequest getImageRequest(int index){
|
||||||
|
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||||
|
return spoilerEmojiHelper.getImageRequest(index);
|
||||||
return emojiHelper.getImageRequest(index);
|
return emojiHelper.getImageRequest(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +76,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
text.setTextIsSelectable(item.textSelectable);
|
text.setTextIsSelectable(item.textSelectable);
|
||||||
text.setInvalidateOnEveryFrame(false);
|
text.setInvalidateOnEveryFrame(false);
|
||||||
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
||||||
spoilerTitle.setText(item.status.spoilerText);
|
spoilerTitle.setText(item.parsedSpoilerText);
|
||||||
if(item.status.spoilerRevealed){
|
if(item.status.spoilerRevealed){
|
||||||
spoilerOverlay.setVisibility(View.GONE);
|
spoilerOverlay.setVisibility(View.GONE);
|
||||||
text.setVisibility(View.VISIBLE);
|
text.setVisibility(View.VISIBLE);
|
||||||
@@ -84,8 +95,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setImage(int index, Drawable image){
|
public void setImage(int index, Drawable image){
|
||||||
item.emojiHelper.setImageDrawable(index, image);
|
getEmojiHelper().setImageDrawable(index, image);
|
||||||
text.invalidate();
|
text.invalidate();
|
||||||
|
spoilerTitle.invalidate();
|
||||||
if(image instanceof Animatable){
|
if(image instanceof Animatable){
|
||||||
((Animatable) image).start();
|
((Animatable) image).start();
|
||||||
if(image instanceof MovieDrawable)
|
if(image instanceof MovieDrawable)
|
||||||
@@ -95,8 +107,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearImage(int index){
|
public void clearImage(int index){
|
||||||
item.emojiHelper.setImageDrawable(index, null);
|
getEmojiHelper().setImageDrawable(index, null);
|
||||||
text.invalidate();
|
text.invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CustomEmojiHelper getEmojiHelper(){
|
||||||
|
return item.spoilerEmojiHelper!=null && !item.status.spoilerRevealed ? item.spoilerEmojiHelper : item.emojiHelper;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.media.AudioManager;
|
|||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.opengl.Visibility;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
@@ -48,6 +49,7 @@ import android.widget.Toolbar;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
|
import org.joinmastodon.android.ui.ImageDescriptionSheet;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -97,6 +99,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|||||||
private TextView videoTimeView;
|
private TextView videoTimeView;
|
||||||
private ImageButton videoPlayPauseButton;
|
private ImageButton videoPlayPauseButton;
|
||||||
private View videoControls;
|
private View videoControls;
|
||||||
|
private MenuItem imageDescriptionButton;
|
||||||
private boolean uiVisible=true;
|
private boolean uiVisible=true;
|
||||||
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
|
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
|
||||||
private Runnable uiAutoHider=()->{
|
private Runnable uiAutoHider=()->{
|
||||||
@@ -174,11 +177,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|||||||
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
|
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
|
||||||
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
||||||
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
||||||
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_fluent_arrow_download_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
imageDescriptionButton = toolbar.getMenu()
|
||||||
toolbar.setOnMenuItemClickListener(item->{
|
.add(R.string.image_description)
|
||||||
saveCurrentFile();
|
.setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
|
||||||
return true;
|
.setVisible(attachments.get(pager.getCurrentItem()).description != null
|
||||||
});
|
&& !attachments.get(pager.getCurrentItem()).description.isEmpty())
|
||||||
|
.setOnMenuItemClickListener(item -> {
|
||||||
|
new ImageDescriptionSheet(activity,attachments.get(pager.getCurrentItem())).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
imageDescriptionButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
|
toolbar.getMenu()
|
||||||
|
.add(R.string.download)
|
||||||
|
.setIcon(R.drawable.ic_fluent_arrow_download_24_regular)
|
||||||
|
.setOnMenuItemClickListener(item -> {
|
||||||
|
saveCurrentFile();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
uiOverlay.setAlpha(0f);
|
uiOverlay.setAlpha(0f);
|
||||||
videoControls=uiOverlay.findViewById(R.id.video_player_controls);
|
videoControls=uiOverlay.findViewById(R.id.video_player_controls);
|
||||||
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
|
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
|
||||||
@@ -374,6 +390,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|||||||
private void onPageChanged(int index){
|
private void onPageChanged(int index){
|
||||||
currentIndex=index;
|
currentIndex=index;
|
||||||
Attachment att=attachments.get(index);
|
Attachment att=attachments.get(index);
|
||||||
|
imageDescriptionButton.setVisible(att.description != null && !att.description.isEmpty());
|
||||||
|
toolbar.invalidate();
|
||||||
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||||
if(att.type==Attachment.Type.VIDEO){
|
if(att.type==Attachment.Type.VIDEO){
|
||||||
videoSeekBar.setSecondaryProgress(0);
|
videoSeekBar.setSecondaryProgress(0);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.joinmastodon.android.ui.text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A span to mark character ranges that should be deleted when copied to the clipboard.
|
||||||
|
* Works with {@link org.joinmastodon.android.ui.views.LinkedTextView}.
|
||||||
|
*/
|
||||||
|
public class DeleteWhenCopiedSpan{
|
||||||
|
}
|
||||||
@@ -67,10 +67,9 @@ public class HtmlParser{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void head(@NonNull Node node, int depth){
|
public void head(@NonNull Node node, int depth){
|
||||||
if(node instanceof TextNode){
|
if(node instanceof TextNode textNode){
|
||||||
ssb.append(((TextNode) node).text());
|
ssb.append(textNode.text());
|
||||||
}else if(node instanceof Element){
|
}else if(node instanceof Element el){
|
||||||
Element el=(Element)node;
|
|
||||||
switch(el.nodeName()){
|
switch(el.nodeName()){
|
||||||
case "a" -> {
|
case "a" -> {
|
||||||
String href=el.attr("href");
|
String href=el.attr("href");
|
||||||
@@ -108,10 +107,9 @@ public class HtmlParser{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void tail(@NonNull Node node, int depth){
|
public void tail(@NonNull Node node, int depth){
|
||||||
if(node instanceof Element){
|
if(node instanceof Element el){
|
||||||
Element el=(Element)node;
|
|
||||||
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
|
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
|
||||||
ssb.append('…');
|
ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}else if("p".equals(el.nodeName())){
|
}else if("p".equals(el.nodeName())){
|
||||||
if(node.nextSibling()!=null)
|
if(node.nextSibling()!=null)
|
||||||
ssb.append("\n\n");
|
ssb.append("\n\n");
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class DiscoverInfoBannerHelper{
|
|||||||
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
|
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
|
||||||
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
||||||
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
||||||
|
case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ public class DiscoverInfoBannerHelper{
|
|||||||
TRENDING_HASHTAGS,
|
TRENDING_HASHTAGS,
|
||||||
TRENDING_LINKS,
|
TRENDING_LINKS,
|
||||||
LOCAL_TIMELINE,
|
LOCAL_TIMELINE,
|
||||||
|
FEDERATED_TIMELINE,
|
||||||
// ACCOUNTS
|
// ACCOUNTS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package org.joinmastodon.android.ui.utils;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||||
|
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
|
private final BaseStatusListFragment<?> listFragment;
|
||||||
|
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private int bgColor;
|
||||||
|
private int borderColor;
|
||||||
|
private RectF rect=new RectF();
|
||||||
|
|
||||||
|
public InsetStatusItemDecoration(BaseStatusListFragment<?> listFragment){
|
||||||
|
this.listFragment=listFragment;
|
||||||
|
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), android.R.attr.colorBackground);
|
||||||
|
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorPollVoted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||||
|
List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
|
||||||
|
int pos=0;
|
||||||
|
for(int i=0; i<parent.getChildCount(); i++){
|
||||||
|
View child=parent.getChildAt(i);
|
||||||
|
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||||
|
pos=holder.getAbsoluteAdapterPosition();
|
||||||
|
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
||||||
|
if(inset){
|
||||||
|
if(rect.isEmpty()){
|
||||||
|
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
||||||
|
}else{
|
||||||
|
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
||||||
|
}
|
||||||
|
}else if(!rect.isEmpty()){
|
||||||
|
drawInsetBackground(parent, c);
|
||||||
|
rect.setEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!rect.isEmpty()){
|
||||||
|
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
|
||||||
|
rect.bottom=parent.getHeight()+V.dp(10);
|
||||||
|
}
|
||||||
|
drawInsetBackground(parent, c);
|
||||||
|
rect.setEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawInsetBackground(RecyclerView list, Canvas c){
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
paint.setColor(bgColor);
|
||||||
|
rect.left=V.dp(12);
|
||||||
|
rect.right=list.getWidth()-V.dp(12);
|
||||||
|
rect.inset(V.dp(4), V.dp(4));
|
||||||
|
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeWidth(V.dp(1));
|
||||||
|
paint.setColor(borderColor);
|
||||||
|
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
|
||||||
|
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||||
|
List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
|
||||||
|
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||||
|
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
||||||
|
boolean inset=sdi.getItem().inset;
|
||||||
|
int pos=holder.getAbsoluteAdapterPosition();
|
||||||
|
if(inset){
|
||||||
|
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||||
|
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||||
|
int pad;
|
||||||
|
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
||||||
|
pad=V.dp(16);
|
||||||
|
else
|
||||||
|
pad=V.dp(12);
|
||||||
|
boolean insetLeft=true, insetRight=true;
|
||||||
|
if(holder instanceof ImageStatusDisplayItem.Holder<?> img){
|
||||||
|
PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout;
|
||||||
|
PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile;
|
||||||
|
// only inset those items that are on the edges of the layout
|
||||||
|
insetLeft=tile.startCol==0;
|
||||||
|
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
|
||||||
|
// inset all items in the bottom row
|
||||||
|
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
|
||||||
|
bottomSiblingInset=false;
|
||||||
|
}
|
||||||
|
if(insetLeft)
|
||||||
|
outRect.left=pad;
|
||||||
|
if(insetRight)
|
||||||
|
outRect.right=pad;
|
||||||
|
if(!topSiblingInset)
|
||||||
|
outRect.top=pad;
|
||||||
|
if(!bottomSiblingInset)
|
||||||
|
outRect.bottom=pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import android.content.res.TypedArray;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.InsetDrawable;
|
import android.graphics.drawable.InsetDrawable;
|
||||||
@@ -20,6 +19,7 @@ import android.os.Build;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
@@ -42,8 +42,13 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
|
|||||||
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
|
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
|
||||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||||
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
@@ -51,8 +56,10 @@ import org.joinmastodon.android.model.Account;
|
|||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||||
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
@@ -62,6 +69,9 @@ import java.time.Instant;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -74,6 +84,10 @@ import androidx.annotation.StringRes;
|
|||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
import androidx.recyclerview.widget.DiffUtil;
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
@@ -85,6 +99,7 @@ import okhttp3.MediaType;
|
|||||||
public class UiUtils{
|
public class UiUtils{
|
||||||
private static Handler mainHandler=new Handler(Looper.getMainLooper());
|
private static Handler mainHandler=new Handler(Looper.getMainLooper());
|
||||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM");
|
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM");
|
||||||
|
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||||
|
|
||||||
private UiUtils(){}
|
private UiUtils(){}
|
||||||
|
|
||||||
@@ -107,7 +122,9 @@ public class UiUtils{
|
|||||||
long t=instant.toEpochMilli();
|
long t=instant.toEpochMilli();
|
||||||
long now=System.currentTimeMillis();
|
long now=System.currentTimeMillis();
|
||||||
long diff=now-t;
|
long diff=now-t;
|
||||||
if(diff<60_000L){
|
if(diff<1000L){
|
||||||
|
return context.getString(R.string.time_now);
|
||||||
|
}else if(diff<60_000L){
|
||||||
return context.getString(R.string.time_seconds, diff/1000L);
|
return context.getString(R.string.time_seconds, diff/1000L);
|
||||||
}else if(diff<3600_000L){
|
}else if(diff<3600_000L){
|
||||||
return context.getString(R.string.time_minutes, diff/60_000L);
|
return context.getString(R.string.time_minutes, diff/60_000L);
|
||||||
@@ -127,6 +144,23 @@ public class UiUtils{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){
|
||||||
|
long t=instant.toEpochMilli();
|
||||||
|
long now=System.currentTimeMillis();
|
||||||
|
long diff=now-t;
|
||||||
|
if(diff<1000L){
|
||||||
|
return context.getString(R.string.time_just_now);
|
||||||
|
}else if(diff<60_000L){
|
||||||
|
int secs=(int)(diff/1000L);
|
||||||
|
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
|
||||||
|
}else if(diff<3600_000L){
|
||||||
|
int mins=(int)(diff/60_000L);
|
||||||
|
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
|
||||||
|
}else{
|
||||||
|
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String formatTimeLeft(Context context, Instant instant){
|
public static String formatTimeLeft(Context context, Instant instant){
|
||||||
long t=instant.toEpochMilli();
|
long t=instant.toEpochMilli();
|
||||||
long now=System.currentTimeMillis();
|
long now=System.currentTimeMillis();
|
||||||
@@ -336,6 +370,7 @@ public class UiUtils{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
resultCallback.accept(result);
|
resultCallback.accept(result);
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +384,88 @@ public class UiUtils{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
|
||||||
|
showConfirmationAlert(activity,
|
||||||
|
pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title,
|
||||||
|
pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post,
|
||||||
|
pinned ? R.string.pin_post : R.string.unpin_post,
|
||||||
|
()->{
|
||||||
|
new SetStatusPinned(status.id, pinned)
|
||||||
|
.setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Status result) {
|
||||||
|
resultCallback.accept(result);
|
||||||
|
E.post(new StatusCountersUpdatedEvent(result));
|
||||||
|
if (!result.pinned)
|
||||||
|
E.post(new StatusUnpinnedEvent(status.id, accountID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
error.showToast(activity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void confirmDeleteAndRedraftPost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
|
||||||
|
showConfirmationAlert(activity, R.string.confirm_delete_and_redraft_title, R.string.confirm_delete_and_redraft, R.string.delete_and_redraft, ()->{
|
||||||
|
new DeleteStatus(status.id)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Status result){
|
||||||
|
resultCallback.accept(result);
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||||
|
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||||
|
UiUtils.redraftStatus(status, accountID, activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(activity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(activity, R.string.deleting, false)
|
||||||
|
.exec(accountID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void redraftStatus(Status status, String accountID, Activity activity) {
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putBoolean("hasDraft", true);
|
||||||
|
args.putString("prefilledText", HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, accountID).toString());
|
||||||
|
args.putString("spoilerText", status.spoilerText);
|
||||||
|
args.putSerializable("visibility", status.visibility);
|
||||||
|
if(status.poll!=null){
|
||||||
|
args.putInt("pollDuration", (int)status.poll.expiresAt.minus(status.createdAt.getEpochSecond(), ChronoUnit.SECONDS).getEpochSecond());
|
||||||
|
ArrayList<String> opts=status.poll.options.stream().map(o -> o.title).collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
args.putStringArrayList("pollOptions", opts);
|
||||||
|
}
|
||||||
|
if(!status.mediaAttachments.isEmpty()){
|
||||||
|
ArrayList<Parcelable> serializedAttachments=status.mediaAttachments.stream()
|
||||||
|
.map(att -> Parcels.wrap(ComposeFragment.redraftAttachment(att)))
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
args.putParcelableArrayList("attachments", serializedAttachments);
|
||||||
|
}
|
||||||
|
Callback<Status> cb=new Callback<>(){
|
||||||
|
@Override public void onError(ErrorResponse error) {
|
||||||
|
onSuccess(null);
|
||||||
|
error.showToast(activity);
|
||||||
|
}
|
||||||
|
@Override public void onSuccess(Status status) {
|
||||||
|
if (status!=null) args.putParcelable("replyTo", Parcels.wrap(status));
|
||||||
|
Nav.go(activity, ComposeFragment.class, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(status.inReplyToId!=null) new GetStatusByID(status.inReplyToId).setCallback(cb).exec(accountID);
|
||||||
|
else cb.onSuccess(null);
|
||||||
|
}
|
||||||
|
|
||||||
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
||||||
boolean secondaryStyle;
|
boolean secondaryStyle;
|
||||||
if(relationship.blocking){
|
if(relationship.blocking){
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class ImageAttachmentFrameLayout extends FrameLayout{
|
|||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int w=Math.min(((View)getParent()).getMeasuredWidth()-horizontalInset, V.dp(MAX_WIDTH));
|
int w=Math.min(((View)getParent()).getMeasuredWidth(), V.dp(MAX_WIDTH))-horizontalInset;
|
||||||
int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1);
|
int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1);
|
||||||
int actualWidth=Math.round(tile.width/1000f*w);
|
int actualWidth=Math.round(tile.width/1000f*w);
|
||||||
if(tile.startCol+tile.colSpan<tileLayout.columnSizes.length)
|
if(tile.startCol+tile.colSpan<tileLayout.columnSizes.length)
|
||||||
|
|||||||
@@ -1,38 +1,68 @@
|
|||||||
package org.joinmastodon.android.ui.views;
|
package org.joinmastodon.android.ui.views;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.ui.text.ClickableLinksDelegate;
|
import org.joinmastodon.android.ui.text.ClickableLinksDelegate;
|
||||||
|
import org.joinmastodon.android.ui.text.DeleteWhenCopiedSpan;
|
||||||
|
|
||||||
public class LinkedTextView extends TextView {
|
public class LinkedTextView extends TextView{
|
||||||
|
|
||||||
private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this);
|
private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this);
|
||||||
private boolean needInvalidate;
|
private boolean needInvalidate;
|
||||||
|
private ActionMode currentActionMode;
|
||||||
public LinkedTextView(Context context) {
|
|
||||||
super(context);
|
public LinkedTextView(Context context){
|
||||||
// TODO Auto-generated constructor stub
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkedTextView(Context context, AttributeSet attrs) {
|
public LinkedTextView(Context context, AttributeSet attrs){
|
||||||
super(context, attrs);
|
this(context, attrs, 0);
|
||||||
// TODO Auto-generated constructor stub
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkedTextView(Context context, AttributeSet attrs, int defStyle) {
|
public LinkedTextView(Context context, AttributeSet attrs, int defStyle){
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
// TODO Auto-generated constructor stub
|
setCustomSelectionActionModeCallback(new ActionMode.Callback(){
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||||
|
currentActionMode=mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||||
|
onTextContextMenuItem(item.getItemId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode){
|
||||||
|
currentActionMode=null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean onTouchEvent(MotionEvent ev){
|
public boolean onTouchEvent(MotionEvent ev){
|
||||||
if(delegate.onTouch(ev)) return true;
|
if(delegate.onTouch(ev)) return true;
|
||||||
return super.onTouchEvent(ev);
|
return super.onTouchEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDraw(Canvas c){
|
public void onDraw(Canvas c){
|
||||||
super.onDraw(c);
|
super.onDraw(c);
|
||||||
delegate.onDraw(c);
|
delegate.onDraw(c);
|
||||||
@@ -47,4 +77,43 @@ public class LinkedTextView extends TextView {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTextContextMenuItem(int id){
|
||||||
|
if(id==android.R.id.copy){
|
||||||
|
final int selStart=getSelectionStart();
|
||||||
|
final int selEnd=getSelectionEnd();
|
||||||
|
int min=Math.max(0, Math.min(selStart, selEnd));
|
||||||
|
int max=Math.max(0, Math.max(selStart, selEnd));
|
||||||
|
final ClipData copyData=ClipData.newPlainText(null, deleteTextWithinDeleteSpans(getText().subSequence(min, max)));
|
||||||
|
ClipboardManager clipboard=getContext().getSystemService(ClipboardManager.class);
|
||||||
|
try {
|
||||||
|
clipboard.setPrimaryClip(copyData);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.w("LinkedTextView", t);
|
||||||
|
}
|
||||||
|
if(currentActionMode!=null){
|
||||||
|
currentActionMode.finish();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onTextContextMenuItem(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence deleteTextWithinDeleteSpans(CharSequence text){
|
||||||
|
if(text instanceof Spanned spanned){
|
||||||
|
DeleteWhenCopiedSpan[] delSpans=spanned.getSpans(0, text.length(), DeleteWhenCopiedSpan.class);
|
||||||
|
if(delSpans.length>0){
|
||||||
|
SpannableStringBuilder ssb=new SpannableStringBuilder(spanned);
|
||||||
|
for(DeleteWhenCopiedSpan span:delSpans){
|
||||||
|
int start=ssb.getSpanStart(span);
|
||||||
|
int end=ssb.getSpanStart(span);
|
||||||
|
if(start==-1)
|
||||||
|
continue;
|
||||||
|
ssb.delete(start, end+1);
|
||||||
|
}
|
||||||
|
return ssb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
mastodon/src/main/res/color/bookmark_icon.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/bookmark_selected" android:state_selected="true"/>
|
||||||
|
<item android:color="?android:textColorSecondary"/>
|
||||||
|
</selector>
|
||||||
7
mastodon/src/main/res/drawable/ic_compose_foreground.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<vector android:height="108dp"
|
||||||
|
android:viewportHeight="48" android:viewportWidth="48"
|
||||||
|
android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<group android:translateX="12" android:translateY="12">
|
||||||
|
<path android:fillColor="@color/shortcut_icon_foreground" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18zM17.502 6.25c0-0.966-0.783-1.75-1.75-1.75H8.253c-0.967 0-1.75 0.784-1.75 1.75v13.532l5.061-3.641c0.262-0.188 0.614-0.188 0.876 0l5.061 3.641V6.25z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_activated="true"/>
|
||||||
|
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_checked="true"/>
|
||||||
|
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_selected="true"/>
|
||||||
|
<item android:drawable="@drawable/ic_fluent_bookmark_24_regular"/>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M4 6.748c0-1.243 1.007-2.25 2.25-2.25h9c1.243 0 2.25 1.007 2.25 2.25V21.25c0 0.268-0.143 0.517-0.376 0.65-0.233 0.134-0.52 0.133-0.751-0.002l-5.623-3.28-5.622 3.28c-0.232 0.135-0.519 0.136-0.752 0.002C4.144 21.767 4 21.52 4 21.25V6.748zM15.25 2C17.873 2 20 4.127 20 6.75v11.873c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75V6.751c0-1.796-1.455-3.25-3.25-3.25H6.637S6.75 2.942 7.434 2.42C8 2 8.602 2 8.602 2h6.648z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M21.03 2.97c1.398 1.397 1.398 3.663 0 5.06L9.062 20c-0.277 0.277-0.621 0.477-0.999 0.58l-5.116 1.395c-0.56 0.153-1.073-0.361-0.92-0.921l1.395-5.116c0.103-0.377 0.302-0.722 0.58-0.999L15.97 2.97c1.397-1.398 3.663-1.398 5.06 0zM15 6.06L5.062 16c-0.092 0.092-0.159 0.207-0.193 0.333l-1.05 3.85 3.85-1.05C7.793 19.096 7.908 19.03 8 18.938L17.94 9 15 6.06zm2.03-2.03L16.06 5 19 7.94l0.97-0.97c0.811-0.812 0.811-2.128 0-2.94-0.812-0.811-2.128-0.811-2.94 0z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M1 3c0-1.105 0.895-2 2-2h7c1.105 0 2 0.895 2 2v6c0 1.105-0.895 2-2 2H3c-1.105 0-2-0.895-2-2V3zm2.5 1C3.224 4 3 4.224 3 4.5S3.224 5 3.5 5h6C9.776 5 10 4.776 10 4.5S9.776 4 9.5 4h-6zm0 3C3.224 7 3 7.224 3 7.5S3.224 8 3.5 8h6C9.776 8 10 7.776 10 7.5S9.776 7 9.5 7h-6zM4 12h1.5v6.75c0 0.208 0.036 0.408 0.103 0.594l5.823-5.701c0.833-0.816 2.142-0.854 3.02-0.116l0.128 0.116 5.822 5.702c0.067-0.186 0.104-0.386 0.104-0.595V7.25c0-0.966-0.784-1.75-1.75-1.75H13V4h5.75C20.545 4 22 5.455 22 7.25v11.5c0 1.795-1.455 3.25-3.25 3.25H7.25C5.455 22 4 20.545 4 18.75V12zm15.33 8.401l-5.805-5.686c-0.265-0.26-0.675-0.283-0.966-0.071l-0.084 0.07-5.807 5.687C6.85 20.465 7.046 20.5 7.25 20.5h11.5c0.203 0 0.399-0.035 0.58-0.099zM16.253 7.5c1.244 0 2.252 1.008 2.252 2.252 0 1.244-1.008 2.252-2.252 2.252-1.244 0-2.252-1.008-2.252-2.252C14 8.508 15.008 7.5 16.252 7.5zm0 1.5C15.837 9 15.5 9.337 15.5 9.752s0.337 0.752 0.752 0.752c0.416 0 0.752-0.336 0.752-0.752C17.004 9.337 16.667 9 16.252 9z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
android:textAppearance="@style/m3_label_medium"
|
android:textAppearance="@style/m3_label_medium"
|
||||||
android:textColor="?colorButtonText"
|
android:textColor="?colorButtonText"
|
||||||
android:gravity="end"
|
android:gravity="end"
|
||||||
|
android:singleLine="true"
|
||||||
tools:text="1:23"/>
|
tools:text="1:23"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -2,49 +2,146 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="8dp"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:background="?colorBackgroundLightest">
|
|
||||||
|
|
||||||
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
|
<RelativeLayout
|
||||||
android:id="@+id/button_bar"
|
android:id="@+id/reblogs"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="64dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:background="?android:selectableItemBackground">
|
||||||
|
|
||||||
<Button
|
<ImageView
|
||||||
android:id="@+id/reblogs"
|
android:id="@+id/icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:src="@drawable/ic_fluent_arrow_repeat_all_24_regular"
|
||||||
|
android:tint="?android:textColorSecondary"
|
||||||
|
android:importantForAccessibility="no"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:layout_marginStart="32dp"
|
||||||
android:padding="8dp"
|
android:layout_toEndOf="@id/icon"
|
||||||
android:textSize="14sp"
|
android:minHeight="22dp"
|
||||||
android:minHeight="36dp"
|
android:singleLine="true"
|
||||||
android:textColor="?android:textColorSecondary"
|
android:text="@string/post_info_reblogs"
|
||||||
android:background="@drawable/bg_text_button"
|
android:textAppearance="@style/m3_body_large" />
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
tools:text="4 reblogs"/>
|
|
||||||
|
|
||||||
<Button
|
<TextView
|
||||||
android:id="@+id/favorites"
|
android:id="@+id/reblogs_count"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:layout_below="@id/title"
|
||||||
android:padding="8dp"
|
android:layout_alignStart="@id/title"
|
||||||
android:textSize="14sp"
|
android:singleLine="true"
|
||||||
android:minHeight="36dp"
|
android:textAppearance="@style/m3_body_medium"
|
||||||
android:textColor="?android:textColorSecondary"
|
android:textColor="?android:textColorSecondary"
|
||||||
android:background="@drawable/bg_text_button"
|
tools:text="123 456"/>
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
tools:text="12 favorites"/>
|
|
||||||
|
|
||||||
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/favorites"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:background="?android:selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:src="@drawable/ic_fluent_star_24_regular"
|
||||||
|
android:tint="?android:textColorSecondary"
|
||||||
|
android:importantForAccessibility="no"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_toEndOf="@id/icon"
|
||||||
|
android:minHeight="22dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="@string/post_info_favorites"
|
||||||
|
android:textAppearance="@style/m3_body_large" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/favorites_count"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/title"
|
||||||
|
android:layout_alignStart="@id/title"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/m3_body_medium"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
tools:text="123 456"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/edit_history"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:background="?android:selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:src="@drawable/ic_fluent_edit_24_regular"
|
||||||
|
android:tint="?android:textColorSecondary"
|
||||||
|
android:importantForAccessibility="no"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_toEndOf="@id/icon"
|
||||||
|
android:minHeight="22dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="@string/edit_history"
|
||||||
|
android:textAppearance="@style/m3_body_large" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/last_edited"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/title"
|
||||||
|
android:layout_alignStart="@id/title"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/m3_body_medium"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
tools:text="123 456"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/timestamp"
|
android:id="@+id/timestamp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="16dp"
|
||||||
android:minHeight="20dp"
|
android:minHeight="20dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
|
|||||||
@@ -73,6 +73,28 @@
|
|||||||
tools:text="123"/>
|
tools:text="123"/>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="1px"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/bookmark_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minWidth="56dp">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bookmark"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:drawableStart="@drawable/ic_fluent_bookmark_24_selector"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:drawableTint="@color/bookmark_icon"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/m3_label_large" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0px"
|
android:layout_width="0px"
|
||||||
android:layout_height="1px"
|
android:layout_height="1px"
|
||||||
|
|||||||
@@ -9,20 +9,21 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<org.joinmastodon.android.ui.views.ComposeMediaLayout
|
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal">
|
android:layout_gravity="center"
|
||||||
|
android:maxWidth="400dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/photo"
|
android:id="@+id/photo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:scaleType="centerCrop"
|
android:adjustViewBounds="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
tools:src="#0f0"/>
|
tools:src="#0f0"/>
|
||||||
|
|
||||||
</org.joinmastodon.android.ui.views.ComposeMediaLayout>
|
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
|
|||||||
@@ -78,102 +78,110 @@
|
|||||||
tools:text="Founder, CEO and lead developer @Mastodon, Germany." />
|
tools:text="Founder, CEO and lead developer @Mastodon, Germany." />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/posts_btn"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_below="@id/bio"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/posts_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/m3_title_large"
|
|
||||||
tools:text="123" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/posts_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/m3_title_small"
|
|
||||||
tools:text="following" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/followers_btn"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_toEndOf="@id/posts_btn"
|
|
||||||
android:layout_alignTop="@id/posts_btn"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/followers_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/m3_title_large"
|
|
||||||
tools:text="123"/>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/followers_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/m3_title_small"
|
|
||||||
tools:text="following"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/following_btn"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_alignTop="@id/posts_btn"
|
|
||||||
android:layout_toEndOf="@id/followers_btn"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/following_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/m3_title_large"
|
|
||||||
tools:text="123"/>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/following_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/m3_title_small"
|
|
||||||
tools:text="following"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/action_btn_wrap"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_below="@id/bio"
|
||||||
android:layout_alignTop="@id/posts_btn"
|
android:orientation="horizontal">
|
||||||
android:layout_marginTop="-8dp"
|
|
||||||
android:padding="8dp"
|
<LinearLayout
|
||||||
android:layout_marginEnd="8dp"
|
android:id="@+id/posts_btn"
|
||||||
android:clipToPadding="false">
|
android:layout_width="wrap_content"
|
||||||
<org.joinmastodon.android.ui.views.ProgressBarButton
|
android:layout_height="48dp"
|
||||||
android:id="@+id/action_btn"
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/posts_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_title_large"
|
||||||
|
tools:text="123" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/posts_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_title_small"
|
||||||
|
tools:text="following" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/followers_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/followers_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_title_large"
|
||||||
|
tools:text="123"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/followers_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_title_small"
|
||||||
|
tools:text="following"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/following_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/following_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_title_large"
|
||||||
|
tools:text="123"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/following_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_title_small"
|
||||||
|
tools:text="following"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="1px"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/action_btn_wrap"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:text="Edit Profile"/>
|
android:layout_marginTop="8dp"
|
||||||
<ProgressBar
|
android:padding="8dp"
|
||||||
android:id="@+id/action_progress"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_width="wrap_content"
|
android:clipToPadding="false">
|
||||||
android:layout_height="wrap_content"
|
<org.joinmastodon.android.ui.views.ProgressBarButton
|
||||||
android:layout_gravity="center"
|
android:id="@+id/action_btn"
|
||||||
android:indeterminate="true"
|
android:layout_width="wrap_content"
|
||||||
style="?android:progressBarStyleSmall"
|
android:layout_height="wrap_content"
|
||||||
android:elevation="10dp"
|
android:singleLine="true"
|
||||||
android:outlineProvider="none"
|
tools:text="@string/follow_back"/>
|
||||||
android:indeterminateTint="?colorButtonText"
|
<ProgressBar
|
||||||
android:visibility="gone"/>
|
android:id="@+id/action_progress"
|
||||||
</FrameLayout>
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
style="?android:progressBarStyleSmall"
|
||||||
|
android:elevation="10dp"
|
||||||
|
android:outlineProvider="none"
|
||||||
|
android:indeterminateTint="?colorButtonText"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
<item android:id="@+id/vis_public"
|
<item android:id="@+id/vis_public"
|
||||||
android:icon="@drawable/ic_fluent_earth_24_filled"
|
android:icon="@drawable/ic_fluent_earth_24_filled"
|
||||||
android:title="@string/visibility_public"/>
|
android:title="@string/visibility_public"/>
|
||||||
|
<item android:id="@+id/vis_unlisted"
|
||||||
|
android:icon="@drawable/ic_fluent_people_community_24_regular"
|
||||||
|
android:title="@string/visibility_unlisted"/>
|
||||||
<item android:id="@+id/vis_followers"
|
<item android:id="@+id/vis_followers"
|
||||||
android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
|
android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
|
||||||
android:title="@string/visibility_followers_only"/>
|
android:title="@string/visibility_followers_only"/>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@+id/edit" android:title="@string/edit"/>
|
||||||
<item android:id="@+id/delete" android:title="@string/delete"/>
|
<item android:id="@+id/delete" android:title="@string/delete"/>
|
||||||
|
<item android:id="@+id/delete_and_redraft" android:title="@string/delete_and_redraft"/>
|
||||||
|
<item android:id="@+id/pin" android:title="@string/pin_post"/>
|
||||||
|
<item android:id="@+id/unpin" android:title="@string/unpin_post"/>
|
||||||
<item android:id="@+id/mute" android:title="@string/mute_user"/>
|
<item android:id="@+id/mute" android:title="@string/mute_user"/>
|
||||||
<item android:id="@+id/block" android:title="@string/block_user"/>
|
<item android:id="@+id/block" android:title="@string/block_user"/>
|
||||||
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
|
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
|
||||||
|
|||||||
@@ -7,4 +7,10 @@
|
|||||||
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
|
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
|
||||||
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
|
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
|
||||||
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
|
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/bookmarks"
|
||||||
|
android:showAsAction="always"
|
||||||
|
android:visible="false"
|
||||||
|
android:icon="@drawable/ic_fluent_bookmark_multiple_24_filled"
|
||||||
|
android:title="@string/bookmarks"/>
|
||||||
</menu>
|
</menu>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/shortcut_icon_background"/>
|
||||||
|
<size android:width="108dp" android:height="108dp"/>
|
||||||
|
</shape>
|
||||||
|
</background>
|
||||||
|
<foreground android:drawable="@drawable/ic_compose_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |