Compare commits
257 Commits
v1.2.3+for
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b399d5815 | ||
|
|
9f18d1bc8b | ||
|
|
9a8cf61e38 | ||
|
|
eb822282c0 | ||
|
|
273823a65f | ||
|
|
f2aa1400c5 | ||
|
|
cf8a9e1823 | ||
|
|
b3320d534b | ||
|
|
f58b4c2989 | ||
|
|
47b3b1e307 | ||
|
|
e5649c4a42 | ||
|
|
cea77eca02 | ||
|
|
4fbbcfba59 | ||
|
|
6a24f70537 | ||
|
|
4681160924 | ||
|
|
6f8c2f4a44 | ||
|
|
69de9dce38 | ||
|
|
721ae9c68d | ||
|
|
6a6ed89d29 | ||
|
|
3e4377a366 | ||
|
|
287de66e0c | ||
|
|
df2ae9d964 | ||
|
|
ef55f1f49b | ||
|
|
aebf7e9f1f | ||
|
|
a014ce6eb5 | ||
|
|
aed1efceb9 | ||
|
|
124c375b14 | ||
|
|
1d46e22a7f | ||
|
|
28c334429d | ||
|
|
4726f98d4f | ||
|
|
985f382436 | ||
|
|
f822c788b0 | ||
|
|
46f8982aa6 | ||
|
|
7b3cec9289 | ||
|
|
a3f227cb8d | ||
|
|
d71feb2cfc | ||
|
|
b69565e9e6 | ||
|
|
bf996feccf | ||
|
|
b1143b0eec | ||
|
|
ce977163c2 | ||
|
|
ded74bda83 | ||
|
|
7180113397 | ||
|
|
81ac8a3bc9 | ||
|
|
4b74da5d38 | ||
|
|
0375cfa260 | ||
|
|
52108a675a | ||
|
|
406e95d3f7 | ||
|
|
a9f355dea9 | ||
|
|
1a63c12327 | ||
|
|
7bc2f8b352 | ||
|
|
d56ef227a7 | ||
|
|
fcc371ab5a | ||
|
|
9f43a99772 | ||
|
|
9fc5a4e390 | ||
|
|
a2b3f873f6 | ||
|
|
f6c1509e48 | ||
|
|
d9bbb32a28 | ||
|
|
d2ef6e77af | ||
|
|
6133e9bac3 | ||
|
|
6ca48f35f1 | ||
|
|
cb016b4383 | ||
|
|
d0b21df28b | ||
|
|
ce48ee888a | ||
|
|
5cd8bc5a46 | ||
|
|
4109cd75d3 | ||
|
|
97a889e019 | ||
|
|
b9a1b3591d | ||
|
|
fde84e3cfb | ||
|
|
0985eb4fac | ||
|
|
5957c1a221 | ||
|
|
e7e34aa2c8 | ||
|
|
b8742591b8 | ||
|
|
50e73ac12e | ||
|
|
9ed277a9b2 | ||
|
|
c9e2984d68 | ||
|
|
00aaff10a7 | ||
|
|
6da4256adf | ||
|
|
da9c826791 | ||
|
|
bed02e248e | ||
|
|
f53531889f | ||
|
|
ef3246ae2a | ||
|
|
5504b534f7 | ||
|
|
0e553d7868 | ||
|
|
38be51367e | ||
|
|
0d4af0970c | ||
|
|
b51335ffd6 | ||
|
|
444fa07984 | ||
|
|
feb0f304fb | ||
|
|
ccd4a1aa9f | ||
|
|
b3723c2977 | ||
|
|
c5b70a9ada | ||
|
|
82789179e7 | ||
|
|
b0a309a817 | ||
|
|
32a27b6e59 | ||
|
|
f73a318dad | ||
|
|
4fff2c5f5c | ||
|
|
81abac657f | ||
|
|
74decd3ec7 | ||
|
|
11d17d1f3f | ||
|
|
ca2384ba8c | ||
|
|
ded23342db | ||
|
|
a35c14865f | ||
|
|
0952d97557 | ||
|
|
e1db5f15ca | ||
|
|
c9e467ac2f | ||
|
|
74a5e970d9 | ||
|
|
935de7d02e | ||
|
|
a04522ff72 | ||
|
|
90a93ffba6 | ||
|
|
069c55d4b9 | ||
|
|
1efaf4b605 | ||
|
|
ff74516ce2 | ||
|
|
aee4b7aaab | ||
|
|
1d94479bde | ||
|
|
85c95d899e | ||
|
|
18d4210e7d | ||
|
|
ac25bc6d42 | ||
|
|
ec05ef3b4c | ||
|
|
9827d97374 | ||
|
|
4fab92d516 | ||
|
|
44bc9c4e40 | ||
|
|
1030773ef6 | ||
|
|
1a0cb4b8c8 | ||
|
|
4295a3672c | ||
|
|
fd2a8fe230 | ||
|
|
e2d1eccfb9 | ||
|
|
05d1d3e725 | ||
|
|
37261928c2 | ||
|
|
63132110d9 | ||
|
|
ffd7e415a2 | ||
|
|
d43664d018 | ||
|
|
8e06362ff8 | ||
|
|
625ccfd31f | ||
|
|
d7b5d242ff | ||
|
|
7973c87b9a | ||
|
|
3085b1507b | ||
|
|
f17ef17492 | ||
|
|
8d0a31d0f9 | ||
|
|
6c6d3fed05 | ||
|
|
b616e2e11b | ||
|
|
b18771bb79 | ||
|
|
40ef4c179a | ||
|
|
62212dc6c9 | ||
|
|
3ed2b67037 | ||
|
|
d240750606 | ||
|
|
ed009d3e2e | ||
|
|
be2fa0f217 | ||
|
|
a8d9b4538b | ||
|
|
2b7b3de043 | ||
|
|
64362968fc | ||
|
|
aa9caefed1 | ||
|
|
14bbe1ffef | ||
|
|
b6976fb519 | ||
|
|
040237de2b | ||
|
|
fc307ff43f | ||
|
|
04304b3397 | ||
|
|
7260db6668 | ||
|
|
1dadc51ddf | ||
|
|
fe85351869 | ||
|
|
74a83c6ac4 | ||
|
|
df77ba61ad | ||
|
|
ed40f74d59 | ||
|
|
42e26bef68 | ||
|
|
af60adb55f | ||
|
|
5dfa9237ad | ||
|
|
c890195567 | ||
|
|
b50a327b17 | ||
|
|
17957b69d1 | ||
|
|
c4ac4ee173 | ||
|
|
659b4e2fcd | ||
|
|
02b1ad8d7a | ||
|
|
47eeb01b75 | ||
|
|
3d5fb2dfea | ||
|
|
ef6238b593 | ||
|
|
bc9bec3d66 | ||
|
|
a24b4363d7 | ||
|
|
02ddad22e7 | ||
|
|
a705512dc5 | ||
|
|
cfd6954755 | ||
|
|
702ac43f86 | ||
|
|
6ede2d22bb | ||
|
|
315d26ad52 | ||
|
|
a78b0687f7 | ||
|
|
148b8e9369 | ||
|
|
ae6ce0f9b0 | ||
|
|
31c8665653 | ||
|
|
d7f73e02c5 | ||
|
|
e897b3af57 | ||
|
|
fdbf331432 | ||
|
|
aed86ac6f0 | ||
|
|
3a13d4d6c0 | ||
|
|
f5336564d0 | ||
|
|
07ca5a8b77 | ||
|
|
6926a212f4 | ||
|
|
936f39161b | ||
|
|
7c6ec2e3d7 | ||
|
|
31c7116a15 | ||
|
|
fe84dc4823 | ||
|
|
5c480b37b3 | ||
|
|
92335d8678 | ||
|
|
1dcb5717ea | ||
|
|
36f1a557d7 | ||
|
|
bd7157c172 | ||
|
|
2e1795dc6f | ||
|
|
cd1be782fa | ||
|
|
67059f3d71 | ||
|
|
15f4d3326b | ||
|
|
e65404a466 | ||
|
|
3d47d1b4db | ||
|
|
d1749ab610 | ||
|
|
806c264686 | ||
|
|
34a9cb5a74 | ||
|
|
64fad2e871 | ||
|
|
34a2af8429 | ||
|
|
9c89c26097 | ||
|
|
e3b6a5d389 | ||
|
|
0fb54efde5 | ||
|
|
a4a3f32dba | ||
|
|
15883f2138 | ||
|
|
03a1e29e0c | ||
|
|
eda9ff272b | ||
|
|
89501271ce | ||
|
|
b3728e06ac | ||
|
|
968a6ea9b3 | ||
|
|
e253d8f4f3 | ||
|
|
cfabe47e10 | ||
|
|
d3fe7857b7 | ||
|
|
642e96a439 | ||
|
|
2b8451e045 | ||
|
|
62074e554a | ||
|
|
0434cda2da | ||
|
|
4b4c88d44d | ||
|
|
9e116bec97 | ||
|
|
dc25e16c00 | ||
|
|
42a7c324fa | ||
|
|
849888d128 | ||
|
|
8a7e910e7c | ||
|
|
72d72d443e | ||
|
|
7b0a3f0f96 | ||
|
|
cf4604e0d8 | ||
|
|
e873dd7d0a | ||
|
|
4492e940e5 | ||
|
|
c833c03dc3 | ||
|
|
30b0d226b5 | ||
|
|
8afad21113 | ||
|
|
6b5e5b0f25 | ||
|
|
3c0ab6822f | ||
|
|
f7215d00ca | ||
|
|
43bbe9be0f | ||
|
|
477a691c9e | ||
|
|
e5d60050a2 | ||
|
|
9a698fda18 | ||
|
|
d5701c1073 | ||
|
|
955b9a4b2b | ||
|
|
09ffda2605 | ||
|
|
039fb0c505 | ||
|
|
20799ef1a8 |
@@ -5,7 +5,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.1.3"
|
||||
classpath "com.android.tools.build:gradle:7.4.2"
|
||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ماستودون هي أكبر شبكة اجتماعية لا مركزيَّة على الإنترنت. بدلاً من كونها على موقع ويب واحد مركزي، هي عبارة عن شبكة من ملايين المستخدمين في مجتمعات مُستقلَّة يمكنهم جميعًا التفاعل مع بعضهم البعض بسلاسة. بغض النظر عن اهتماماتك، يمكنك مقابلة أشخاص متحمسين ينشرون عنها في ماستودون!
|
||||
ماستدون هي أكبر شبكة اجتماعية لا مركزيَّة على الإنترنت. بدلاً من كونها على موقع ويب واحد مركزي، هي عبارة عن شبكة من ملايين المستخدمين في مجتمعات مُستقلَّة يمكنهم جميعًا التفاعل مع بعضهم البعض بسلاسة. بغض النظر عن اهتماماتك، يمكنك مقابلة أشخاص متحمسين ينشرون عنها في ماستودون!
|
||||
|
||||
اِنضم إلَى مُجتَمع وأنشئ مِلَفَّكَ التَّعريفِيّ. ابحث عن أشخاص رائعين، تابعهم واقرأ منشوراتهم في خطٍّ زمني خالٍ من الإعلانات. عبِّر عَن نَفسِكَ باِستخدام رُموزٍ تَعبيرِيَّةٍ مُخصَّصَة، أو صُوَر، أو صُوَرٍ مُتحَرِّكَة، أو مَقاطِعٍ مَرئِّيَة أو مَقاطِعٍ صَوتِيَّةٍ فِي مَنشوراتٍ ذَاتُ خَمسِمائَة حَرف. رُدّ على سَلاسِلِ المَنشوراتِ، وأعِد تَدوينَ مَنشُوراتِ أيِّ شَخصٍ لِمُشارَكَةِ الأُمُورِ الرَّائِعَة. اِبحَث عَن حِساباتٍ جَديدَةٍ لِمُتابَعَتِها، وَعَن وُسُومٍ شَائِعَةٍ لِتَوسيعِ شَبَكَتِك.
|
||||
|
||||
|
||||
@@ -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 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.
|
||||
ماستودون با تمرکز بر حریم خصوصی و ایمنی ساخته شده است. تصمیم بگیرید که آیا پستهای شما با پیگیرهایتان، فقط افرادی که نام میبرید، یا کل دنیا به اشتراک گذاشته شود. هشدارهای محتوا به شما امکان میدهند پستهای حاوی مطالب حساس یا محرک را پنهان کنید تا زمانی که آماده تعامل با آنها باشید. هر اجتماعای دستورالعملها و ناظران خود را دارد تا اعضای خود را ایمن نگه دارد و ابزارهای قوی مسدود کردن و گزارشدهی به جلوگیری از سوء استفاده کمک میکند.
|
||||
|
||||
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 +1 @@
|
||||
Decentralized social network
|
||||
شبکه اجتماعی نامتمرکز
|
||||
@@ -1,4 +1,4 @@
|
||||
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke gemeenschappen die allemaal naadloos met elkaar kunnen communiceren. Wat je ook leuk vindt, je kunt gepassioneerde mensen volgen die het met je willen delen op Mastodon!
|
||||
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke communities die allemaal naadloos met elkaar kunnen communiceren. Wat je ook leuk vindt, je kunt gepassioneerde mensen volgen die het met je willen delen op Mastodon!
|
||||
|
||||
Meld je aan bij een community en maak je profiel aan. Vind en volg fascinerende mensen en lees hun berichten in een advertentievrije, chronologische tijdlijn. Druk jezelf uit met aangepaste emoji, afbeeldingen, GIF’s, video’s en audio in berichten van 500 karakters. Reageer op discussies en boost berichten van anderen om geweldige dingen te delen. Vind nieuwe accounts om te volgen en hashtags om je netwerk uit te breiden.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
Mạng xã hội phi tập trung
|
||||
Mạng xã hội liên hợp
|
||||
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Thu Jan 13 11:33:43 MSK 2022
|
||||
#Sat Jun 03 23:40:27 MSK 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 54
|
||||
versionName "1.2.3"
|
||||
versionCode 60
|
||||
versionName "2.0.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
@@ -37,6 +37,9 @@ android {
|
||||
githubRelease{
|
||||
initWith release
|
||||
}
|
||||
githubDebug{
|
||||
initWith debug
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -53,6 +56,9 @@ android {
|
||||
githubRelease{
|
||||
setRoot "src/github"
|
||||
}
|
||||
githubDebug{
|
||||
setRoot "src/github"
|
||||
}
|
||||
}
|
||||
lintOptions{
|
||||
checkReleaseBuilds false
|
||||
@@ -69,14 +75,15 @@ dependencies {
|
||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.7'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.8'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
implementation 'de.psdev:async-otto:1.0.3'
|
||||
implementation 'org.parceler:parceler-api:1.1.12'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
def appCenterSdkVersion = "4.4.2"
|
||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
@@ -84,8 +91,8 @@ dependencies {
|
||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.*;
|
||||
@LargeTest
|
||||
public class StoreScreenshotsGenerator{
|
||||
private static final String PHOTO_FILE="IMG_1010.jpg";
|
||||
private static final long LOAD_WAIT_TIMEOUT=20_000;
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
|
||||
@@ -84,14 +85,14 @@ public class StoreScreenshotsGenerator{
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||
MastodonApp.context.deleteDatabase(session.getID()+".db");
|
||||
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("HomeTimeline");
|
||||
|
||||
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
|
||||
activityScenarioRule.getScenario().recreate();
|
||||
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("HomeTimeline_Dark");
|
||||
|
||||
@@ -100,8 +101,8 @@ public class StoreScreenshotsGenerator{
|
||||
|
||||
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
|
||||
Thread.sleep(500);
|
||||
onView(isRoot()).perform(waitId(R.id.avatar_border, 5000)); // wait for profile to load
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000)); // wait for timeline to load
|
||||
onView(isRoot()).perform(waitId(R.id.avatar_border, LOAD_WAIT_TIMEOUT)); // wait for profile to load
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT)); // wait for timeline to load
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("Profile");
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||
if(timeSinceLastCheck>CHECK_PERIOD){
|
||||
if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
@@ -109,23 +109,26 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
try(Response resp=call.execute()){
|
||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)");
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)(?:\\.(\\d+))?");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||
return;
|
||||
}
|
||||
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=Integer.parseInt(matcher.group(3));
|
||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
if(!matcher.find()){
|
||||
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=matcher.group(3)!=null ? Integer.parseInt(matcher.group(3)) : 0;
|
||||
Matcher curMatcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
if(!curMatcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||
return;
|
||||
}
|
||||
int curMajor=Integer.parseInt(matcher.group(1)), curMinor=Integer.parseInt(matcher.group(2)), curRevision=Integer.parseInt(matcher.group(3));
|
||||
int curMajor=Integer.parseInt(curMatcher.group(1)), curMinor=Integer.parseInt(curMatcher.group(2)), curRevision=matcher.group(3)!=null ? Integer.parseInt(curMatcher.group(3)) : 0;
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || BuildConfig.DEBUG){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision;
|
||||
if(newVersion>curVersion || forceUpdate){
|
||||
forceUpdate=false;
|
||||
String version=newMajor+"."+newMinor;
|
||||
if(matcher.group(3)!=null)
|
||||
version+="."+newRevision;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
JsonObject asset=el.getAsJsonObject();
|
||||
@@ -295,6 +298,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
getPrefs().edit().clear().apply();
|
||||
File apk=getUpdateApkFile();
|
||||
if(apk.exists())
|
||||
apk.delete();
|
||||
state=UpdateState.NO_UPDATE;
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Mastodon.AutoLightDark"
|
||||
android:largeHeap="true">
|
||||
|
||||
@@ -37,6 +36,18 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="https" android:host="mastodon.social" android:pathPrefix="/@"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="https" android:host="mastodon.online" android:pathPrefix="/@"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
|
||||
51
mastodon/src/main/assets/server_about_template.htm
Normal file
51
mastodon/src/main/assets/server_about_template.htm
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
body{
|
||||
background: {{colorSurface}};
|
||||
padding: 16px 16px 0 16px;
|
||||
margin: 0;
|
||||
color: {{colorOnSurface}};
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
color: {{colorPrimary}};
|
||||
}
|
||||
p, h1, h2, h3, h4, h5, h6, ul, ol{
|
||||
margin-bottom: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
h1, h2{
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
h3, h4, h5, h6{
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
b, strong{
|
||||
font-weight: 500;
|
||||
}
|
||||
ul, ol{
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
ul>li, ol>li{
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
|
||||
private static HashSet<Callback> callbacks=new HashSet<>();
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
||||
private boolean resumeAfterAudioFocusGain;
|
||||
private boolean isBuffering=true;
|
||||
|
||||
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
@@ -176,6 +176,7 @@ public class AudioPlayerService extends Service{
|
||||
player.setOnErrorListener(this::onPlayerError);
|
||||
player.setOnCompletionListener(this::onPlayerCompletion);
|
||||
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
||||
player.setOnInfoListener(this::onPlayerInfo);
|
||||
try{
|
||||
player.setDataSource(this, Uri.parse(attachment.url));
|
||||
player.prepareAsync();
|
||||
@@ -187,7 +188,9 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
private void onPlayerPrepared(MediaPlayer mp){
|
||||
Log.i(TAG, "onPlayerPrepared");
|
||||
playerReady=true;
|
||||
isBuffering=false;
|
||||
player.start();
|
||||
updateSessionState(false);
|
||||
}
|
||||
@@ -205,6 +208,21 @@ public class AudioPlayerService extends Service{
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
|
||||
switch(what){
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
||||
isBuffering=true;
|
||||
updateSessionState(false);
|
||||
}
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
||||
isBuffering=false;
|
||||
updateSessionState(false);
|
||||
}
|
||||
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onAudioFocusChanged(int change){
|
||||
switch(change){
|
||||
case AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
@@ -212,7 +230,7 @@ public class AudioPlayerService extends Service{
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
resumeAfterAudioFocusGain=true;
|
||||
resumeAfterAudioFocusGain=isPlaying();
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
@@ -232,12 +250,16 @@ public class AudioPlayerService extends Service{
|
||||
|
||||
private void updateSessionState(boolean removeNotification){
|
||||
session.setPlaybackState(new PlaybackState.Builder()
|
||||
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f)
|
||||
.setState(switch(getPlayState()){
|
||||
case PLAYING -> PlaybackState.STATE_PLAYING;
|
||||
case PAUSED -> PlaybackState.STATE_PAUSED;
|
||||
case BUFFERING -> PlaybackState.STATE_BUFFERING;
|
||||
}, player.getCurrentPosition(), 1f)
|
||||
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
||||
.build());
|
||||
updateNotification(!player.isPlaying(), removeNotification);
|
||||
for(Callback cb:callbacks)
|
||||
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition());
|
||||
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
|
||||
}
|
||||
|
||||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||
@@ -310,6 +332,12 @@ public class AudioPlayerService extends Service{
|
||||
return attachment.id;
|
||||
}
|
||||
|
||||
public PlayState getPlayState(){
|
||||
if(isBuffering)
|
||||
return PlayState.BUFFERING;
|
||||
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
|
||||
}
|
||||
|
||||
public static void registerCallback(Callback cb){
|
||||
callbacks.add(cb);
|
||||
}
|
||||
@@ -333,7 +361,13 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onPlayStateChanged(String attachmentID, boolean playing, int position);
|
||||
void onPlayStateChanged(String attachmentID, PlayState state, int position);
|
||||
void onPlaybackStopped(String attachmentID);
|
||||
}
|
||||
|
||||
public enum PlayState{
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
BUFFERING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.SharedPreferences;
|
||||
public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean trueBlackTheme;
|
||||
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||
public static ThemePreference theme;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
@@ -17,7 +17,10 @@ public class GlobalUserPreferences{
|
||||
SharedPreferences prefs=getPrefs();
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
altTextReminders=prefs.getBoolean("altTextReminders", false);
|
||||
confirmUnfollow=prefs.getBoolean("confirmUnfollow", false);
|
||||
confirmBoost=prefs.getBoolean("confirmBoost", false);
|
||||
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
}
|
||||
|
||||
@@ -25,8 +28,11 @@ public class GlobalUserPreferences{
|
||||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putBoolean("altTextReminders", altTextReminders)
|
||||
.putBoolean("confirmUnfollow", confirmUnfollow)
|
||||
.putBoolean("confirmBoost", confirmBoost)
|
||||
.putBoolean("confirmDeletePost", confirmDeletePost)
|
||||
.apply();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@ import android.Manifest;
|
||||
import android.app.Application;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
@@ -20,6 +22,7 @@ import org.joinmastodon.android.fragments.SplashFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
@@ -28,6 +31,9 @@ import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
@@ -64,6 +70,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}else{
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
@@ -105,11 +113,55 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||
}*/
|
||||
}
|
||||
|
||||
public void handleURL(Uri uri, String accountID){
|
||||
if(uri==null)
|
||||
return;
|
||||
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
|
||||
return;
|
||||
AccountSession session;
|
||||
if(accountID==null)
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
else
|
||||
session=AccountSessionManager.get(accountID);
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
||||
}
|
||||
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
||||
new GetSearchResults(q, null, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if(result.statuses!=null && !result.statuses.isEmpty()){
|
||||
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
|
||||
Nav.go(MainActivity.this, ThreadFragment.class, args);
|
||||
}else if(result.accounts!=null && !result.accounts.isEmpty()){
|
||||
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
|
||||
Nav.go(MainActivity.this, ProfileFragment.class, args);
|
||||
}else{
|
||||
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(MainActivity.this);
|
||||
}
|
||||
})
|
||||
.wrapProgress(this, progressText, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void showFragmentForNotification(Notification notification, String accountID){
|
||||
Fragment fragment;
|
||||
Bundle args=new Bundle();
|
||||
|
||||
@@ -3,11 +3,10 @@ package org.joinmastodon.android;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.NetworkUtils;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -30,5 +29,8 @@ public class MastodonApp extends Application{
|
||||
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
GlobalUserPreferences.load();
|
||||
if(BuildConfig.DEBUG){
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
|
||||
return;
|
||||
}
|
||||
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
|
||||
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
|
||||
return;
|
||||
}
|
||||
String accountID=account.getID();
|
||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||
new GetNotificationByID(pn.notificationId+"")
|
||||
|
||||
@@ -15,19 +15,17 @@ import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -35,13 +33,15 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=2;
|
||||
private static final int DB_VERSION=3;
|
||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
private final String accountID;
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@@ -57,28 +57,23 @@ public class CacheController{
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Status> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +85,9 @@ public class CacheController{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -113,7 +110,7 @@ public class CacheController{
|
||||
runOnDbThread((db)->{
|
||||
if(clear)
|
||||
db.delete("home_timeline", null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Status s:posts){
|
||||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
@@ -121,6 +118,7 @@ public class CacheController{
|
||||
if(s.hasGapAfter)
|
||||
flags|=POST_FLAG_GAP_AFTER;
|
||||
values.put("flags", flags);
|
||||
values.put("time", s.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
@@ -130,28 +128,27 @@ public class CacheController{
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
if(!onlyMentions && loadingNotifications){
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
pendingNotificationsCallbacks.add(callback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Notification> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||
return;
|
||||
}
|
||||
@@ -159,26 +156,40 @@ public class CacheController{
|
||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||
}
|
||||
}
|
||||
if(!onlyMentions)
|
||||
loadingNotifications=true;
|
||||
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
|
||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
callback.onSuccess(res);
|
||||
putNotifications(result, onlyMentions, maxID==null);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onSuccess(res);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
callback.onError(error);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onError(error);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -196,7 +207,7 @@ public class CacheController{
|
||||
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
if(clear)
|
||||
db.delete(table, null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Notification n:notifications){
|
||||
if(n.type==null){
|
||||
continue;
|
||||
@@ -204,6 +215,7 @@ public class CacheController{
|
||||
values.put("id", n.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||
values.put("type", n.type.ordinal());
|
||||
values.put("time", n.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
@@ -300,30 +312,36 @@ public class CacheController{
|
||||
CREATE TABLE `home_timeline` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_all` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_mentions` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
createRecentSearchesTable(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||
if(oldVersion==1){
|
||||
if(oldVersion<2){
|
||||
createRecentSearchesTable(db);
|
||||
}
|
||||
if(oldVersion<3){
|
||||
addTimeColumns(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||
@@ -334,6 +352,15 @@ public class CacheController{
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
}
|
||||
|
||||
private void addTimeColumns(SQLiteDatabase db){
|
||||
db.execSQL("DELETE FROM `home_timeline`");
|
||||
db.execSQL("DELETE FROM `notifications_all`");
|
||||
db.execSQL("DELETE FROM `notifications_mentions`");
|
||||
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -122,13 +122,17 @@ public class MastodonAPIController{
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(respJson, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}else{
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(reader, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
|
||||
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
|
||||
super(method, path, (Class<Void>)null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
|
||||
public GetAccountFeaturedHashtags(String id){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("exclude_reblogs", "true");
|
||||
}
|
||||
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
||||
case PINNED -> addQueryParameter("pinned", "true");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
INCLUDE_REPLIES,
|
||||
MEDIA,
|
||||
NO_REBLOGS,
|
||||
OWN_POSTS_AND_REPLIES
|
||||
OWN_POSTS_AND_REPLIES,
|
||||
PINNED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Boolean locked, discoverable;
|
||||
public RequestSource source;
|
||||
|
||||
public Request(Boolean locked, Boolean discoverable, RequestSource source){
|
||||
this.locked=locked;
|
||||
this.discoverable=discoverable;
|
||||
this.source=source;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestSource{
|
||||
public StatusPrivacy privacy;
|
||||
public String language;
|
||||
|
||||
public RequestSource(StatusPrivacy privacy, String language){
|
||||
this.privacy=privacy;
|
||||
this.language=language;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CreateFilter extends MastodonAPIRequest<Filter>{
|
||||
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
|
||||
super(HttpMethod.POST, "/filters", Filter.class);
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteFilter extends ResultlessMastodonAPIRequest{
|
||||
public DeleteFilter(String id){
|
||||
super(HttpMethod.DELETE, "/filters/"+id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
class FilterRequest{
|
||||
public String title;
|
||||
public EnumSet<FilterContext> context;
|
||||
public FilterAction filterAction;
|
||||
public Integer expiresIn;
|
||||
public List<KeywordAttribute> keywordsAttributes;
|
||||
|
||||
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
|
||||
this.title=title;
|
||||
this.context=context;
|
||||
this.filterAction=filterAction;
|
||||
this.expiresIn=expiresIn;
|
||||
this.keywordsAttributes=keywordsAttributes;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetWordFilters(){
|
||||
public class GetFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
|
||||
public GetLegacyFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
public Boolean delete;
|
||||
public String keyword;
|
||||
public Boolean wholeWord;
|
||||
|
||||
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
|
||||
this.id=id;
|
||||
this.delete=delete;
|
||||
this.keyword=keyword;
|
||||
this.wholeWord=wholeWord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class UpdateFilter extends MastodonAPIRequest<Filter>{
|
||||
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
|
||||
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
|
||||
|
||||
List<KeywordAttribute> attrs=Stream.of(
|
||||
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
|
||||
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
|
||||
).flatMap(Function.identity()).collect(Collectors.toList());
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
|
||||
public GetInstanceExtendedDescription(){
|
||||
super(HttpMethod.GET, "/instance/extended_description", Response.class);
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Instant updatedAt;
|
||||
public String content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
||||
public class GetMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||
public GetMarkers(){
|
||||
super(HttpMethod.GET, "/markers", TimelineMarkers.class);
|
||||
addQueryParameter("timeline[]", "home");
|
||||
addQueryParameter("timeline[]", "notifications");
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
||||
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
||||
super(HttpMethod.POST, "/markers", Response.class);
|
||||
super(HttpMethod.POST, "/markers", TimelineMarkers.class);
|
||||
JsonObjectBuilder builder=new JsonObjectBuilder();
|
||||
if(lastSeenHomePostID!=null)
|
||||
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
||||
@@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Marker home, notifications;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
|
||||
addQueryParameter("resolve", "true");
|
||||
}
|
||||
|
||||
public GetSearchResults limit(int limit){
|
||||
addQueryParameter("limit", String.valueOf(limit));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class AccountLocalPreferences{
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public boolean showInteractionCounts;
|
||||
public boolean customEmojiInNames;
|
||||
public boolean showCWs;
|
||||
public boolean hideSensitiveMedia;
|
||||
public boolean serverSideFiltersSupported;
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs){
|
||||
this.prefs=prefs;
|
||||
showInteractionCounts=prefs.getBoolean("interactionCounts", true);
|
||||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
showCWs=prefs.getBoolean("showCWs", true);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
return prefs.getLong("notificationsPauseTime", 0L);
|
||||
}
|
||||
|
||||
public void setNotificationsPauseEndTime(long time){
|
||||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||
}
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("showCWs", showCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,48 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class AccountSession{
|
||||
private static final String TAG="AccountSession";
|
||||
|
||||
public Token token;
|
||||
public Account self;
|
||||
public String domain;
|
||||
@@ -26,13 +55,17 @@ public class AccountSession{
|
||||
public PushSubscription pushSubscription;
|
||||
public boolean needUpdatePushSettings;
|
||||
public long filtersLastUpdated;
|
||||
public List<Filter> wordFilters=new ArrayList<>();
|
||||
public List<LegacyFilter> wordFilters=new ArrayList<>();
|
||||
public String pushAccountID;
|
||||
public AccountActivationInfo activationInfo;
|
||||
public Preferences preferences;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController;
|
||||
private transient CacheController cacheController;
|
||||
private transient PushSubscriptionManager pushSubscriptionManager;
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -73,4 +106,159 @@ public class AccountSession{
|
||||
pushSubscriptionManager=new PushSubscriptionManager(getID());
|
||||
return pushSubscriptionManager;
|
||||
}
|
||||
|
||||
public String getFullUsername(){
|
||||
return '@'+self.username+'@'+domain;
|
||||
}
|
||||
|
||||
public void reloadPreferences(Consumer<Preferences> callback){
|
||||
new GetPreferences()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Preferences result){
|
||||
preferences=result;
|
||||
if(callback!=null)
|
||||
callback.accept(result);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public SharedPreferences getRawLocalPreferences(){
|
||||
if(prefs==null)
|
||||
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public void reloadNotificationsMarker(Consumer<String> callback){
|
||||
new GetMarkers()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(TimelineMarkers result){
|
||||
if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){
|
||||
String id=result.notifications.lastReadId;
|
||||
String lastKnown=getLastKnownNotificationsMarker();
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){
|
||||
// Marker moved back -- previous marker update must have failed.
|
||||
// Pretend it didn't happen and repeat the request.
|
||||
id=lastKnown;
|
||||
new SaveMarkers(null, id).exec(getID());
|
||||
}
|
||||
callback.accept(id);
|
||||
setNotificationsMarker(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public String getLastKnownNotificationsMarker(){
|
||||
return getRawLocalPreferences().getString("notificationsMarker", null);
|
||||
}
|
||||
|
||||
public void setNotificationsMarker(String id, boolean clearUnread){
|
||||
getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
|
||||
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
|
||||
}
|
||||
|
||||
public void logOut(Activity activity, Runnable onDone){
|
||||
new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public void savePreferencesLater(){
|
||||
preferencesNeedSaving=true;
|
||||
}
|
||||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
preferencesNeedSaving=false;
|
||||
self=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.e(TAG, "failed to save preferences: "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
}
|
||||
|
||||
public AccountLocalPreferences getLocalPreferences(){
|
||||
if(localPreferences==null)
|
||||
localPreferences=new AccountLocalPreferences(getRawLocalPreferences());
|
||||
return localPreferences;
|
||||
}
|
||||
|
||||
public void filterStatuses(List<Status> statuses, FilterContext context){
|
||||
filterStatusContainingObjects(statuses, Function.identity(), context);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
if(s.filtered==null)
|
||||
return false;
|
||||
for(FilterResult filter:s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(wordFilters==null)
|
||||
return;
|
||||
for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
getLocalPreferences().serverSideFiltersSupported=true;
|
||||
getLocalPreferences().save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
for(LegacyFilter filter:wordFilters){
|
||||
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
@@ -22,7 +20,7 @@ import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
@@ -32,7 +30,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
@@ -142,6 +140,10 @@ public class AccountSessionManager{
|
||||
return session;
|
||||
}
|
||||
|
||||
public static AccountSession get(String id){
|
||||
return getInstance().getAccount(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(String id){
|
||||
return sessions.get(id);
|
||||
@@ -174,12 +176,19 @@ public class AccountSessionManager{
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
MastodonApp.context.deleteSharedPreferences(id);
|
||||
}else{
|
||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
||||
}
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
if(sessions.isEmpty())
|
||||
lastActiveAccountID=null;
|
||||
else
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
@@ -251,7 +260,7 @@ public class AccountSessionManager{
|
||||
if(now-session.infoLastUpdated>24L*3600_000L){
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
if(now-session.filtersLastUpdated>3600_000L){
|
||||
if(!session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L){
|
||||
updateSessionWordFilters(session);
|
||||
}
|
||||
}
|
||||
@@ -289,10 +298,10 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
private void updateSessionWordFilters(AccountSession session){
|
||||
new GetWordFilters()
|
||||
new GetLegacyFilters()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
public void onSuccess(List<LegacyFilter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationsMarkerUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final String marker;
|
||||
public final boolean clearUnread;
|
||||
|
||||
public NotificationsMarkerUpdatedEvent(String accountID, String marker, boolean clearUnread){
|
||||
this.accountID=accountID;
|
||||
this.marker=marker;
|
||||
this.clearUnread=clearUnread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
|
||||
public class SettingsFilterCreatedOrUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final Filter filter;
|
||||
|
||||
public SettingsFilterCreatedOrUpdatedEvent(String accountID, Filter filter){
|
||||
this.accountID=accountID;
|
||||
this.filter=filter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class SettingsFilterDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String filterID;
|
||||
|
||||
public SettingsFilterDeletedEvent(String accountID, String filterID){
|
||||
this.accountID=accountID;
|
||||
this.filterID=filterID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class StatusDisplaySettingsChangedEvent{
|
||||
public final String accountID;
|
||||
|
||||
public StatusDisplaySettingsChangedEvent(String accountID){
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package org.joinmastodon.android.fragments;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
@@ -10,28 +12,36 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
|
||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AccountTimelineFragment extends StatusListFragment{
|
||||
private Account user;
|
||||
private GetAccountStatuses.Filter filter;
|
||||
private HorizontalScrollView filtersBar;
|
||||
private FilterChipView defaultFilter, withRepliesFilter, mediaFilter;
|
||||
|
||||
public AccountTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_no_refresh);
|
||||
}
|
||||
|
||||
public static AccountTimelineFragment newInstance(String accountID, Account profileAccount, GetAccountStatuses.Filter filter, boolean load){
|
||||
public static AccountTimelineFragment newInstance(String accountID, Account profileAccount, boolean load){
|
||||
AccountTimelineFragment f=new AccountTimelineFragment();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
|
||||
args.putString("filter", filter.toString());
|
||||
if(!load)
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
args.putBoolean("__is_tab", true);
|
||||
@@ -41,22 +51,22 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter"));
|
||||
filter=GetAccountStatuses.Filter.DEFAULT;
|
||||
super.onAttach(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(user==null) // TODO figure out why this happens
|
||||
return;
|
||||
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
boolean empty=result.isEmpty();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.ACCOUNT);
|
||||
onDataLoaded(result, !empty);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -65,6 +75,7 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.setBackground(null); // prevents unnecessary overdraw
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,22 +85,95 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
loadData();
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
||||
protected void onStatusCreated(Status status){
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, status.account))
|
||||
return;
|
||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||
// Keep replies to self, discard all other replies
|
||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
if(status.inReplyToAccountId!=null && !status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
return;
|
||||
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
||||
if(ev.status.mediaAttachments.isEmpty())
|
||||
if(status.mediaAttachments.isEmpty())
|
||||
return;
|
||||
}
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
prependItems(Collections.singletonList(status), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
filtersBar=new HorizontalScrollView(getActivity());
|
||||
LinearLayout filtersLayout=new LinearLayout(getActivity());
|
||||
filtersBar.addView(filtersLayout);
|
||||
filtersLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
filtersLayout.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(8));
|
||||
filtersLayout.setDividerDrawable(new EmptyDrawable(V.dp(8), 1));
|
||||
filtersLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
|
||||
|
||||
defaultFilter=new FilterChipView(getActivity());
|
||||
defaultFilter.setText(R.string.posts);
|
||||
defaultFilter.setTag(GetAccountStatuses.Filter.DEFAULT);
|
||||
defaultFilter.setSelected(filter==GetAccountStatuses.Filter.DEFAULT);
|
||||
defaultFilter.setOnClickListener(this::onFilterClick);
|
||||
filtersLayout.addView(defaultFilter);
|
||||
|
||||
withRepliesFilter=new FilterChipView(getActivity());
|
||||
withRepliesFilter.setText(R.string.posts_and_replies);
|
||||
withRepliesFilter.setTag(GetAccountStatuses.Filter.INCLUDE_REPLIES);
|
||||
withRepliesFilter.setSelected(filter==GetAccountStatuses.Filter.INCLUDE_REPLIES);
|
||||
withRepliesFilter.setOnClickListener(this::onFilterClick);
|
||||
filtersLayout.addView(withRepliesFilter);
|
||||
|
||||
mediaFilter=new FilterChipView(getActivity());
|
||||
mediaFilter.setText(R.string.media);
|
||||
mediaFilter.setTag(GetAccountStatuses.Filter.MEDIA);
|
||||
mediaFilter.setSelected(filter==GetAccountStatuses.Filter.MEDIA);
|
||||
mediaFilter.setOnClickListener(this::onFilterClick);
|
||||
filtersLayout.addView(mediaFilter);
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(filtersBar));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMainAdapterOffset(){
|
||||
return super.getMainAdapterOffset()+1;
|
||||
}
|
||||
|
||||
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
|
||||
return switch(filter){
|
||||
case DEFAULT -> defaultFilter;
|
||||
case INCLUDE_REPLIES -> withRepliesFilter;
|
||||
case MEDIA -> mediaFilter;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+filter);
|
||||
};
|
||||
}
|
||||
|
||||
private void onFilterClick(View v){
|
||||
GetAccountStatuses.Filter newFilter=(GetAccountStatuses.Filter) v.getTag();
|
||||
if(newFilter==filter)
|
||||
return;
|
||||
// TODO maybe cache the filtered timelines that were already loaded?
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
getViewForFilter(filter).setSelected(false);
|
||||
filter=newFilter;
|
||||
v.setSelected(true);
|
||||
data.clear();
|
||||
preloadedData.clear();
|
||||
int size=displayItems.size();
|
||||
displayItems.clear();
|
||||
adapter.notifyItemRangeRemoved(0, size);
|
||||
loaded=false;
|
||||
dataLoading=true;
|
||||
doLoadData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,9 @@ import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
@@ -29,25 +24,26 @@ import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.MediaGridLayout;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -56,15 +52,15 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends MastodonRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{
|
||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||
protected DisplayItemsAdapter adapter;
|
||||
protected String accountID;
|
||||
@@ -267,7 +263,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
private Rect tmpRect=new Rect();
|
||||
@Override
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
}else{
|
||||
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
|
||||
}
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
|
||||
@@ -312,6 +312,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
protected int getMainAdapterOffset(){
|
||||
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
|
||||
return mergeAdapter.getPositionForAdapter(adapter);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -323,6 +326,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void onItemClick(String id);
|
||||
|
||||
protected void updatePoll(String itemID, Status status, Poll poll){
|
||||
@@ -405,54 +412,27 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
revealSpoiler(status, holder.getItemID());
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
revealSpoiler(status, holder.getItemID());
|
||||
}
|
||||
|
||||
protected void revealSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=true;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
updateImagesSpoilerState(status, itemID);
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||
}
|
||||
}
|
||||
holder.rebind();
|
||||
updateImagesSpoilerState(status, holder.getItemID());
|
||||
}
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null)
|
||||
spoiler.rebind();
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
|
||||
protected void updateImagesSpoilerState(Status status, String itemID){
|
||||
ArrayList<Integer> updatedPositions=new ArrayList<>();
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
mediaGrid.setRevealed(status.spoilerRevealed);
|
||||
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
}
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
i++;
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
||||
}else{
|
||||
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||
}
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
@@ -580,6 +560,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
displayItems.addAll(buildDisplayItems(item));
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
@@ -589,7 +579,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@NonNull
|
||||
@Override
|
||||
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent);
|
||||
BindableViewHolder<StatusDisplayItem> holder=(BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent, BaseStatusListFragment.this);
|
||||
onModifyItemViewHolder(holder);
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -620,15 +612,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
|
||||
private Paint dividerPaint=new Paint(), hiddenMediaPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Typeface mediumTypeface=Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private Layout mediaHiddenTitleLayout, mediaHiddenTextLayout, tapToRevealTextLayout;
|
||||
private int currentMediaHiddenLayoutsWidth=0;
|
||||
private Paint dividerPaint=new Paint();
|
||||
|
||||
{
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
|
||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||
dividerPaint.setStrokeWidth(V.dp(1));
|
||||
dividerPaint.setStrokeWidth(V.dp(0.5f));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -638,79 +627,22 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
View bottomSibling=parent.getChildAt(i+1);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
||||
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
||||
if(needDrawDivider(holder, siblingHolder)){
|
||||
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
|
||||
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
|
||||
hiddenMediaPaint.setColor(0x80000000);
|
||||
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
|
||||
}
|
||||
}
|
||||
private boolean needDrawDivider(RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
if(needDividerForExtraItem(holder.itemView, siblingHolder.itemView, holder, siblingHolder))
|
||||
return true;
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh){
|
||||
// Do not draw dividers between hashtag and/or account rows
|
||||
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
|
||||
return false;
|
||||
return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
|
||||
}
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
|
||||
if(!imgHolder.getItem().status.spoilerRevealed){
|
||||
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
|
||||
int listWidth=getListWidthForMediaLayout();
|
||||
int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH));
|
||||
if(currentMediaHiddenLayoutsWidth!=width)
|
||||
rebuildMediaHiddenLayouts(width-V.dp(32));
|
||||
c.save();
|
||||
float totalHeight;
|
||||
boolean hiddenByAuthor=imgHolder.getItem().status.sensitive;
|
||||
if(hiddenByAuthor)
|
||||
totalHeight=mediaHiddenTitleLayout.getHeight()+mediaHiddenTextLayout.getHeight()+V.dp(8);
|
||||
else
|
||||
totalHeight=tapToRevealTextLayout.getHeight();
|
||||
c.translate(child.getX()+V.dp(16), child.getY()+child.getHeight()/2f-totalHeight/2f);
|
||||
if(hiddenByAuthor){
|
||||
mediaHiddenTitleLayout.draw(c);
|
||||
c.translate(0, mediaHiddenTitleLayout.getHeight()+V.dp(8));
|
||||
mediaHiddenTextLayout.draw(c);
|
||||
}else{
|
||||
tapToRevealTextLayout.draw(c);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildMediaHiddenLayouts(int width){
|
||||
currentMediaHiddenLayoutsWidth=width;
|
||||
String title=getString(R.string.sensitive_content);
|
||||
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
titlePaint.setColor(getResources().getColor(R.color.gray_50));
|
||||
titlePaint.setTextSize(V.dp(22));
|
||||
titlePaint.setTypeface(mediumTypeface);
|
||||
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.build();
|
||||
String tapToReveal=getString(R.string.tap_to_reveal);
|
||||
tapToRevealTextLayout=StaticLayout.Builder.obtain(tapToReveal, 0, tapToReveal.length(), titlePaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.build();
|
||||
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
textPaint.setColor(getResources().getColor(R.color.gray_200));
|
||||
textPaint.setTextSize(V.dp(16));
|
||||
String text=getString(R.string.sensitive_content_explain);
|
||||
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.setLineSpacing(V.dp(5), 1f)
|
||||
.build();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -34,4 +35,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -12,28 +20,34 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.parceler.Parcels;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import java.util.Collections;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
|
||||
private static final String TAG="ComposeImageDescription";
|
||||
|
||||
private String accountID, attachmentID;
|
||||
private EditText edit;
|
||||
private Button saveButton;
|
||||
private FixedAspectRatioImageView image;
|
||||
private ContextThemeWrapper themeWrapper;
|
||||
private PhotoViewer photoViewer;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -46,7 +60,13 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.edit_image);
|
||||
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
|
||||
setTitle(R.string.add_alt_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
return super.onCreateView(themeWrapper.getSystemService(LayoutInflater.class), container, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,14 +74,48 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
View view=inflater.inflate(R.layout.fragment_image_description, container, false);
|
||||
|
||||
edit=view.findViewById(R.id.edit);
|
||||
ImageView image=view.findViewById(R.id.photo);
|
||||
image=view.findViewById(R.id.photo);
|
||||
int width=getArguments().getInt("width", 0);
|
||||
int height=getArguments().getInt("height", 0);
|
||||
if(width>0 && height>0){
|
||||
image.setAspectRatio(Math.max(1f, (float)width/height));
|
||||
}
|
||||
image.setOnClickListener(v->openPhotoViewer());
|
||||
Uri uri=getArguments().getParcelable("uri");
|
||||
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
|
||||
Attachment.Type type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
|
||||
if(type==Attachment.Type.IMAGE)
|
||||
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
|
||||
else
|
||||
loadVideoThumbIntoView(image, uri);
|
||||
edit.setText(getArguments().getString("existingDescription"));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void loadVideoThumbIntoView(ImageView target, Uri uri){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Context context=getActivity();
|
||||
if(context==null)
|
||||
return;
|
||||
try{
|
||||
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
|
||||
mmr.setDataSource(context, uri);
|
||||
Bitmap frame=mmr.getFrameAtTime(3_000_000);
|
||||
mmr.release();
|
||||
int size=Math.max(frame.getWidth(), frame.getHeight());
|
||||
int maxSize=V.dp(250);
|
||||
if(size>maxSize){
|
||||
float factor=maxSize/(float)size;
|
||||
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
|
||||
}
|
||||
Bitmap finalFrame=frame;
|
||||
target.post(()->target.setImageBitmap(finalFrame));
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -71,43 +125,114 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle});
|
||||
int buttonStyle=ta.getResourceId(0, 0);
|
||||
ta.recycle();
|
||||
saveButton=new Button(getActivity(), null, 0, buttonStyle);
|
||||
saveButton.setText(R.string.save);
|
||||
saveButton.setOnClickListener(this::onSaveClick);
|
||||
FrameLayout wrap=new FrameLayout(getActivity());
|
||||
wrap.addView(saveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
|
||||
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
wrap.setClipToPadding(false);
|
||||
MenuItem item=menu.add(R.string.publish);
|
||||
item.setActionView(wrap);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
inflater.inflate(R.menu.compose_image_description, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.help){
|
||||
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
|
||||
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
|
||||
for(BulletSpan span:spans){
|
||||
BulletSpan betterSpan;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
|
||||
else
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
|
||||
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
|
||||
msg.removeSpan(span);
|
||||
}
|
||||
new M3AlertDialogBuilder(themeWrapper)
|
||||
.setTitle(R.string.what_is_alt_text)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSaveClick(View v){
|
||||
new UpdateAttachment(attachmentID, edit.getText().toString().trim())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
Bundle r=new Bundle();
|
||||
r.putParcelable("attachment", Parcels.wrap(result));
|
||||
setResult(true, r);
|
||||
Nav.finish(ComposeImageDescriptionFragment.this);
|
||||
}
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
deliverResult();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, false)
|
||||
.exec(accountID);
|
||||
@Override
|
||||
protected LayoutInflater getToolbarLayoutInflater(){
|
||||
return LayoutInflater.from(themeWrapper);
|
||||
}
|
||||
|
||||
private void deliverResult(){
|
||||
Bundle r=new Bundle();
|
||||
r.putString("text", edit.getText().toString().trim());
|
||||
r.putString("attachment", attachmentID);
|
||||
setResult(true, r);
|
||||
}
|
||||
|
||||
private void openPhotoViewer(){
|
||||
Attachment fakeAttachment=new Attachment();
|
||||
fakeAttachment.id="local";
|
||||
fakeAttachment.type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
|
||||
int width=getArguments().getInt("width", 0);
|
||||
int height=getArguments().getInt("height", 0);
|
||||
Uri uri=getArguments().getParcelable("uri");
|
||||
fakeAttachment.url=uri.toString();
|
||||
fakeAttachment.meta=new Attachment.Metadata();
|
||||
fakeAttachment.meta.width=width;
|
||||
fakeAttachment.meta.height=height;
|
||||
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
image.setAlpha(visible ? 1f : 0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||
int[] pos={0, 0};
|
||||
image.getLocationOnScreen(pos);
|
||||
outRect.set(pos[0], pos[1], pos[0]+image.getWidth(), pos[1]+image.getHeight());
|
||||
image.setElevation(1f);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
|
||||
image.setTranslationX(translateX);
|
||||
image.setTranslationY(translateY);
|
||||
image.setScaleX(scale);
|
||||
image.setScaleY(scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endPhotoViewTransition(){
|
||||
Drawable d=image.getDrawable();
|
||||
image.setImageDrawable(null);
|
||||
image.setImageDrawable(d);
|
||||
|
||||
image.setTranslationX(0f);
|
||||
image.setTranslationY(0f);
|
||||
image.setScaleX(1f);
|
||||
image.setScaleY(1f);
|
||||
image.setElevation(0f);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Drawable getPhotoViewCurrentDrawable(int index){
|
||||
return image.getDrawable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
photoViewer=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissions(String[] permissions){
|
||||
|
||||
}
|
||||
});
|
||||
photoViewer.removeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
|
||||
private Account account;
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
onDataLoaded(getArguments().getParcelableArrayList("hashtags").stream().map(p->(Hashtag)Parcels.unwrap(p)).collect(Collectors.toList()), false);
|
||||
setTitle(R.string.hashtags);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Hashtag s){
|
||||
return Collections.singletonList(new HashtagStatusDisplayItem(s.name, this, s));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Hashtag s){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,6 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
@@ -44,7 +57,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
private FragmentRootLinearLayout content;
|
||||
private HomeTimelineFragment homeTimelineFragment;
|
||||
private NotificationsFragment notificationsFragment;
|
||||
private NotificationsListFragment notificationsFragment;
|
||||
private DiscoverFragment searchFragment;
|
||||
private ProfileFragment profileFragment;
|
||||
private TabBar tabBar;
|
||||
@@ -52,6 +65,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
private ImageView tabBarAvatar;
|
||||
@IdRes
|
||||
private int currentTab=R.id.tab_home;
|
||||
private TextView notificationsBadge;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@@ -73,7 +87,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
searchFragment.setArguments(args);
|
||||
notificationsFragment=new NotificationsFragment();
|
||||
notificationsFragment=new NotificationsListFragment();
|
||||
notificationsFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self));
|
||||
@@ -82,6 +96,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
profileFragment.setArguments(args);
|
||||
}
|
||||
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -100,15 +121,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
tabBarWrap=content.findViewById(R.id.tabbar_wrap);
|
||||
|
||||
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
|
||||
tabBarAvatar.setOutlineProvider(new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setOval(0, 0, view.getWidth(), view.getHeight());
|
||||
}
|
||||
});
|
||||
tabBarAvatar.setOutlineProvider(OutlineProviders.OVAL);
|
||||
tabBarAvatar.setClipToOutline(true);
|
||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
|
||||
ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
|
||||
|
||||
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
|
||||
if(savedInstanceState==null){
|
||||
getChildFragmentManager().beginTransaction()
|
||||
@@ -142,7 +161,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
return;
|
||||
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
|
||||
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
notificationsFragment=(NotificationsListFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||
currentTab=savedInstanceState.getInt("selectedTab");
|
||||
tabBar.selectTab(currentTab);
|
||||
@@ -165,7 +184,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme();
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,7 +196,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(24)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
@@ -202,6 +221,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
public void setCurrentTab(@IdRes int tab){
|
||||
if(tab==currentTab)
|
||||
return;
|
||||
tabBar.selectTab(tab);
|
||||
onTabSelected(tab);
|
||||
}
|
||||
|
||||
private void onTabSelected(@IdRes int tab){
|
||||
Fragment newFragment=fragmentForTab(tab);
|
||||
if(tab==currentTab){
|
||||
@@ -221,9 +247,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
lf.loadData();
|
||||
}else if(newFragment instanceof DiscoverFragment){
|
||||
((DiscoverFragment) newFragment).loadData();
|
||||
}else if(newFragment instanceof NotificationsFragment){
|
||||
((NotificationsFragment) newFragment).loadData();
|
||||
// TODO make an interface?
|
||||
}
|
||||
if(newFragment instanceof NotificationsListFragment){
|
||||
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
|
||||
nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID);
|
||||
}
|
||||
@@ -235,10 +260,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new AccountSwitcherSheet(getActivity()).show();
|
||||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
}
|
||||
if(tab==R.id.tab_home){
|
||||
if(tab==R.id.tab_home && BuildConfig.DEBUG){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
@@ -264,4 +289,72 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
reloadNotificationsForUnreadCount();
|
||||
}
|
||||
|
||||
private void reloadNotificationsForUnreadCount(){
|
||||
List<Notification>[] notifications=new List[]{null};
|
||||
String[] marker={null};
|
||||
|
||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
marker[0]=m;
|
||||
if(notifications[0]!=null){
|
||||
updateUnreadCount(notifications[0], marker[0]);
|
||||
}
|
||||
});
|
||||
|
||||
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, true, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
notifications[0]=result.items;
|
||||
if(marker[0]!=null)
|
||||
updateUnreadCount(notifications[0], marker[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void updateUnreadCount(List<Notification> notifications, String marker){
|
||||
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
}else{
|
||||
notificationsBadge.setVisibility(View.VISIBLE);
|
||||
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
|
||||
notificationsBadge.setText(String.format("%d+", notifications.size()));
|
||||
}else{
|
||||
int count=0;
|
||||
for(Notification n:notifications){
|
||||
if(n.id.equals(marker))
|
||||
break;
|
||||
count++;
|
||||
}
|
||||
notificationsBadge.setText(String.format("%d", count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(ev.clearUnread)
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(homeTimelineFragment.loaded)
|
||||
homeTimelineFragment.rebuildAllDisplayItems();
|
||||
if(notificationsFragment.loaded)
|
||||
notificationsFragment.rebuildAllDisplayItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,20 +30,20 @@ import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -122,7 +122,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
new SaveMarkers(topPostID, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SaveMarkers.Response result){
|
||||
public void onSuccess(TimelineMarkers result){
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,8 +167,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
public void onStatusCreated(Status status){
|
||||
prependItems(Collections.singletonList(status), true);
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
@@ -199,10 +199,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!filters.isEmpty()){
|
||||
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
@@ -276,19 +273,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
outer:
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(s)){
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
targetList.addAll(buildDisplayItems(s));
|
||||
insertedPosts.add(s);
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, FilterContext.HOME);
|
||||
if(targetList.isEmpty()){
|
||||
// oops. We didn't add new posts, but at least we know there are none.
|
||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||
@@ -440,7 +431,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
|
||||
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T>{
|
||||
protected ElevationOnScrollListener elevationOnScrollListener;
|
||||
|
||||
public MastodonRecyclerFragment(int perPage){
|
||||
super(perPage);
|
||||
}
|
||||
|
||||
public MastodonRecyclerFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
Toolbar toolbar=getToolbar();
|
||||
return toolbar!=null ? Collections.singletonList(toolbar) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
if(refreshLayout!=null){
|
||||
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
|
||||
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
|
||||
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
|
||||
refreshLayout.setColorSchemeColors(colorPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
if(elevationOnScrollListener!=null){
|
||||
elevationOnScrollListener.setViews(getViewsForElevationEffect());
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,15 @@ import androidx.annotation.CallSuper;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
|
||||
public abstract class MastodonToolbarFragment extends ToolbarFragment{
|
||||
|
||||
public MastodonToolbarFragment(){
|
||||
super();
|
||||
}
|
||||
|
||||
protected MastodonToolbarFragment(int layout){
|
||||
super(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false);
|
||||
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[2];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.notifications_all;
|
||||
case 1 -> R.id.notifications_mentions;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
tabLayout.setTabTextSize(V.dp(16));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setAdapter(new DiscoverPagerAdapter());
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
public void onPageSelected(int position){
|
||||
if(position==0)
|
||||
return;
|
||||
Fragment _page=getFragmentForPage(position);
|
||||
if(_page instanceof BaseRecyclerFragment<?> page){
|
||||
if(!page.loaded && !page.isDataLoading())
|
||||
page.loadData();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(allNotificationsFragment==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
|
||||
allNotificationsFragment=new NotificationsListFragment();
|
||||
allNotificationsFragment.setArguments(args);
|
||||
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("onlyMentions", true);
|
||||
mentionsFragment=new NotificationsListFragment();
|
||||
mentionsFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.notifications_all, allNotificationsFragment)
|
||||
.add(R.id.notifications_mentions, mentionsFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.all_notifications;
|
||||
case 1 -> R.string.mentions;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
}
|
||||
});
|
||||
tabLayoutMediator.attach();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
getFragmentForPage(pager.getCurrentItem()).scrollToTop();
|
||||
}
|
||||
|
||||
public void loadData(){
|
||||
if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading)
|
||||
allNotificationsFragment.loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateToolbar(){
|
||||
super.updateToolbar();
|
||||
getToolbar().setOutlineProvider(null);
|
||||
getToolbar().setOnClickListener(v->scrollToTop());
|
||||
}
|
||||
|
||||
private NotificationsListFragment getFragmentForPage(int page){
|
||||
return switch(page){
|
||||
case 0 -> allNotificationsFragment;
|
||||
case 1 -> mentionsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
FrameLayout view=tabViews[viewType];
|
||||
((ViewGroup)view.getParent()).removeView(view);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
return new SimpleViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -15,33 +23,45 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
private boolean onlyMentions;
|
||||
private boolean onlyMentions=true;
|
||||
private String maxID;
|
||||
private View tabBar;
|
||||
private View mentionsTab, allTab;
|
||||
private View endMark;
|
||||
private String unreadMarker, realUnreadMarker;
|
||||
private MenuItem markAllReadItem;
|
||||
private boolean reloadingFromCache;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setLayout(R.layout.fragment_notifications);
|
||||
E.register(this);
|
||||
if(savedInstanceState!=null){
|
||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", true);
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,28 +73,29 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
String extraText=switch(n.type){
|
||||
case FOLLOW -> getString(R.string.user_followed_you);
|
||||
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
|
||||
case MENTION, STATUS -> null;
|
||||
case REBLOG -> getString(R.string.notification_boosted);
|
||||
case FAVORITE -> getString(R.string.user_favorited);
|
||||
case POLL -> getString(R.string.poll_ended);
|
||||
};
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
|
||||
NotificationHeaderStatusDisplayItem titleItem;
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
titleItem=null;
|
||||
}else{
|
||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||
if(n.status!=null){
|
||||
n.status.card=null;
|
||||
n.status.spoilerText=null;
|
||||
}
|
||||
}
|
||||
if(n.status!=null){
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else if(titleItem!=null){
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
|
||||
return Arrays.asList(titleItem, card);
|
||||
return Collections.singletonList(titleItem);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -90,46 +111,41 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(!refreshing && !reloadingFromCache)
|
||||
endMark.setVisibility(View.GONE);
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(refreshing)
|
||||
relationships.clear();
|
||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||
Set<String> needRelationships=result.items.stream()
|
||||
.filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id))
|
||||
.map(ntf->ntf.account.id)
|
||||
.collect(Collectors.toSet());
|
||||
loadRelationships(needRelationships);
|
||||
maxID=result.maxID;
|
||||
|
||||
if(offset==0 && !result.items.isEmpty()){
|
||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||
}
|
||||
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
reloadingFromCache=false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRelationshipsLoaded(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
|
||||
accountHolder.rebind();
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
if(!dataLoading){
|
||||
if(onlyMentions){
|
||||
refresh();
|
||||
}else{
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
// loadData();
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
resetUnreadBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,7 +155,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
Status status=n.status;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
@@ -153,8 +169,62 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
tabBar=view.findViewById(R.id.tabbar);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
|
||||
View tabBarItself=view.findViewById(R.id.tabbar_inner);
|
||||
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
tabBarItself.setClipToOutline(true);
|
||||
|
||||
mentionsTab=view.findViewById(R.id.mentions_tab);
|
||||
allTab=view.findViewById(R.id.all_tab);
|
||||
mentionsTab.setOnClickListener(this::onTabClick);
|
||||
allTab.setOnClickListener(this::onTabClick);
|
||||
mentionsTab.setSelected(onlyMentions);
|
||||
allTab.setSelected(!onlyMentions);
|
||||
|
||||
NestedRecyclerScrollView scroller=view.findViewById(R.id.scroller);
|
||||
scroller.setScrollableChildSupplier(()->list);
|
||||
scroller.setTakePriorityOverChildViews(true);
|
||||
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint();
|
||||
private Rect tmpRect=new Rect();
|
||||
|
||||
{
|
||||
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3SurfaceVariant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(TextUtils.isEmpty(unreadMarker))
|
||||
return;
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
||||
String itemID=holder.getItemID();
|
||||
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
c.drawRect(tmpRect, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
ArrayList<View> views=new ArrayList<>(super.getViewsForElevationEffect());
|
||||
views.add(tabBar);
|
||||
return views;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("onlyMentions", onlyMentions);
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
@@ -211,4 +281,87 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
private void onTabClick(View v){
|
||||
boolean newOnlyMentions=v.getId()==R.id.mentions_tab;
|
||||
if(newOnlyMentions==onlyMentions)
|
||||
return;
|
||||
onlyMentions=newOnlyMentions;
|
||||
mentionsTab.setSelected(onlyMentions);
|
||||
allTab.setSelected(!onlyMentions);
|
||||
maxID=null;
|
||||
showProgress();
|
||||
loadData(0, 20);
|
||||
refreshing=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateFooterView(LayoutInflater inflater){
|
||||
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||
endMark=v.findViewById(R.id.end_mark);
|
||||
endMark.setVisibility(View.GONE);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.mark_all_read){
|
||||
markAsRead();
|
||||
resetUnreadBackground();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void markAsRead(){
|
||||
String id=data.get(0).id;
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetUnreadBackground(){
|
||||
unreadMarker=realUnreadMarker;
|
||||
list.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
super.onRefresh();
|
||||
resetUnreadBackground();
|
||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
unreadMarker=realUnreadMarker=m;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMarkAllReadButton(){
|
||||
markAllReadItem.setEnabled(!data.isEmpty() && !realUnreadMarker.equals(data.get(0).id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendItems(List<Notification> items){
|
||||
super.onAppendItems(items);
|
||||
if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker))
|
||||
return;
|
||||
for(Notification n:items){
|
||||
if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0){
|
||||
markAsRead();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class PinnedPostsListFragment extends StatusListFragment{
|
||||
private Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
setTitle(R.string.posts);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -48,10 +49,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
public UsableRecyclerView list;
|
||||
private List<AccountField> fields=Collections.emptyList();
|
||||
private AboutAdapter adapter;
|
||||
private Paint dividerPaint=new Paint();
|
||||
private boolean isInEditMode;
|
||||
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
|
||||
private RecyclerView.ViewHolder draggedViewHolder;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
|
||||
public void setFields(List<AccountField> fields){
|
||||
@@ -74,27 +73,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
|
||||
list.setAdapter(adapter=new AboutAdapter());
|
||||
int pad=V.dp(16);
|
||||
list.setPadding(pad, pad, pad, pad);
|
||||
list.setPadding(0, V.dp(16), 0, 0);
|
||||
list.setClipToPadding(false);
|
||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||
dividerPaint.setStrokeWidth(V.dp(1));
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View item=parent.getChildAt(i);
|
||||
int pos=parent.getChildAdapterPosition(item);
|
||||
int draggedPos=draggedViewHolder==null ? -1 : draggedViewHolder.getAbsoluteAdapterPosition();
|
||||
if(pos<adapter.getItemCount()-1 && pos!=draggedPos && pos!=draggedPos-1){
|
||||
float y=item.getY()+item.getHeight();
|
||||
dividerPaint.setAlpha(Math.round(255*item.getAlpha()));
|
||||
c.drawLine(item.getLeft(), y, item.getRight(), y, dividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -183,36 +163,25 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
|
||||
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
|
||||
protected ShapeDrawable background=new ShapeDrawable();
|
||||
|
||||
public BaseViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
itemView.setBackground(background);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==adapter.getItemCount()-1;
|
||||
float radius=V.dp(10);
|
||||
float[] rad=new float[8];
|
||||
if(first)
|
||||
rad[0]=rad[1]=rad[2]=rad[3]=radius;
|
||||
if(last)
|
||||
rad[4]=rad[5]=rad[6]=rad[7]=radius;
|
||||
background.setShape(new RoundRectShape(rad, null, null));
|
||||
itemView.invalidateOutline();
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
|
||||
private TextView title;
|
||||
private LinkedTextView value;
|
||||
private final TextView title;
|
||||
private final LinkedTextView value;
|
||||
private final ImageView verifiedIcon;
|
||||
|
||||
public AboutViewHolder(){
|
||||
super(R.layout.item_profile_about);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
verifiedIcon=findViewById(R.id.verified_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,20 +189,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
super.onBind(item);
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
if(item.verifiedAt!=null){
|
||||
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
|
||||
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||
value.setTextColor(textColor);
|
||||
value.setLinkTextColor(textColor);
|
||||
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
|
||||
check.setTint(textColor);
|
||||
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
|
||||
}else{
|
||||
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
|
||||
value.setCompoundDrawables(null, null, null, null);
|
||||
}
|
||||
verifiedIcon.setVisibility(item.verifiedAt!=null ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -251,20 +207,20 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
|
||||
private class EditableAboutViewHolder extends BaseViewHolder{
|
||||
private EditText title;
|
||||
private EditText value;
|
||||
private final EditText title;
|
||||
private final EditText value;
|
||||
|
||||
public EditableAboutViewHolder(){
|
||||
super(R.layout.item_profile_about_editable);
|
||||
super(R.layout.onboarding_profile_field);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
value=findViewById(R.id.content);
|
||||
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
||||
dragHelper.startDrag(this);
|
||||
return true;
|
||||
});
|
||||
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
|
||||
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
|
||||
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick);
|
||||
findViewById(R.id.delete).setOnClickListener(this::onRemoveRowClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -323,8 +279,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
}
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
((BindableViewHolder)viewHolder).rebind();
|
||||
((BindableViewHolder)target).rebind();
|
||||
((BindableViewHolder<?>)viewHolder).rebind();
|
||||
((BindableViewHolder<?>)target).rebind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -339,7 +295,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
|
||||
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
|
||||
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
draggedViewHolder=viewHolder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +302,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
draggedViewHolder=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFeaturedHashtags;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SectionHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult>{
|
||||
private Account profileAccount;
|
||||
private List<Hashtag> featuredTags;
|
||||
// private List<Account> endorsedAccounts;
|
||||
private List<Status> pinnedStatuses;
|
||||
private boolean tagsLoaded, statusesLoaded;
|
||||
|
||||
public ProfileFeaturedFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_no_refresh);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
profileAccount=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){
|
||||
ArrayList<StatusDisplayItem> items=switch(s.type){
|
||||
case ACCOUNT -> new ArrayList<>(Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)));
|
||||
case HASHTAG -> new ArrayList<>(Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)));
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
|
||||
};
|
||||
|
||||
if(s.firstInSection){
|
||||
items.add(0, new SectionHeaderStatusDisplayItem(this, getString(switch(s.type){
|
||||
case ACCOUNT -> R.string.profile_endorsed_accounts;
|
||||
case HASHTAG -> R.string.hashtags;
|
||||
case STATUS -> R.string.posts;
|
||||
}), getString(R.string.view_all), switch(s.type){
|
||||
case ACCOUNT -> (Runnable)this::showAllEndorsedAccounts;
|
||||
case HASHTAG -> (Runnable)this::showAllFeaturedHashtags;
|
||||
case STATUS -> (Runnable)this::showAllPinnedPosts;
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(SearchResult s){
|
||||
Account acc=switch(s.type){
|
||||
case ACCOUNT -> s.account;
|
||||
case STATUS -> s.status.account;
|
||||
case HASHTAG -> null;
|
||||
};
|
||||
if(acc!=null && !knownAccounts.containsKey(acc.id))
|
||||
knownAccounts.put(acc.id, acc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
SearchResult res=getResultByID(id);
|
||||
if(res==null)
|
||||
return;
|
||||
switch(res.type){
|
||||
case ACCOUNT -> {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
|
||||
case STATUS -> {
|
||||
Status status=res.status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(!statusesLoaded){
|
||||
new GetAccountStatuses(profileAccount.id, null, null, 2, GetAccountStatuses.Filter.PINNED)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
pinnedStatuses=result;
|
||||
statusesLoaded=true;
|
||||
onOneApiRequestCompleted();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
if(!tagsLoaded){
|
||||
new GetAccountFeaturedHashtags(profileAccount.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Hashtag> result){
|
||||
featuredTags=result;
|
||||
tagsLoaded=true;
|
||||
onOneApiRequestCompleted();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
statusesLoaded=false;
|
||||
tagsLoaded=false;
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
private void onOneApiRequestCompleted(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(tagsLoaded && statusesLoaded){
|
||||
ArrayList<SearchResult> results=new ArrayList<>();
|
||||
for(int i=0;i<Math.min(2, pinnedStatuses.size());i++){
|
||||
SearchResult res=new SearchResult(pinnedStatuses.get(i));
|
||||
res.firstInSection=(i==0);
|
||||
results.add(res);
|
||||
}
|
||||
for(int i=0;i<Math.min(5, featuredTags.size());i++){
|
||||
SearchResult res=new SearchResult(featuredTags.get(i));
|
||||
res.firstInSection=(i==0);
|
||||
results.add(res);
|
||||
}
|
||||
onDataLoaded(results, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected SearchResult getResultByID(String id){
|
||||
for(SearchResult s:data){
|
||||
if(s.id.equals(id)){
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
if(holder instanceof FooterStatusDisplayItem.Holder && siblingHolder instanceof StatusDisplayItem.Holder<?> sdi && sdi.getItemID().startsWith("post_")){
|
||||
super.drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void showAllPinnedPosts(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
|
||||
Nav.go(getActivity(), PinnedPostsListFragment.class, args);
|
||||
}
|
||||
|
||||
private void showAllFeaturedHashtags(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
|
||||
args.putParcelableArrayList("hashtags", tags);
|
||||
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);
|
||||
}
|
||||
|
||||
private void showAllEndorsedAccounts(){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,21 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.view.Gravity;
|
||||
import android.transition.ChangeBounds;
|
||||
import android.transition.Fade;
|
||||
import android.transition.TransitionManager;
|
||||
import android.transition.TransitionSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -29,12 +34,11 @@ import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
@@ -43,7 +47,6 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
@@ -55,9 +58,10 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -65,8 +69,10 @@ import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
import org.joinmastodon.android.ui.views.CustomDrawingOrderLinearLayout;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -92,6 +98,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
|
||||
private static final int AVATAR_RESULT=722;
|
||||
@@ -100,27 +107,30 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ImageView avatar;
|
||||
private CoverImageView cover;
|
||||
private View avatarBorder;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
|
||||
private ProgressBarButton actionButton;
|
||||
private ViewPager2 pager;
|
||||
private NestedRecyclerScrollView scrollView;
|
||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
|
||||
private ProfileFeaturedFragment featuredFragment;
|
||||
private AccountTimelineFragment timelineFragment;
|
||||
private ProfileAboutFragment aboutFragment;
|
||||
private TabLayout tabbar;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
private CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable();
|
||||
private float titleTransY;
|
||||
private View postsBtn, followersBtn, followingBtn;
|
||||
private View followersBtn, followingBtn;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ProgressBar actionProgress;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private TextView followsYouView;
|
||||
private LinearLayout countersLayout;
|
||||
private View nameEditWrap, bioEditWrap;
|
||||
private View tabsDivider;
|
||||
private View actionButtonWrap;
|
||||
private CustomDrawingOrderLinearLayout scrollableContent;
|
||||
|
||||
private Account account;
|
||||
private String accountID;
|
||||
private Relationship relationship;
|
||||
private int statusBarHeight;
|
||||
private boolean isOwnProfile;
|
||||
private ArrayList<AccountField> fields=new ArrayList<>();
|
||||
|
||||
@@ -132,10 +142,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private WindowInsets childInsets;
|
||||
private PhotoViewer currentPhotoViewer;
|
||||
private boolean editModeLoading;
|
||||
|
||||
public ProfileFragment(){
|
||||
super(R.layout.loader_fragment_overlay_toolbar);
|
||||
}
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private Drawable tabsColorBackground;
|
||||
private boolean tabBarIsAtTop;
|
||||
private Animator tabBarColorAnim;
|
||||
private MenuItem editSaveMenuItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -180,9 +191,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followingCount=content.findViewById(R.id.following_count);
|
||||
followingLabel=content.findViewById(R.id.following_label);
|
||||
followingBtn=content.findViewById(R.id.following_btn);
|
||||
postsCount=content.findViewById(R.id.posts_count);
|
||||
postsLabel=content.findViewById(R.id.posts_label);
|
||||
postsBtn=content.findViewById(R.id.posts_btn);
|
||||
actionButton=content.findViewById(R.id.profile_action_btn);
|
||||
pager=content.findViewById(R.id.pager);
|
||||
scrollView=content.findViewById(R.id.scroller);
|
||||
@@ -190,36 +198,34 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
refreshLayout=content.findViewById(R.id.refresh_layout);
|
||||
nameEdit=content.findViewById(R.id.name_edit);
|
||||
bioEdit=content.findViewById(R.id.bio_edit);
|
||||
nameEditWrap=content.findViewById(R.id.name_edit_wrap);
|
||||
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
|
||||
actionProgress=content.findViewById(R.id.action_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
followsYouView=content.findViewById(R.id.follows_you);
|
||||
countersLayout=content.findViewById(R.id.profile_counters);
|
||||
tabsDivider=content.findViewById(R.id.tabs_divider);
|
||||
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
|
||||
scrollableContent=content.findViewById(R.id.scrollable_content);
|
||||
|
||||
avatar.setOutlineProvider(new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(25));
|
||||
}
|
||||
});
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
Toolbar toolbar=getToolbar();
|
||||
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-toolbar.getLayoutParams().height-statusBarHeight-V.dp(38);
|
||||
coverGradient.setTopPadding(statusBarHeight+toolbar.getLayoutParams().height);
|
||||
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[4];
|
||||
tabViews=new FrameLayout[3];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.profile_posts;
|
||||
case 1 -> R.id.profile_posts_with_replies;
|
||||
case 2 -> R.id.profile_media;
|
||||
case 3 -> R.id.profile_about;
|
||||
case 0 -> R.id.profile_featured;
|
||||
case 1 -> R.id.profile_timeline;
|
||||
case 2 -> R.id.profile_about;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -235,22 +241,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
sizeWrapper.addView(content);
|
||||
|
||||
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
tabbar.setTabTextSize(V.dp(16));
|
||||
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabbar.setTabTextSize(V.dp(14));
|
||||
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.posts;
|
||||
case 1 -> R.string.posts_and_replies;
|
||||
case 2 -> R.string.media;
|
||||
case 3 -> R.string.profile_about;
|
||||
case 0 -> R.string.profile_featured;
|
||||
case 1 -> R.string.profile_timeline;
|
||||
case 2 -> R.string.profile_about;
|
||||
default -> throw new IllegalStateException();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cover.setForeground(coverGradient);
|
||||
cover.setOutlineProvider(new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
@@ -287,6 +291,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return true;
|
||||
});
|
||||
|
||||
scrollableContent.setDrawingOrderCallback((count, pos)->{
|
||||
// The header is the first child, draw it last to overlap everything for the photo viewer transition to look nice
|
||||
if(pos==count-1)
|
||||
return 0;
|
||||
// Offset the order of other child views to compensate
|
||||
return pos+1;
|
||||
});
|
||||
|
||||
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
|
||||
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
|
||||
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
|
||||
refreshLayout.setColorSchemeColors(colorPrimary);
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
@@ -309,12 +326,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(refreshing){
|
||||
refreshing=false;
|
||||
refreshLayout.setRefreshing(false);
|
||||
if(postsFragment.loaded)
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
if(timelineFragment.loaded)
|
||||
timelineFragment.onRefresh();
|
||||
if(featuredFragment.loaded)
|
||||
featuredFragment.onRefresh();
|
||||
}
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
@@ -334,14 +349,28 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public void dataLoaded(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(postsFragment==null){
|
||||
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
||||
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
||||
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
||||
if(featuredFragment==null){
|
||||
featuredFragment=new ProfileFeaturedFragment();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
args.putBoolean("__is_tab", true);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
featuredFragment.setArguments(args);
|
||||
timelineFragment=AccountTimelineFragment.newInstance(accountID, account, true);
|
||||
aboutFragment=new ProfileAboutFragment();
|
||||
aboutFragment.setFields(fields);
|
||||
}
|
||||
pager.getAdapter().notifyDataSetChanged();
|
||||
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
pager.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
pager.setCurrentItem(1, false);
|
||||
tabbar.selectTab(tabbar.getTabAt(1));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
super.dataLoaded();
|
||||
}
|
||||
|
||||
@@ -357,10 +386,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
public void onPageSelected(int position){
|
||||
if(position==0)
|
||||
return;
|
||||
Fragment _page=getFragmentForPage(position);
|
||||
if(_page instanceof BaseRecyclerFragment<?> page){
|
||||
if(_page instanceof BaseRecyclerFragment<?> page && page.isAdded()){
|
||||
if(!page.loaded && !page.isDataLoading())
|
||||
page.loadData();
|
||||
}
|
||||
@@ -368,6 +395,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state){
|
||||
if(isInEditMode)
|
||||
return;
|
||||
refreshLayout.setEnabled(state!=ViewPager2.SCROLL_STATE_DRAGGING);
|
||||
}
|
||||
});
|
||||
@@ -375,17 +404,23 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
});
|
||||
|
||||
scrollView.setOnScrollChangeListener(this::onScrollChanged);
|
||||
titleTransY=getToolbar().getLayoutParams().height;
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
toolbarSubtitleView.setTranslationY(titleTransY);
|
||||
}
|
||||
}
|
||||
tabsColorBackground=((LayerDrawable)tabbar.getBackground()).findDrawableByLayerId(R.id.color_overlay);
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar());
|
||||
scrollView.setOnScrollChangeListener(this::onScrollChanged);
|
||||
scrollView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
scrollView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
tabBarIsAtTop=!scrollView.canScrollVertically(1) && scrollView.getHeight()>0;
|
||||
tabsColorBackground.setAlpha(tabBarIsAtTop ? 20 : 0);
|
||||
tabbar.setTranslationZ(tabBarIsAtTop ? V.dp(3) : 0);
|
||||
tabsDivider.setAlpha(tabBarIsAtTop ? 0 : 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -396,28 +431,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
statusBarHeight=insets.getSystemWindowInsetTop();
|
||||
if(contentView!=null){
|
||||
((ViewGroup.MarginLayoutParams) getToolbar().getLayoutParams()).topMargin=statusBarHeight;
|
||||
refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24));
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+insetBottom;
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+insetBottom;
|
||||
applyChildWindowInsets();
|
||||
insets=insets.inset(0, 0, 0, insetBottom);
|
||||
}else{
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24);
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16);
|
||||
}
|
||||
}
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
private void applyChildWindowInsets(){
|
||||
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
|
||||
postsFragment.onApplyWindowInsets(childInsets);
|
||||
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
|
||||
mediaFragment.onApplyWindowInsets(childInsets);
|
||||
if(timelineFragment!=null && timelineFragment.isAdded() && childInsets!=null){
|
||||
timelineFragment.onApplyWindowInsets(childInsets);
|
||||
featuredFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +458,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
@@ -441,7 +473,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_fluent_lock_closed_20_filled, getActivity().getTheme()).mutate();
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
lock.setTint(username.getCurrentTextColor());
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
@@ -459,16 +491,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
|
||||
|
||||
UiUtils.loadCustomEmojiInTextView(name);
|
||||
UiUtils.loadCustomEmojiInTextView(bio);
|
||||
|
||||
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
TypedArray ta=actionButton.getContext().obtainStyledAttributes(R.style.Widget_Mastodon_M3_Button_Tonal, new int[]{android.R.attr.background});
|
||||
actionButton.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=actionButton.getContext().obtainStyledAttributes(R.style.Widget_Mastodon_M3_Button_Tonal, new int[]{android.R.attr.textColor});
|
||||
actionButton.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}else{
|
||||
actionButton.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -503,47 +539,34 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
private void updateToolbar(){
|
||||
getToolbar().setBackgroundColor(0);
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
toolbarSubtitleView.setTranslationY(titleTransY);
|
||||
}
|
||||
getToolbar().setOnClickListener(v->scrollToTop());
|
||||
getToolbar().setNavigationContentDescription(R.string.back);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return false;
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
if(isOwnProfile && isInEditMode){
|
||||
Button cancelButton=new Button(getActivity(), null, 0, R.style.Widget_Mastodon_Button_Secondary_LightOnDark);
|
||||
cancelButton.setText(R.string.cancel);
|
||||
cancelButton.setOnClickListener(v->exitEditMode());
|
||||
FrameLayout wrap=new FrameLayout(getActivity());
|
||||
wrap.addView(cancelButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
|
||||
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
wrap.setClipToPadding(false);
|
||||
MenuItem item=menu.add(R.string.cancel);
|
||||
item.setActionView(wrap);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
editSaveMenuItem=menu.add(0, R.id.save, 0, R.string.save_changes);
|
||||
editSaveMenuItem.setIcon(R.drawable.ic_save_24px);
|
||||
editSaveMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
editSaveMenuItem.setVisible(!isActionButtonInView());
|
||||
return;
|
||||
}
|
||||
if(relationship==null && !isOwnProfile)
|
||||
return;
|
||||
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.share).setTitle(R.string.share_user);
|
||||
if(isOwnProfile)
|
||||
return;
|
||||
|
||||
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.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.displayName));
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.displayName));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.displayName));
|
||||
if(relationship.following)
|
||||
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.displayName));
|
||||
else
|
||||
menu.findItem(R.id.hide_boosts).setVisible(false);
|
||||
if(!account.isLocal())
|
||||
@@ -568,6 +591,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("reportAccount", Parcels.wrap(account));
|
||||
args.putParcelable("relationship", Parcels.wrap(relationship));
|
||||
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(getActivity(), account.url);
|
||||
@@ -599,15 +623,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
saveAndExitEditMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getToolbarResource(){
|
||||
return R.layout.profile_toolbar;
|
||||
}
|
||||
|
||||
private void loadRelationship(){
|
||||
new GetAccountRelationships(Collections.singletonList(account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@@ -630,49 +652,71 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private void updateRelationship(){
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||
if(scrollY>avatarBorder.getTop()-topBarsH){
|
||||
float avaAlpha=Math.max(1f-((scrollY-(avatarBorder.getTop()-topBarsH))/(float)V.dp(38)), 0f);
|
||||
avatarBorder.setAlpha(avaAlpha);
|
||||
}else{
|
||||
avatarBorder.setAlpha(1f);
|
||||
}
|
||||
if(scrollY>cover.getHeight()-topBarsH){
|
||||
cover.setTranslationY(scrollY-(cover.getHeight()-topBarsH));
|
||||
if(scrollY>cover.getHeight()){
|
||||
cover.setTranslationY(scrollY-(cover.getHeight()));
|
||||
cover.setTranslationZ(V.dp(10));
|
||||
cover.setTransform(cover.getHeight()/2f-topBarsH/2f, 1f);
|
||||
cover.setTransform(cover.getHeight()/2f);
|
||||
}else{
|
||||
cover.setTranslationY(0f);
|
||||
cover.setTranslationZ(0f);
|
||||
cover.setTransform(scrollY/2f, 1f);
|
||||
cover.setTransform(scrollY/2f);
|
||||
}
|
||||
coverGradient.setTopOffset(scrollY);
|
||||
cover.invalidate();
|
||||
titleTransY=getToolbar().getHeight();
|
||||
if(scrollY>name.getTop()-topBarsH){
|
||||
titleTransY=Math.max(0f, titleTransY-(scrollY-(name.getTop()-topBarsH)));
|
||||
}
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
toolbarSubtitleView.setTranslationY(titleTransY);
|
||||
}
|
||||
if(currentPhotoViewer!=null){
|
||||
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
|
||||
}
|
||||
onScrollListener.onScrollChange(v, scrollX, scrollY, oldScrollX, oldScrollY);
|
||||
|
||||
boolean newTabBarIsAtTop=!scrollView.canScrollVertically(1);
|
||||
if(newTabBarIsAtTop!=tabBarIsAtTop){
|
||||
tabBarIsAtTop=newTabBarIsAtTop;
|
||||
|
||||
if(tabBarIsAtTop){
|
||||
// ScrollView would sometimes leave 1 pixel unscrolled, force it into the correct scrollY
|
||||
int maxY=scrollView.getChildAt(0).getHeight()-scrollView.getHeight();
|
||||
if(scrollView.getScrollY()!=maxY)
|
||||
scrollView.scrollTo(0, maxY);
|
||||
}
|
||||
|
||||
if(tabBarColorAnim!=null)
|
||||
tabBarColorAnim.cancel();
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(tabsColorBackground, "alpha", tabBarIsAtTop ? 20 : 0),
|
||||
ObjectAnimator.ofFloat(tabbar, View.TRANSLATION_Z, tabBarIsAtTop ? V.dp(3) : 0),
|
||||
ObjectAnimator.ofFloat(getToolbar(), View.TRANSLATION_Z, tabBarIsAtTop ? 0 : V.dp(3)),
|
||||
ObjectAnimator.ofFloat(tabsDivider, View.ALPHA, tabBarIsAtTop ? 0 : 1)
|
||||
);
|
||||
set.setDuration(150);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
tabBarColorAnim=null;
|
||||
}
|
||||
});
|
||||
tabBarColorAnim=set;
|
||||
set.start();
|
||||
}
|
||||
if(isInEditMode && editSaveMenuItem!=null){
|
||||
boolean buttonInView=isActionButtonInView();
|
||||
if(buttonInView==editSaveMenuItem.isVisible()){
|
||||
editSaveMenuItem.setVisible(!buttonInView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
return switch(page){
|
||||
case 0 -> postsFragment;
|
||||
case 1 -> postsWithRepliesFragment;
|
||||
case 2 -> mediaFragment;
|
||||
case 3 -> aboutFragment;
|
||||
case 0 -> featuredFragment;
|
||||
case 1 -> timelineFragment;
|
||||
case 2 -> aboutFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
@@ -695,6 +739,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
if(visible)
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
|
||||
@@ -732,40 +778,41 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
isInEditMode=true;
|
||||
invalidateOptionsMenu();
|
||||
pager.setUserInputEnabled(false);
|
||||
actionButton.setText(R.string.done);
|
||||
pager.setCurrentItem(3);
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
actionButton.setText(R.string.save_changes);
|
||||
pager.setCurrentItem(2);
|
||||
for(int i=0;i<3;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
|
||||
tabbar.getTabAt(i).view.setEnabled(false);
|
||||
}
|
||||
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
|
||||
avatar.setForeground(overlay);
|
||||
animators.add(ObjectAnimator.ofInt(overlay, "alpha", 0, 255));
|
||||
|
||||
nameEdit.setVisibility(View.VISIBLE);
|
||||
Toolbar toolbar=getToolbar();
|
||||
Drawable close=getToolbarContext().getDrawable(R.drawable.ic_baseline_close_24).mutate();
|
||||
close.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
toolbar.setNavigationIcon(close);
|
||||
toolbar.setNavigationContentDescription(R.string.discard);
|
||||
|
||||
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
|
||||
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
|
||||
.addTransition(new Fade(Fade.IN | Fade.OUT))
|
||||
.addTransition(new ChangeBounds())
|
||||
.setDuration(250)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
);
|
||||
|
||||
name.setVisibility(View.GONE);
|
||||
username.setVisibility(View.GONE);
|
||||
bio.setVisibility(View.GONE);
|
||||
countersLayout.setVisibility(View.GONE);
|
||||
|
||||
nameEditWrap.setVisibility(View.VISIBLE);
|
||||
nameEdit.setText(account.displayName);
|
||||
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
|
||||
lp.addRule(RelativeLayout.BELOW, R.id.name_edit);
|
||||
username.getParent().requestLayout();
|
||||
animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f, 1f));
|
||||
|
||||
bioEdit.setVisibility(View.VISIBLE);
|
||||
bioEditWrap.setVisibility(View.VISIBLE);
|
||||
bioEdit.setText(account.source.note);
|
||||
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 0f));
|
||||
|
||||
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, .3f));
|
||||
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, .3f));
|
||||
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, .3f));
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(animators);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.start();
|
||||
|
||||
aboutFragment.enterEditMode(account.source.fields);
|
||||
refreshLayout.setEnabled(false);
|
||||
}
|
||||
|
||||
private void exitEditMode(){
|
||||
@@ -774,39 +821,38 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
isInEditMode=false;
|
||||
|
||||
invalidateOptionsMenu();
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
for(int i=0;i<3;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
|
||||
tabbar.getTabAt(i).view.setEnabled(true);
|
||||
}
|
||||
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
|
||||
animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f));
|
||||
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f));
|
||||
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, 1f));
|
||||
pager.setUserInputEnabled(true);
|
||||
avatar.setForeground(null);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(animators);
|
||||
set.setDuration(200);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
for(int i=0;i<3;i++){
|
||||
tabbar.getTabAt(i).view.setEnabled(true);
|
||||
}
|
||||
pager.setUserInputEnabled(true);
|
||||
nameEdit.setVisibility(View.GONE);
|
||||
bioEdit.setVisibility(View.GONE);
|
||||
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
|
||||
lp.addRule(RelativeLayout.BELOW, R.id.name);
|
||||
username.getParent().requestLayout();
|
||||
avatar.setForeground(null);
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(canGoBack()){
|
||||
Drawable back=getToolbarContext().getDrawable(R.drawable.ic_arrow_back).mutate();
|
||||
back.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
toolbar.setNavigationIcon(back);
|
||||
toolbar.setNavigationContentDescription(0);
|
||||
}else{
|
||||
toolbar.setNavigationIcon(null);
|
||||
}
|
||||
editSaveMenuItem=null;
|
||||
|
||||
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
|
||||
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
|
||||
.addTransition(new Fade(Fade.IN | Fade.OUT))
|
||||
.addTransition(new ChangeBounds())
|
||||
.setDuration(250)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
);
|
||||
nameEditWrap.setVisibility(View.GONE);
|
||||
bioEditWrap.setVisibility(View.GONE);
|
||||
name.setVisibility(View.VISIBLE);
|
||||
username.setVisibility(View.VISIBLE);
|
||||
bio.setVisibility(View.VISIBLE);
|
||||
countersLayout.setVisibility(View.VISIBLE);
|
||||
refreshLayout.setEnabled(true);
|
||||
|
||||
bindHeaderView();
|
||||
}
|
||||
@@ -850,7 +896,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isInEditMode){
|
||||
exitEditMode();
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -901,9 +951,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
private void startImagePicker(int requestCode){
|
||||
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("image/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
Intent intent=UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
@@ -912,10 +960,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(resultCode==Activity.RESULT_OK){
|
||||
if(requestCode==AVATAR_RESULT){
|
||||
editNewAvatar=data.getData();
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
|
||||
}else if(requestCode==COVER_RESULT){
|
||||
editNewCover=data.getData();
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
|
||||
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -940,6 +988,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
Nav.go(getActivity(), cls, args);
|
||||
}
|
||||
|
||||
private boolean isActionButtonInView(){
|
||||
return actionButton.getVisibility()==View.VISIBLE && actionButtonWrap.getTop()+actionButtonWrap.getHeight()>scrollView.getScrollY();
|
||||
}
|
||||
|
||||
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -959,6 +1011,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
if(fragment.isAdded()){
|
||||
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
applyChildWindowInsets();
|
||||
@@ -971,7 +1024,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return loaded ? 4 : 0;
|
||||
return loaded ? 3 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,761 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private UsableRecyclerView list;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private ThemeItem themeItem;
|
||||
private NotificationPolicyItem notificationPolicyItem;
|
||||
private String accountID;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.settings);
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||
items.add(new UpdateItem());
|
||||
}
|
||||
}
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
GlobalUserPreferences.playGifs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.settings_custom_tabs, R.drawable.ic_fluent_link_24_regular, GlobalUserPreferences.useCustomTabs, i->{
|
||||
GlobalUserPreferences.useCustomTabs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
PushSubscription pushSubscription=getPushSubscription();
|
||||
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_boring));
|
||||
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
|
||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
|
||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
if(BuildConfig.DEBUG){
|
||||
items.add(new RedHeaderItem("Debug options"));
|
||||
items.add(new TextItem("Test e-mail confirmation flow", ()->{
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
if(themeTransitionWindowView!=null){
|
||||
// Activity has finished recreating. Remove the overlay.
|
||||
MastodonApp.context.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
|
||||
themeTransitionWindowView=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
list=new UsableRecyclerView(getActivity());
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(new SettingsAdapter());
|
||||
list.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12));
|
||||
list.setClipToPadding(false);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
// Add 32dp gaps between sections
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
|
||||
outRect.top=V.dp(32);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
||||
GlobalUserPreferences.theme=theme;
|
||||
GlobalUserPreferences.save();
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
|
||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||
GlobalUserPreferences.save();
|
||||
|
||||
RecyclerView.ViewHolder themeHolder=list.findViewHolderForAdapterPosition(items.indexOf(themeItem));
|
||||
if(themeHolder!=null){
|
||||
((ThemeViewHolder)themeHolder).bindSubitems();
|
||||
}else{
|
||||
list.getAdapter().notifyItemChanged(items.indexOf(themeItem));
|
||||
}
|
||||
|
||||
if(UiUtils.isDarkTheme()){
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
private void restartActivityToApplyNewTheme(){
|
||||
// Calling activity.recreate() causes a black screen for like half a second.
|
||||
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
|
||||
// As a bonus, we can fade it out to make it even smoother.
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
View activityDecorView=getActivity().getWindow().getDecorView();
|
||||
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
activityDecorView.draw(new Canvas(bitmap));
|
||||
themeTransitionWindowView=new ImageView(MastodonApp.context);
|
||||
themeTransitionWindowView.setImageBitmap(bitmap);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
|
||||
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
|
||||
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
|
||||
lp.token=getActivity().getWindow().getAttributes().token;
|
||||
lp.windowAnimations=R.style.window_fade_out;
|
||||
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
|
||||
}
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
private PushSubscription getPushSubscription(){
|
||||
if(pushSubscription!=null)
|
||||
return pushSubscription;
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(session.pushSubscription==null){
|
||||
pushSubscription=new PushSubscription();
|
||||
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
|
||||
}else{
|
||||
pushSubscription=session.pushSubscription.clone();
|
||||
}
|
||||
return pushSubscription;
|
||||
}
|
||||
|
||||
private void onNotificationsChanged(PushNotification.Type type, boolean enabled){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
switch(type){
|
||||
case FAVORITE -> subscription.alerts.favourite=enabled;
|
||||
case FOLLOW -> subscription.alerts.follow=enabled;
|
||||
case REBLOG -> subscription.alerts.reblog=enabled;
|
||||
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
|
||||
}
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
PushSubscription.Policy prevPolicy=subscription.policy;
|
||||
if(prevPolicy==policy)
|
||||
return;
|
||||
subscription.policy=policy;
|
||||
int index=items.indexOf(notificationPolicyItem);
|
||||
RecyclerView.ViewHolder policyHolder=list.findViewHolderForAdapterPosition(index);
|
||||
if(policyHolder!=null){
|
||||
((NotificationPolicyViewHolder)policyHolder).rebind();
|
||||
}else{
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
}
|
||||
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
|
||||
index++;
|
||||
while(items.get(index) instanceof SwitchItem si){
|
||||
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
|
||||
if(holder!=null)
|
||||
((BindableViewHolder<?>)holder).rebind();
|
||||
else
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void confirmLogOut(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.log_out)
|
||||
.setMessage(R.string.confirm_log_out)
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void logOut(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
onLoggedOut();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut();
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onLoggedOut(){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void clearImageCache(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
if(items.get(0) instanceof UpdateItem item){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
|
||||
if(holder instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
|
||||
private class HeaderItem extends Item{
|
||||
private String text;
|
||||
|
||||
public HeaderItem(@StringRes int text){
|
||||
this.text=getString(text);
|
||||
}
|
||||
|
||||
public HeaderItem(String text){
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchItem extends Item{
|
||||
private String text;
|
||||
private int icon;
|
||||
private boolean checked;
|
||||
private Consumer<SwitchItem> onChanged;
|
||||
private boolean enabled=true;
|
||||
|
||||
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
this.onChanged=onChanged;
|
||||
}
|
||||
|
||||
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
this.onChanged=onChanged;
|
||||
this.enabled=enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThemeItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NotificationPolicyItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public TextItem(String text, Runnable onClick){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
private class RedHeaderItem extends HeaderItem{
|
||||
|
||||
public RedHeaderItem(int text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
public RedHeaderItem(String text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterItem extends Item{
|
||||
private String text;
|
||||
|
||||
public FooterItem(String text){
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
||||
@NonNull
|
||||
@Override
|
||||
public BindableViewHolder<Item> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
//noinspection unchecked
|
||||
return (BindableViewHolder<Item>) switch(viewType){
|
||||
case 0 -> new HeaderViewHolder(false);
|
||||
case 1 -> new SwitchViewHolder();
|
||||
case 2 -> new ThemeViewHolder();
|
||||
case 3 -> new NotificationPolicyViewHolder();
|
||||
case 4 -> new TextViewHolder();
|
||||
case 5 -> new HeaderViewHolder(true);
|
||||
case 6 -> new FooterViewHolder();
|
||||
case 7 -> new UpdateViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BindableViewHolder<Item> holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getViewType();
|
||||
}
|
||||
}
|
||||
|
||||
private class HeaderViewHolder extends BindableViewHolder<HeaderItem>{
|
||||
private final TextView text;
|
||||
public HeaderViewHolder(boolean red){
|
||||
super(getActivity(), R.layout.item_settings_header, list);
|
||||
text=(TextView) itemView;
|
||||
if(red)
|
||||
text.setTextColor(getResources().getColor(UiUtils.isDarkTheme() ? R.color.error_400 : R.color.error_700));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(HeaderItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchViewHolder extends BindableViewHolder<SwitchItem> implements UsableRecyclerView.DisableableClickable{
|
||||
private final TextView text;
|
||||
private final ImageView icon;
|
||||
private final Switch checkbox;
|
||||
|
||||
public SwitchViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_switch, list);
|
||||
text=findViewById(R.id.text);
|
||||
icon=findViewById(R.id.icon);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SwitchItem item){
|
||||
text.setText(item.text);
|
||||
icon.setImageResource(item.icon);
|
||||
checkbox.setChecked(item.checked && item.enabled);
|
||||
checkbox.setEnabled(item.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.checked=!item.checked;
|
||||
checkbox.setChecked(item.checked);
|
||||
item.onChanged.accept(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private class ThemeViewHolder extends BindableViewHolder<ThemeItem>{
|
||||
private SubitemHolder autoHolder, lightHolder, darkHolder;
|
||||
|
||||
public ThemeViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_theme, list);
|
||||
autoHolder=new SubitemHolder(findViewById(R.id.theme_auto));
|
||||
lightHolder=new SubitemHolder(findViewById(R.id.theme_light));
|
||||
darkHolder=new SubitemHolder(findViewById(R.id.theme_dark));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ThemeItem item){
|
||||
bindSubitems();
|
||||
}
|
||||
|
||||
public void bindSubitems(){
|
||||
autoHolder.bind(R.string.theme_auto, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_auto_trueblack : R.drawable.theme_auto, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO);
|
||||
lightHolder.bind(R.string.theme_light, R.drawable.theme_light, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.LIGHT);
|
||||
darkHolder.bind(R.string.theme_dark, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_dark_trueblack : R.drawable.theme_dark, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK);
|
||||
}
|
||||
|
||||
private void onSubitemClick(View v){
|
||||
GlobalUserPreferences.ThemePreference pref;
|
||||
if(v.getId()==R.id.theme_auto)
|
||||
pref=GlobalUserPreferences.ThemePreference.AUTO;
|
||||
else if(v.getId()==R.id.theme_light)
|
||||
pref=GlobalUserPreferences.ThemePreference.LIGHT;
|
||||
else if(v.getId()==R.id.theme_dark)
|
||||
pref=GlobalUserPreferences.ThemePreference.DARK;
|
||||
else
|
||||
return;
|
||||
onThemePreferenceClick(pref);
|
||||
}
|
||||
|
||||
private class SubitemHolder{
|
||||
public TextView text;
|
||||
public ImageView icon;
|
||||
public RadioButton checkbox;
|
||||
|
||||
public SubitemHolder(View view){
|
||||
text=view.findViewById(R.id.text);
|
||||
icon=view.findViewById(R.id.icon);
|
||||
checkbox=view.findViewById(R.id.checkbox);
|
||||
view.setOnClickListener(ThemeViewHolder.this::onSubitemClick);
|
||||
|
||||
icon.setClipToOutline(true);
|
||||
icon.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
}
|
||||
|
||||
public void bind(int text, int icon, boolean checked){
|
||||
this.text.setText(text);
|
||||
this.icon.setImageResource(icon);
|
||||
checkbox.setChecked(checked);
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
checkbox.setChecked(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
|
||||
private final Button button;
|
||||
private final PopupMenu popupMenu;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public NotificationPolicyViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_notification_policy, list);
|
||||
button=findViewById(R.id.button);
|
||||
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.notification_policy);
|
||||
popupMenu.setOnMenuItemClickListener(item->{
|
||||
PushSubscription.Policy policy;
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.notify_anyone)
|
||||
policy=PushSubscription.Policy.ALL;
|
||||
else if(id==R.id.notify_followed)
|
||||
policy=PushSubscription.Policy.FOLLOWED;
|
||||
else if(id==R.id.notify_follower)
|
||||
policy=PushSubscription.Policy.FOLLOWER;
|
||||
else if(id==R.id.notify_none)
|
||||
policy=PushSubscription.Policy.NONE;
|
||||
else
|
||||
return false;
|
||||
onNotificationsPolicyChanged(policy);
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
|
||||
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
button.setOnClickListener(v->popupMenu.show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(NotificationPolicyItem item){
|
||||
button.setText(switch(getPushSubscription().policy){
|
||||
case ALL -> R.string.notify_anyone;
|
||||
case FOLLOWED -> R.string.notify_followed;
|
||||
case FOLLOWER -> R.string.notify_follower;
|
||||
case NONE -> R.string.notify_none;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
|
||||
private final TextView text;
|
||||
public FooterViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_footer, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(FooterItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
|
||||
private final TextView text;
|
||||
private final Button button;
|
||||
private final ImageButton cancelBtn;
|
||||
private final ProgressBar progress;
|
||||
|
||||
private ObjectAnimator rotationAnimator;
|
||||
private Runnable progressUpdater=this::updateProgress;
|
||||
|
||||
public UpdateViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_update, list);
|
||||
text=findViewById(R.id.text);
|
||||
button=findViewById(R.id.button);
|
||||
cancelBtn=findViewById(R.id.cancel_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
button.setOnClickListener(v->{
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
switch(updater.getState()){
|
||||
case UPDATE_AVAILABLE -> updater.downloadUpdate();
|
||||
case DOWNLOADED -> updater.installUpdate(getActivity());
|
||||
}
|
||||
});
|
||||
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
|
||||
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(UpdateItem item){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
}else{
|
||||
text.setText(getString(R.string.update_ready, info.version));
|
||||
button.setText(R.string.install_update);
|
||||
}
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
rotationAnimator.start();
|
||||
button.setVisibility(View.INVISIBLE);
|
||||
cancelBtn.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
updateProgress();
|
||||
}else{
|
||||
rotationAnimator.cancel();
|
||||
button.setVisibility(View.VISIBLE);
|
||||
cancelBtn.setVisibility(View.GONE);
|
||||
progress.setVisibility(View.GONE);
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
|
||||
return;
|
||||
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
progress.setProgress(value, true);
|
||||
else
|
||||
progress.setProgress(value);
|
||||
progress.postDelayed(progressUpdater, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,32 +57,32 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){}
|
||||
protected void onStatusCreated(Status status){}
|
||||
|
||||
protected void onStatusUpdated(StatusUpdatedEvent ev){
|
||||
protected void onStatusUpdated(Status status){
|
||||
ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
|
||||
for(int i=0;i<data.size();i++){
|
||||
Status s=data.get(i);
|
||||
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
|
||||
s.reblog=ev.status;
|
||||
if(s.reblog!=null && s.reblog.id.equals(status.id)){
|
||||
s.reblog=status.clone();
|
||||
statusesForDisplayItems.add(s);
|
||||
}else if(s.id.equals(ev.status.id)){
|
||||
data.set(i, ev.status);
|
||||
statusesForDisplayItems.add(ev.status);
|
||||
}else if(s.id.equals(status.id)){
|
||||
data.set(i, status);
|
||||
statusesForDisplayItems.add(status);
|
||||
}
|
||||
}
|
||||
for(int i=0;i<preloadedData.size();i++){
|
||||
Status s=preloadedData.get(i);
|
||||
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
|
||||
s.reblog=ev.status;
|
||||
}else if(s.id.equals(ev.status.id)){
|
||||
preloadedData.set(i, ev.status);
|
||||
if(s.reblog!=null && s.reblog.id.equals(status.id)){
|
||||
s.reblog=status.clone();
|
||||
}else if(s.id.equals(status.id)){
|
||||
preloadedData.set(i, status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,12 +209,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
StatusListFragment.this.onStatusCreated(ev);
|
||||
StatusListFragment.this.onStatusCreated(ev.status.clone());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusUpdated(StatusUpdatedEvent ev){
|
||||
StatusListFragment.this.onStatusUpdated(ev);
|
||||
StatusListFragment.this.onStatusUpdated(ev.status);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -224,7 +224,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
for(Status status:data){
|
||||
Status contentStatus=status.getContentStatus();
|
||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
||||
updatePoll(status.id, status, ev.poll);
|
||||
updatePoll(status.id, contentStatus, ev.poll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
@@ -23,10 +27,15 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment{
|
||||
private Status mainStatus;
|
||||
private ImageView endMark;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -37,7 +46,10 @@ public class ThreadFragment extends StatusListFragment{
|
||||
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
else
|
||||
setTitle(getString(R.string.post_from_user, mainStatus.account.displayName));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,8 +81,8 @@ public class ThreadFragment extends StatusListFragment{
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
}
|
||||
result.descendants=filterStatuses(result.descendants);
|
||||
result.ancestors=filterStatuses(result.ancestors);
|
||||
filterStatuses(result.descendants);
|
||||
filterStatuses(result.ancestors);
|
||||
if(footerProgress!=null)
|
||||
footerProgress.setVisibility(View.GONE);
|
||||
data.addAll(result.descendants);
|
||||
@@ -91,17 +103,8 @@ public class ThreadFragment extends StatusListFragment{
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
|
||||
if(filters.isEmpty())
|
||||
return statuses;
|
||||
return statuses.stream().filter(status->{
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList());
|
||||
private void filterStatuses(List<Status> statuses){
|
||||
AccountSessionManager.get(accountID).filterStatuses(statuses, FilterContext.THREAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,9 +125,10 @@ public class ThreadFragment extends StatusListFragment{
|
||||
footerProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
|
||||
onAppendItems(Collections.singletonList(ev.status));
|
||||
protected void onStatusCreated(Status status){
|
||||
if(status.inReplyToId!=null && getStatusByID(status.inReplyToId)!=null){
|
||||
onAppendItems(Collections.singletonList(status));
|
||||
data.add(status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,4 +136,36 @@ public class ThreadFragment extends StatusListFragment{
|
||||
public boolean isItemEnabled(String id){
|
||||
return !id.equals(mainStatus.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter a=new MergeRecyclerAdapter();
|
||||
a.addAdapter(super.getAdapter());
|
||||
|
||||
endMark=new ImageView(getActivity());
|
||||
endMark.setScaleType(ImageView.ScaleType.CENTER);
|
||||
endMark.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant)));
|
||||
endMark.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(25)));
|
||||
endMark.setImageResource(R.drawable.thread_end_mark);
|
||||
a.addAdapter(new SingleViewRecyclerAdapter(endMark));
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return bottomSibling==endMark;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onErrorRetryClick(){
|
||||
if(preloadingFailed){
|
||||
preloadingFailed=false;
|
||||
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
|
||||
V.setVisibilityAnimated(footerError, View.GONE);
|
||||
doLoadData();
|
||||
return;
|
||||
}
|
||||
super.onErrorRetryClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -43,20 +26,15 @@ import java.util.stream.Collectors;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseAccountListFragment.AccountItem>{
|
||||
public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<AccountViewModel>{
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
@@ -65,6 +43,10 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
super(40);
|
||||
}
|
||||
|
||||
public BaseAccountListFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -72,7 +54,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDataLoaded(List<AccountItem> d, boolean more){
|
||||
protected void onDataLoaded(List<AccountViewModel> d, boolean more){
|
||||
if(refreshing){
|
||||
relationships.clear();
|
||||
}
|
||||
@@ -89,7 +71,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
protected void loadRelationships(List<AccountItem> accounts){
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
@@ -125,9 +107,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// list.setPadding(0, V.dp(16), 0, V.dp(16));
|
||||
list.setClipToPadding(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@@ -160,6 +140,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
|
||||
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(16));
|
||||
@@ -167,6 +149,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -175,7 +159,9 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AccountViewHolder();
|
||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
|
||||
onConfigureViewHolder(holder);
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -196,199 +182,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
AccountItem item=data.get(position);
|
||||
AccountViewModel item=data.get(position);
|
||||
return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
protected class AccountViewHolder extends BindableViewHolder<AccountItem> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
|
||||
private final TextView name, username;
|
||||
private final ImageView avatar;
|
||||
private final Button button;
|
||||
private final PopupMenu contextMenu;
|
||||
private final View menuAnchor;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(getActivity(), R.layout.item_account_list, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
button=findViewById(R.id.button);
|
||||
menuAnchor=findViewById(R.id.menu_anchor);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
button.setOnClickListener(this::onButtonClick);
|
||||
|
||||
contextMenu=new PopupMenu(getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
public void bindRelationship(){
|
||||
Relationship rel=relationships.get(item.account.id);
|
||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||
button.setVisibility(View.GONE);
|
||||
}else{
|
||||
button.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButton(rel, button);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
}
|
||||
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(float x, float y){
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_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.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
ProgressDialog progress=new ProgressDialog(getActivity());
|
||||
progress.setMessage(getString(R.string.loading));
|
||||
progress.setCancelable(false);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
|
||||
itemView.setHasTransientState(progressShown);
|
||||
if(progressShown)
|
||||
progress.show();
|
||||
else
|
||||
progress.dismiss();
|
||||
}, result->{
|
||||
relationships.put(item.account.id, result);
|
||||
bindRelationship();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onContextMenuItemSelected(MenuItem item){
|
||||
Relationship relationship=relationships.get(this.item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Account account=this.item.account;
|
||||
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("reportAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(getActivity(), account.url);
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
|
||||
relationship.domainBlocking=!relationship.domainBlocking;
|
||||
bindRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
relationships.put(AccountViewHolder.this.item.account.id, result);
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateRelationship(Relationship r){
|
||||
relationships.put(item.account.id, r);
|
||||
bindRelationship();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class AccountItem{
|
||||
public final Account account;
|
||||
public final ImageLoaderRequest avaRequest;
|
||||
public final CustomEmojiHelper emojiHelper;
|
||||
public final CharSequence parsedName;
|
||||
|
||||
public AccountItem(Account account){
|
||||
this.account=account;
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private String currentQuery;
|
||||
private boolean resultDelivered;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
|
||||
searchViewHelper.setListeners(this::onQueryChanged, null);
|
||||
searchViewHelper.addDivider(contentView);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
view.setBackgroundResource(R.drawable.bg_m3_surface3);
|
||||
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
|
||||
setStatusBarColor(color);
|
||||
setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
searchViewHelper.install(getToolbar());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setOnClickListener(this::onItemClick);
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
|
||||
}
|
||||
|
||||
private void onItemClick(AccountViewHolder holder){
|
||||
if(resultDelivered)
|
||||
return;
|
||||
|
||||
resultDelivered=true;
|
||||
Bundle res=new Bundle();
|
||||
res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account));
|
||||
setResult(true, res);
|
||||
Nav.finish(this, false);
|
||||
}
|
||||
|
||||
private void onQueryChanged(String q){
|
||||
currentQuery=q;
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
if(!TextUtils.isEmpty(currentQuery))
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.account_list;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -23,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -1,80 +1,39 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.parceler.Parcels;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
|
||||
public DiscoverAccountsFragment(){
|
||||
super(20);
|
||||
}
|
||||
public class DiscoverAccountsFragment extends BaseAccountListFragment implements ScrollableToTop{
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.ACCOUNTS, accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(relationshipsRequest!=null){
|
||||
relationshipsRequest.cancel();
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
currentRequest=new GetFollowSuggestions(count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
|
||||
onDataLoaded(accounts, false);
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -82,219 +41,14 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new AccountsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
outRect.bottom=outRect.left=outRect.right=V.dp(16);
|
||||
if(parent.getChildAdapterPosition(view)==0)
|
||||
outRect.top=V.dp(16);
|
||||
}
|
||||
});
|
||||
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
|
||||
}
|
||||
|
||||
private void loadRelationships(){
|
||||
relationships=Collections.emptyMap();
|
||||
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
|
||||
relationshipsRequest.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequest=null;
|
||||
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof AccountViewHolder avh)
|
||||
avh.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(relationshipsRequest!=null){
|
||||
relationshipsRequest.cancel();
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, adapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AccountViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return 2+data.get(position).emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
AccountWrapper item=data.get(position);
|
||||
if(image==0)
|
||||
return item.avaRequest;
|
||||
else if(image==1)
|
||||
return item.coverRequest;
|
||||
else
|
||||
return item.emojiHelper.getImageRequest(image-2);
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final ImageView cover, avatar;
|
||||
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
|
||||
private final ProgressBarButton actionButton;
|
||||
private final ProgressBar actionProgress;
|
||||
private final View actionWrap;
|
||||
|
||||
private Relationship relationship;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(getActivity(), R.layout.item_discover_account, list);
|
||||
cover=findViewById(R.id.cover);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
bio=findViewById(R.id.bio);
|
||||
followersCount=findViewById(R.id.followers_count);
|
||||
followersLabel=findViewById(R.id.followers_label);
|
||||
followingCount=findViewById(R.id.following_count);
|
||||
followingLabel=findViewById(R.id.following_label);
|
||||
postsCount=findViewById(R.id.posts_count);
|
||||
postsLabel=findViewById(R.id.posts_label);
|
||||
actionButton=findViewById(R.id.action_btn);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
actionWrap=findViewById(R.id.action_btn_wrap);
|
||||
|
||||
itemView.setOutlineProvider(OutlineProviders.roundedRect(6));
|
||||
itemView.setClipToOutline(true);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
|
||||
cover.setClipToOutline(true);
|
||||
actionButton.setOnClickListener(this::onActionButtonClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountWrapper item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.account.acct);
|
||||
bio.setText(item.parsedBio);
|
||||
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||
relationship=relationships.get(item.account.id);
|
||||
if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
}else{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else if(index==1){
|
||||
cover.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-2, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
rebind();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
}
|
||||
|
||||
protected class AccountWrapper{
|
||||
public Account account;
|
||||
public ImageLoaderRequest avaRequest, coverRequest;
|
||||
public CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
public CharSequence parsedName, parsedBio;
|
||||
|
||||
public AccountWrapper(Account account){
|
||||
this.account=account;
|
||||
if(!TextUtils.isEmpty(account.avatar))
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
if(!TextUtils.isEmpty(account.header))
|
||||
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(account.emojis.isEmpty()){
|
||||
parsedName=account.displayName;
|
||||
}else{
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,19 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.app.Fragment;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -28,22 +25,24 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
|
||||
private static final int QUERY_RESULT=937;
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private EditText searchEdit;
|
||||
private boolean searchActive;
|
||||
private FrameLayout searchView;
|
||||
private ImageButton searchBack, searchClear;
|
||||
private ProgressBar searchProgress;
|
||||
private ImageButton searchBack;
|
||||
private TextView searchText;
|
||||
private View tabsDivider;
|
||||
|
||||
private DiscoverPostsFragment postsFragment;
|
||||
private TrendingHashtagsFragment hashtagsFragment;
|
||||
@@ -53,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private LocalTimelineFragment localTimelineFragment;
|
||||
|
||||
private String accountID;
|
||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||
private String currentQuery;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -88,8 +87,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
tabLayout.setTabTextSize(V.dp(16));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabLayout.setTabTextSize(V.dp(14));
|
||||
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setAdapter(new DiscoverPagerAdapter());
|
||||
@@ -146,7 +145,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
case 4 -> R.string.for_you;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
}
|
||||
});
|
||||
tabLayoutMediator.attach();
|
||||
@@ -163,58 +161,39 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
});
|
||||
|
||||
searchEdit=view.findViewById(R.id.search_edit);
|
||||
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
|
||||
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
||||
searchEdit.addTextChangedListener(new TextWatcher(){
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||
if(s.length()==0){
|
||||
V.setVisibilityAnimated(searchClear, View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
searchEdit.postDelayed(searchDebouncer, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
if(s.length()==0){
|
||||
V.setVisibilityAnimated(searchClear, View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchView=view.findViewById(R.id.search_fragment);
|
||||
if(searchFragment==null){
|
||||
searchFragment=new SearchFragment();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
searchFragment.setArguments(args);
|
||||
searchFragment.setProgressVisibilityListener(this::onSearchProgressVisibilityChanged);
|
||||
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
|
||||
}
|
||||
|
||||
searchBack=view.findViewById(R.id.search_back);
|
||||
searchClear=view.findViewById(R.id.search_clear);
|
||||
searchProgress=view.findViewById(R.id.search_progress);
|
||||
searchText=view.findViewById(R.id.search_text);
|
||||
searchBack.setEnabled(searchActive);
|
||||
searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
searchBack.setOnClickListener(v->exitSearch());
|
||||
if(searchActive){
|
||||
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
|
||||
searchBack.setImageResource(R.drawable.ic_arrow_back);
|
||||
pager.setVisibility(View.GONE);
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
searchView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
searchClear.setOnClickListener(v->{
|
||||
searchEdit.setText("");
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
onSearchChangedDebounced();
|
||||
|
||||
View searchWrap=view.findViewById(R.id.search_wrap);
|
||||
searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
|
||||
searchWrap.setClipToOutline(true);
|
||||
searchText.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if(!TextUtils.isEmpty(currentQuery)){
|
||||
args.putString("query", currentQuery);
|
||||
}
|
||||
Nav.goForResult(getActivity(), SearchQueryFragment.class, args, QUERY_RESULT, DiscoverFragment.this);
|
||||
});
|
||||
tabsDivider=view.findViewById(R.id.tabs_divider);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -233,35 +212,32 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
postsFragment.loadData();
|
||||
}
|
||||
|
||||
private void onSearchEditFocusChanged(View v, boolean hasFocus){
|
||||
if(!searchActive && hasFocus){
|
||||
private void enterSearch(){
|
||||
if(!searchActive){
|
||||
searchActive=true;
|
||||
pager.setVisibility(View.GONE);
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
searchView.setVisibility(View.VISIBLE);
|
||||
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
|
||||
searchBack.setImageResource(R.drawable.ic_arrow_back);
|
||||
searchBack.setEnabled(true);
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
tabsDivider.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void exitSearch(){
|
||||
if(!searchActive)
|
||||
return;
|
||||
searchActive=false;
|
||||
pager.setVisibility(View.VISIBLE);
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
searchView.setVisibility(View.GONE);
|
||||
searchEdit.clearFocus();
|
||||
searchEdit.setText("");
|
||||
searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular);
|
||||
searchText.setText(R.string.search_mastodon);
|
||||
searchBack.setImageResource(R.drawable.ic_search_24px);
|
||||
searchBack.setEnabled(false);
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
|
||||
tabsDivider.setVisibility(View.VISIBLE);
|
||||
currentQuery=null;
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
@@ -284,23 +260,20 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onSearchChangedDebounced(){
|
||||
searchFragment.setQuery(searchEdit.getText().toString());
|
||||
}
|
||||
|
||||
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
|
||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||
return true;
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
onSearchChangedDebounced();
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSearchProgressVisibilityChanged(boolean visible){
|
||||
V.setVisibilityAnimated(searchProgress, visible ? View.VISIBLE : View.INVISIBLE);
|
||||
if(searchEdit.length()>0)
|
||||
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(reqCode==QUERY_RESULT && success){
|
||||
enterSearch();
|
||||
currentQuery=result.getString("query");
|
||||
SearchResult.Type type;
|
||||
if(result.containsKey("filter")){
|
||||
type=SearchResult.Type.values()[result.getInt("filter")];
|
||||
}else{
|
||||
type=null;
|
||||
}
|
||||
searchFragment.setQuery(currentQuery, type);
|
||||
searchText.setText(currentQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -12,32 +13,43 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.model.viewmodel.CardViewModel;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop{
|
||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
private UsableRecyclerView cardsList;
|
||||
private ArrayList<CardViewModel> top3=new ArrayList<>();
|
||||
private CardLinksAdapter cardsAdapter;
|
||||
|
||||
public DiscoverNewsFragment(){
|
||||
super(10);
|
||||
@@ -47,6 +59,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,10 +68,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Card> result){
|
||||
imageRequests=result.stream()
|
||||
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150)))
|
||||
.collect(Collectors.toList());
|
||||
onDataLoaded(result, false);
|
||||
top3.clear();
|
||||
top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
|
||||
cardsAdapter.notifyDataSetChanged();
|
||||
|
||||
onDataLoaded(result.subList(top3.size(), result.size()).stream()
|
||||
.map(card->new CardViewModel(card, 56, 56))
|
||||
.collect(Collectors.toList()), false);
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -66,14 +83,27 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new LinksAdapter();
|
||||
}
|
||||
cardsList=new UsableRecyclerView(getActivity());
|
||||
cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
|
||||
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
|
||||
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
|
||||
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
|
||||
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
|
||||
cardsList.setClipToPadding(false);
|
||||
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
outRect.right=V.dp(16);
|
||||
}
|
||||
});
|
||||
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
|
||||
cardsList.setDrawSelectorOnTop(true);
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0));
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, mergeAdapter);
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
|
||||
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,14 +111,17 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public LinksAdapter(){
|
||||
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
private final List<CardViewModel> data;
|
||||
|
||||
public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
|
||||
super(imgLoader);
|
||||
this.data=data;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new LinkViewHolder();
|
||||
}
|
||||
|
||||
@@ -98,46 +131,51 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(LinkViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
public void onBindViewHolder(BaseLinkViewHolder holder, int position){
|
||||
holder.bind(data.get(position).card);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return imageRequests.get(position)==null ? 0 : 1;
|
||||
return data.get(position).imageRequest==null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return imageRequests.get(position);
|
||||
return data.get(position).imageRequest;
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
|
||||
private final TextView name, title, subtitle;
|
||||
private final ImageView photo;
|
||||
private class CardLinksAdapter extends LinksAdapter{
|
||||
public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
|
||||
super(imgLoader, data);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new LinkCardViewHolder();
|
||||
}
|
||||
}
|
||||
|
||||
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
|
||||
protected final TextView name, title;
|
||||
protected final ImageView photo;
|
||||
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||
private boolean didClear;
|
||||
|
||||
public LinkViewHolder(){
|
||||
super(getActivity(), R.layout.item_trending_link, list);
|
||||
public BaseLinkViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
name=findViewById(R.id.name);
|
||||
title=findViewById(R.id.title);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
photo=findViewById(R.id.photo);
|
||||
photo.setOutlineProvider(OutlineProviders.roundedRect(2));
|
||||
photo.setClipToOutline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Card item){
|
||||
name.setText(item.providerName);
|
||||
title.setText(item.title);
|
||||
int num=item.history.get(0).uses;
|
||||
if(item.history.size()>1)
|
||||
num+=item.history.get(1).uses;
|
||||
subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num));
|
||||
crossfadeDrawable.setSize(item.width, item.height);
|
||||
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
@@ -164,4 +202,20 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
UiUtils.launchWebBrowser(getActivity(), item.url);
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkViewHolder extends BaseLinkViewHolder{
|
||||
public LinkViewHolder(){
|
||||
super(R.layout.item_trending_link);
|
||||
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
photo.setClipToOutline(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkCardViewHolder extends BaseLinkViewHolder{
|
||||
public LinkCardViewHolder(){
|
||||
super(R.layout.item_trending_link_card);
|
||||
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
itemView.setClipToOutline(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
@@ -10,10 +9,18 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class DiscoverPostsFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
@@ -22,13 +29,16 @@ public class DiscoverPostsFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, adapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,29 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
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 androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class LocalTimelineFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
||||
@@ -27,15 +35,20 @@ public class LocalTimelineFragment extends StatusListFragment{
|
||||
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());
|
||||
boolean empty=result.isEmpty();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||
onDataLoaded(result, !empty);
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, adapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
@@ -22,7 +21,6 @@ import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -33,12 +31,9 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
@@ -46,12 +41,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
private List<StatusDisplayItem> prevDisplayItems;
|
||||
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
||||
private List<SearchResult> unfilteredResults=Collections.emptyList();
|
||||
private HideableSingleViewRecyclerAdapter headerAdapter;
|
||||
private ProgressVisibilityListener progressVisibilityListener;
|
||||
private InputMethodManager imm;
|
||||
|
||||
private TabLayout tabLayout;
|
||||
|
||||
public SearchFragment(){
|
||||
setLayout(R.layout.fragment_search);
|
||||
}
|
||||
@@ -61,6 +52,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
super.onCreate(savedInstanceState);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
setEmptyText(R.string.no_search_results);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@@ -119,51 +111,52 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(isInRecentMode()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
unfilteredResults=sr;
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
onDataLoaded(sr, false);
|
||||
});
|
||||
GetSearchResults.Type type;
|
||||
if(currentFilter.size()==1){
|
||||
type=switch(currentFilter.iterator().next()){
|
||||
case ACCOUNT -> GetSearchResults.Type.ACCOUNTS;
|
||||
case HASHTAG -> GetSearchResults.Type.HASHTAGS;
|
||||
case STATUS -> GetSearchResults.Type.STATUSES;
|
||||
};
|
||||
}else{
|
||||
progressVisibilityListener.onProgressVisibilityChanged(true);
|
||||
currentRequest=new GetSearchResults(currentQuery, null, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
ArrayList<SearchResult> results=new ArrayList<>();
|
||||
if(result.accounts!=null){
|
||||
for(Account acc:result.accounts)
|
||||
results.add(new SearchResult(acc));
|
||||
}
|
||||
if(result.hashtags!=null){
|
||||
for(Hashtag tag:result.hashtags)
|
||||
results.add(new SearchResult(tag));
|
||||
}
|
||||
if(result.statuses!=null){
|
||||
for(Status status:result.statuses)
|
||||
results.add(new SearchResult(status));
|
||||
}
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
unfilteredResults=results;
|
||||
onDataLoaded(filterSearchResults(results), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
Activity a=getActivity();
|
||||
if(a==null)
|
||||
return;
|
||||
error.showToast(a);
|
||||
if(progressVisibilityListener!=null)
|
||||
progressVisibilityListener.onProgressVisibilityChanged(false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
type=null;
|
||||
}
|
||||
if(currentQuery==null){
|
||||
dataLoaded();
|
||||
return;
|
||||
}
|
||||
currentRequest=new GetSearchResults(currentQuery, type, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
ArrayList<SearchResult> results=new ArrayList<>();
|
||||
if(result.accounts!=null){
|
||||
for(Account acc:result.accounts)
|
||||
results.add(new SearchResult(acc));
|
||||
}
|
||||
if(result.hashtags!=null){
|
||||
for(Hashtag tag:result.hashtags)
|
||||
results.add(new SearchResult(tag));
|
||||
}
|
||||
if(result.statuses!=null){
|
||||
for(Status status:result.statuses)
|
||||
results.add(new SearchResult(status));
|
||||
}
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
unfilteredResults=results;
|
||||
onDataLoaded(filterSearchResults(results), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
Activity a=getActivity();
|
||||
if(a==null)
|
||||
return;
|
||||
error.showToast(a);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -173,86 +166,30 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
return;
|
||||
}
|
||||
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
|
||||
boolean recent=isInRecentMode();
|
||||
if(recent!=headerAdapter.isVisible())
|
||||
headerAdapter.setVisible(recent);
|
||||
imgLoader.forceUpdateImages();
|
||||
prevDisplayItems=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDataLoaded(List<SearchResult> d, boolean more){
|
||||
super.onDataLoaded(d, more);
|
||||
if(progressVisibilityListener!=null)
|
||||
progressVisibilityListener.onProgressVisibilityChanged(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
tabLayout.setTabTextSize(V.dp(16));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_all));
|
||||
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_people));
|
||||
tabLayout.addTab(tabLayout.newTab().setText(R.string.hashtags));
|
||||
tabLayout.addTab(tabLayout.newTab().setText(R.string.posts));
|
||||
for(int i=0;i<tabLayout.getTabCount();i++){
|
||||
tabLayout.getTabAt(i).view.textView.setAllCaps(true);
|
||||
}
|
||||
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab){
|
||||
setFilter(switch(tab.getPosition()){
|
||||
case 0 -> EnumSet.allOf(SearchResult.Type.class);
|
||||
case 1 -> EnumSet.of(SearchResult.Type.ACCOUNT);
|
||||
case 2 -> EnumSet.of(SearchResult.Type.HASHTAG);
|
||||
case 3 -> EnumSet.of(SearchResult.Type.STATUS);
|
||||
default -> throw new IllegalStateException("Unexpected value: "+tab.getPosition());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
View header=getActivity().getLayoutInflater().inflate(R.layout.item_recent_searches_header, list, false);
|
||||
header.findViewById(R.id.clear).setOnClickListener(this::onClearRecentClick);
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(headerAdapter=new HideableSingleViewRecyclerAdapter(header));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
headerAdapter.setVisible(isInRecentMode());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public void setQuery(String q){
|
||||
if(Objects.equals(q, currentQuery) || q.isBlank())
|
||||
public void setQuery(String q, SearchResult.Type filter){
|
||||
if(q.isBlank())
|
||||
return;
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
currentQuery=q;
|
||||
if(filter==null)
|
||||
currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
||||
else
|
||||
currentFilter=EnumSet.of(filter);
|
||||
refreshing=true;
|
||||
doLoadData(0, 0);
|
||||
loadData();
|
||||
}
|
||||
|
||||
private void setFilter(EnumSet<SearchResult.Type> filter){
|
||||
if(filter.equals(currentFilter))
|
||||
return;
|
||||
currentFilter=filter;
|
||||
if(isInRecentMode())
|
||||
return;
|
||||
// This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
refreshing=true;
|
||||
@@ -274,23 +211,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
return null;
|
||||
}
|
||||
|
||||
private void onClearRecentClick(View v){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
|
||||
if(isInRecentMode()){
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
refreshing=true;
|
||||
onDataLoaded(unfilteredResults=Collections.emptyList(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInRecentMode(){
|
||||
return TextUtils.isEmpty(currentQuery);
|
||||
}
|
||||
|
||||
public void setProgressVisibilityListener(ProgressVisibilityListener progressVisibilityListener){
|
||||
this.progressVisibilityListener=progressVisibilityListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStarted(){
|
||||
super.onScrollStarted();
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Fragment;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.RoundedCorner;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.SearchResultViewModel;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
|
||||
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
private HideableSingleViewRecyclerAdapter recentsHeader;
|
||||
private ListItem<Void> openUrlItem, goToHashtagItem, goToAccountItem, goToStatusSearchItem, goToAccountSearchItem;
|
||||
private ArrayList<ListItem<Void>> topOptions=new ArrayList<>();
|
||||
private GenericListItemsAdapter<Void> topOptionsAdapter;
|
||||
|
||||
private String accountID;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
private String currentQuery;
|
||||
private LayerDrawable navigationIcon;
|
||||
private Drawable searchIcon, backIcon;
|
||||
|
||||
public SearchQueryFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
|
||||
openUrlItem=new ListItem<>(R.string.search_open_url, 0, R.drawable.ic_link_24px, this::onOpenURLClick);
|
||||
goToHashtagItem=new ListItem<>("", null, R.drawable.ic_tag_24px, this::onGoToHashtagClick);
|
||||
goToAccountItem=new ListItem<>("", null, R.drawable.ic_person_24px, this::onGoToAccountClick);
|
||||
goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_search_24px, this::onGoToStatusSearchClick);
|
||||
goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_group_24px, this::onGoToAccountSearchClick);
|
||||
currentQuery=getArguments().getString("query");
|
||||
|
||||
dataLoaded();
|
||||
doLoadData(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(isInRecentMode()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(results->{
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
|
||||
onDataLoaded(results.stream().map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.onClick=()->openHashtag(sr);
|
||||
}
|
||||
return vm;
|
||||
}).collect(Collectors.toList()), false);
|
||||
recentsHeader.setVisible(!data.isEmpty());
|
||||
});
|
||||
}else{
|
||||
currentRequest=new GetSearchResults(currentQuery, null, false)
|
||||
.limit(2)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
|
||||
.flatMap(Function.identity())
|
||||
.map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.onClick=()->openHashtag(sr);
|
||||
}
|
||||
return vm;
|
||||
})
|
||||
.collect(Collectors.toList()), false);
|
||||
recentsHeader.setVisible(false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View header=getActivity().getLayoutInflater().inflate(R.layout.display_item_section_header, list, false);
|
||||
TextView title=header.findViewById(R.id.title);
|
||||
Button action=header.findViewById(R.id.action_btn);
|
||||
title.setText(R.string.recent_searches);
|
||||
action.setText(R.string.clear_all);
|
||||
action.setOnClickListener(v->onClearRecentClick());
|
||||
recentsHeader=new HideableSingleViewRecyclerAdapter(header);
|
||||
|
||||
mergeAdapter.addAdapter(recentsHeader);
|
||||
mergeAdapter.addAdapter(topOptionsAdapter=new GenericListItemsAdapter<>(topOptions));
|
||||
mergeAdapter.addAdapter(new SearchResultsAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_mastodon));
|
||||
searchViewHelper.setListeners(this::onQueryChanged, this::onQueryChangedNoDebounce);
|
||||
searchViewHelper.addDivider(contentView);
|
||||
searchViewHelper.setEnterCallback(this::onSearchViewEnter);
|
||||
|
||||
navigationIcon=new LayerDrawable(new Drawable[]{
|
||||
searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_search_24px, getToolbarContext().getTheme()).mutate(),
|
||||
backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_arrow_back, getToolbarContext().getTheme()).mutate()
|
||||
}){
|
||||
@Override
|
||||
public Drawable mutate(){
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
view.setBackgroundResource(R.drawable.bg_m3_surface3);
|
||||
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
|
||||
setStatusBarColor(color);
|
||||
setNavigationBarColor(color);
|
||||
if(currentQuery!=null){
|
||||
searchViewHelper.setQuery(currentQuery);
|
||||
searchIcon.setAlpha(0);
|
||||
}
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=V.dp(8);
|
||||
searchViewHelper.install(getToolbar());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onQueryChanged(String q){
|
||||
currentQuery=q;
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
refreshing=true;
|
||||
doLoadData(0, 0);
|
||||
}
|
||||
|
||||
private void onQueryChangedNoDebounce(String q){
|
||||
updateTopOptions(q);
|
||||
if(!TextUtils.isEmpty(q)){
|
||||
recentsHeader.setVisible(false);
|
||||
}
|
||||
data.clear();
|
||||
mergeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void updateTopOptions(String q){
|
||||
topOptions.clear();
|
||||
// https://github.com/mastodon/mastodon/blob/a985d587e13494b78ef2879e4d97f78a2df693db/app/javascript/mastodon/features/compose/components/search.jsx#L233
|
||||
String trimmedValue=q.trim();
|
||||
if(trimmedValue.length()>0){
|
||||
boolean couldBeURL=trimmedValue.startsWith("https://") && !trimmedValue.contains(" ");
|
||||
if(couldBeURL){
|
||||
topOptions.add(openUrlItem);
|
||||
}
|
||||
|
||||
boolean couldBeHashtag=(trimmedValue.startsWith("#") && trimmedValue.length()>1 && !trimmedValue.contains(" ")) || HASHTAG_REGEX.matcher(trimmedValue).find();
|
||||
if(couldBeHashtag){
|
||||
String tag=trimmedValue.startsWith("#") ? trimmedValue.substring(1) : trimmedValue;
|
||||
goToHashtagItem.title=getString(R.string.posts_matching_hashtag, "#"+tag);
|
||||
topOptions.add(goToHashtagItem);
|
||||
}
|
||||
|
||||
Matcher usernameMatcher=USERNAME_REGEX.matcher(trimmedValue);
|
||||
if(usernameMatcher.find()){
|
||||
String username="@"+usernameMatcher.group(1);
|
||||
String atDomain=usernameMatcher.group(2);
|
||||
if(atDomain==null){
|
||||
username+="@"+AccountSessionManager.get(accountID).domain;
|
||||
}
|
||||
goToAccountItem.title=getString(R.string.search_go_to_account, username);
|
||||
topOptions.add(goToAccountItem);
|
||||
}
|
||||
|
||||
goToStatusSearchItem.title=getString(R.string.posts_matching_string, trimmedValue);
|
||||
topOptions.add(goToStatusSearchItem);
|
||||
goToAccountSearchItem.title=getString(R.string.accounts_matching_string, trimmedValue);
|
||||
topOptions.add(goToAccountSearchItem);
|
||||
}
|
||||
topOptionsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateEnterTransition(View prev, View container){
|
||||
return createTransition(prev, container, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateExitTransition(View prev, View container){
|
||||
return createTransition(prev, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsCustomNavigationIcon(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Drawable getNavigationIconDrawable(){
|
||||
return navigationIcon;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
getActivity().getSystemService(InputMethodManager.class).showSoftInput(getActivity().getCurrentFocus(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.S)
|
||||
private float getScreenCornerRadius(WindowInsets insets, int pos){
|
||||
RoundedCorner corner=insets.getRoundedCorner(pos);
|
||||
if(corner==null)
|
||||
return 0;
|
||||
return corner.getRadius();
|
||||
}
|
||||
|
||||
private Animator createTransition(View prev, View container, boolean enter){
|
||||
int[] loc={0, 0};
|
||||
View searchBtn=prev.findViewById(R.id.search_wrap);
|
||||
searchBtn.getLocationInWindow(loc);
|
||||
int btnLeft=loc[0], btnTop=loc[1];
|
||||
container.getLocationInWindow(loc);
|
||||
int offX=btnLeft-loc[0], offY=btnTop-loc[1];
|
||||
|
||||
float screenRadius;
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
|
||||
WindowInsets insets=container.getRootWindowInsets();
|
||||
screenRadius=Math.min(
|
||||
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT)),
|
||||
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT))
|
||||
);
|
||||
}else{
|
||||
screenRadius=0;
|
||||
}
|
||||
float buttonRadius=V.dp(26);
|
||||
|
||||
Rect buttonBounds=new Rect(offX, offY, offX+searchBtn.getWidth(), offY+searchBtn.getHeight());
|
||||
Rect containerBounds=new Rect(0, 0, container.getWidth(), container.getHeight());
|
||||
AnimatableOutlineProvider outlineProvider=new AnimatableOutlineProvider(enter ? buttonBounds : containerBounds, enter ? containerBounds : buttonBounds, enter ? buttonRadius : screenRadius);
|
||||
container.setOutlineProvider(outlineProvider);
|
||||
container.setClipToOutline(true);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
ObjectAnimator boundsAnim;
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
float toolbarTX=offX-toolbar.getX();
|
||||
float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f;
|
||||
ArrayList<Animator> anims=new ArrayList<>();
|
||||
anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f));
|
||||
anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius));
|
||||
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX));
|
||||
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY));
|
||||
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getSearchLayout(), View.TRANSLATION_X, enter ? V.dp(-4) : 0, enter ? 0 : V.dp(-4)));
|
||||
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
|
||||
View parentContent=prev.findViewById(R.id.discover_content);
|
||||
View parentContentParent=(View) parentContent.getParent();
|
||||
parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
|
||||
if(enter){
|
||||
anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0));
|
||||
}else{
|
||||
}
|
||||
anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
|
||||
for(Animator anim:anims){
|
||||
anim.setDuration(enter ? 700 : 300);
|
||||
}
|
||||
if(TextUtils.isEmpty(currentQuery)){
|
||||
anims.add(ObjectAnimator.ofInt(searchIcon, "alpha", enter ? 255 : 0, enter ? 0 : 255).setDuration(200));
|
||||
anims.add(ObjectAnimator.ofInt(backIcon, "alpha", enter ? 0 : 255, enter ? 255 : 0).setDuration(200));
|
||||
}
|
||||
ObjectAnimator parentContentFade;
|
||||
anims.add(parentContentFade=ObjectAnimator.ofFloat(parentContent, View.ALPHA, enter ? 1 : 0, enter ? 0 : 1).setDuration(enter ? 350 : 250));
|
||||
if(!enter){
|
||||
parentContentFade.setStartDelay(50);
|
||||
ObjectAnimator parentContentTY;
|
||||
anims.add(parentContentTY=ObjectAnimator.ofFloat(parentContent, View.TRANSLATION_Y, V.dp(16), 0).setDuration(250));
|
||||
parentContentTY.setStartDelay(50);
|
||||
}
|
||||
|
||||
set.playTogether(anims);
|
||||
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_emphasized_decelerate));
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
container.setOutlineProvider(null);
|
||||
container.setClipToOutline(false);
|
||||
parentContentParent.setBackground(null);
|
||||
}
|
||||
});
|
||||
boundsAnim.addUpdateListener(animation->{
|
||||
container.invalidateOutline();
|
||||
navigationIcon.invalidateSelf();
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
private void openHashtag(SearchResult res){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
|
||||
}
|
||||
|
||||
private boolean isInRecentMode(){
|
||||
return TextUtils.isEmpty(currentQuery);
|
||||
}
|
||||
|
||||
private void onSearchViewEnter(){
|
||||
deliverResult(currentQuery, null);
|
||||
}
|
||||
|
||||
private void onOpenURLClick(){
|
||||
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
|
||||
}
|
||||
|
||||
private void onGoToHashtagClick(){
|
||||
String q=searchViewHelper.getQuery();
|
||||
if(q.startsWith("#"))
|
||||
q=q.substring(1);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, q);
|
||||
}
|
||||
|
||||
private void onGoToAccountClick(){
|
||||
String q=searchViewHelper.getQuery();
|
||||
if(!q.startsWith("@")){
|
||||
q="@"+q;
|
||||
}
|
||||
if(q.lastIndexOf('@')==0){
|
||||
q+="@"+AccountSessionManager.get(accountID).domain;
|
||||
}
|
||||
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
|
||||
}
|
||||
|
||||
private void onGoToStatusSearchClick(){
|
||||
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
|
||||
}
|
||||
|
||||
private void onGoToAccountSearchClick(){
|
||||
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
|
||||
}
|
||||
|
||||
private void onClearRecentClick(){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
|
||||
if(isInRecentMode()){
|
||||
data.clear();
|
||||
recentsHeader.setVisible(false);
|
||||
mergeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverResult(String query, SearchResult.Type typeFilter){
|
||||
Bundle res=new Bundle();
|
||||
res.putString("query", query);
|
||||
if(typeFilter!=null)
|
||||
res.putInt("filter", typeFilter.ordinal());
|
||||
setResult(true, res);
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
String initialQuery=getArguments().getString("query");
|
||||
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
|
||||
currentQuery=initialQuery;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
|
||||
private float boundsFraction, radius;
|
||||
private final Rect boundsFrom, boundsTo;
|
||||
|
||||
private AnimatableOutlineProvider(Rect boundsFrom, Rect boundsTo, float radius){
|
||||
this.boundsFrom=boundsFrom;
|
||||
this.boundsTo=boundsTo;
|
||||
this.radius=radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(
|
||||
UiUtils.lerp(boundsFrom.left, boundsTo.left, boundsFraction),
|
||||
UiUtils.lerp(boundsFrom.top, boundsTo.top, boundsFraction),
|
||||
UiUtils.lerp(boundsFrom.right, boundsTo.right, boundsFraction),
|
||||
UiUtils.lerp(boundsFrom.bottom, boundsTo.bottom, boundsFraction),
|
||||
radius
|
||||
);
|
||||
}
|
||||
|
||||
@Keep
|
||||
public float getBoundsFraction(){
|
||||
return boundsFraction;
|
||||
}
|
||||
|
||||
@Keep
|
||||
public void setBoundsFraction(float boundsFraction){
|
||||
this.boundsFraction=boundsFraction;
|
||||
}
|
||||
|
||||
@Keep
|
||||
public float getRadius(){
|
||||
return radius;
|
||||
}
|
||||
|
||||
@Keep
|
||||
public void setRadius(float radius){
|
||||
this.radius=radius;
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchResultsAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public SearchResultsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
if(viewType==R.id.list_item_account){
|
||||
return new CustomAccountViewHolder(SearchQueryFragment.this, parent, null);
|
||||
}else if(viewType==R.id.list_item_simple){
|
||||
return new SimpleListItemViewHolder(parent.getContext(), parent);
|
||||
}
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position){
|
||||
if(holder instanceof CustomAccountViewHolder avh){
|
||||
avh.bind(data.get(position).account);
|
||||
avh.searchResult=data.get(position).result;
|
||||
}else if(holder instanceof SimpleListItemViewHolder ivh){
|
||||
ivh.bind(data.get(position).hashtagItem);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return switch(data.get(position).result.type){
|
||||
case ACCOUNT -> R.id.list_item_account;
|
||||
case HASHTAG -> R.id.list_item_simple;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+data.get(position).result.type);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
SearchResultViewModel vm=data.get(position);
|
||||
if(vm.account!=null)
|
||||
return vm.account.emojiHelper.getImageCount()+1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
SearchResultViewModel vm=data.get(position);
|
||||
if(vm.account!=null){
|
||||
if(image==0)
|
||||
return vm.account.avaRequest;
|
||||
return vm.account.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomAccountViewHolder extends AccountViewHolder{
|
||||
public SearchResult searchResult;
|
||||
|
||||
public CustomAccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
|
||||
super(fragment, list, relationships);
|
||||
setStyle(AccessoryType.NONE, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
super.onClick();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(searchResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
|
||||
|
||||
public TrendingHashtagsFragment(){
|
||||
super(10);
|
||||
@@ -54,13 +53,6 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
||||
return new HashtagsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16));
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.SettingsFragment;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AccountActivationFragment extends ToolbarFragment{
|
||||
private String accountID;
|
||||
@@ -70,7 +67,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
openEmailBtn.setOnLongClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
return true;
|
||||
});
|
||||
resendBtn=view.findViewById(R.id.btn_resend);
|
||||
@@ -89,13 +86,6 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
@@ -110,18 +100,12 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
new AccountSwitcherSheet(getActivity()).show();
|
||||
new AccountSwitcherSheet(getActivity(), null).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
contentView.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(contentView, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -75,7 +75,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
|
||||
items.add(new Item("Mastodon for Android Privacy Policy", getString(R.string.privacy_policy_explanation), "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
|
||||
@@ -123,16 +123,12 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
@@ -155,13 +151,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private void loadServerPrivacyPolicy(){
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
@@ -330,13 +331,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -164,7 +164,6 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
@@ -57,7 +45,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
setTitle(R.string.instance_rules_title);
|
||||
}
|
||||
@@ -74,9 +62,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(new ItemsAdapter());
|
||||
adapter.addAdapter(new InstanceRulesAdapter(instance.rules));
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
@@ -90,16 +77,12 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
@@ -121,51 +104,6 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(instance.rules.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return instance.rules.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
|
||||
private final TextView text, number;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_server_rule, list);
|
||||
text=findViewById(R.id.text);
|
||||
number=findViewById(R.id.number);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(Instance.Rule item){
|
||||
if(item.parsedText==null){
|
||||
item.parsedText=HtmlParser.parseLinks(item.text);
|
||||
}
|
||||
text.setText(item.parsedText);
|
||||
number.setText(String.format("%d", getAbsoluteAdapterPosition()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,33 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.ParsedAccount;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
private View buttonBar;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private int numRunningFollowRequests=0;
|
||||
@@ -77,8 +49,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
|
||||
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
|
||||
@@ -88,8 +58,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
@@ -101,51 +69,15 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void loadRelationships(){
|
||||
relationships=Collections.emptyMap();
|
||||
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
|
||||
relationshipsRequest.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequest=null;
|
||||
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof SuggestionViewHolder svh)
|
||||
svh.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new SuggestionsAdapter();
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private void onFollowAllClick(View v){
|
||||
@@ -156,7 +88,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
return;
|
||||
}
|
||||
ArrayList<String> accountIdsToFollow=new ArrayList<>();
|
||||
for(ParsedAccount acc:data){
|
||||
for(AccountViewModel acc:data){
|
||||
Relationship rel=relationships.get(acc.account.id);
|
||||
if(rel==null)
|
||||
continue;
|
||||
@@ -193,7 +125,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
public void onSuccess(Relationship result){
|
||||
relationships.put(id, result);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof SuggestionViewHolder svh && svh.getItem().account.id.equals(id)){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof AccountViewHolder svh && svh.getItem().account.id.equals(id)){
|
||||
svh.rebind();
|
||||
break;
|
||||
}
|
||||
@@ -219,126 +151,9 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
Nav.go(getActivity(), OnboardingProfileSetupFragment.class, args);
|
||||
}
|
||||
|
||||
private class SuggestionsAdapter extends UsableRecyclerView.Adapter<SuggestionViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public SuggestionsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new SuggestionViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SuggestionViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return data.get(position).emojiHelper.getImageCount()+1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ParsedAccount account=data.get(position);
|
||||
if(image==0)
|
||||
return account.avatarRequest;
|
||||
return account.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name, username, bio;
|
||||
private final ImageView avatar;
|
||||
private final ProgressBarButton actionButton;
|
||||
private final ProgressBar actionProgress;
|
||||
private final View actionWrap;
|
||||
|
||||
private Relationship relationship;
|
||||
|
||||
public SuggestionViewHolder(){
|
||||
super(getActivity(), R.layout.item_user_row_m3, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
bio=findViewById(R.id.bio);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
actionButton=findViewById(R.id.action_btn);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
actionWrap=findViewById(R.id.action_btn_wrap);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
actionButton.setOnClickListener(UiUtils.rateLimitedClickListener(this::onActionButtonClick));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ParsedAccount item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText(item.account.getDisplayUsername());
|
||||
if(TextUtils.isEmpty(item.parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
bio.setVisibility(View.VISIBLE);
|
||||
bio.setText(item.parsedBio);
|
||||
}
|
||||
|
||||
relationship=relationships.get(item.account.id);
|
||||
if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
}else{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
rebind();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.profile_setup);
|
||||
}
|
||||
@@ -118,16 +118,12 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
scroller.setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
@@ -165,13 +161,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private View makeFieldsRow(){
|
||||
|
||||
@@ -148,16 +148,12 @@ public class SignupFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
@@ -370,13 +366,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private void onGoBackLinkClick(LinkSpan span){
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -23,14 +21,9 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
|
||||
@@ -38,12 +31,13 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
|
||||
private MergeRecyclerAdapter adapter;
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
protected ArrayList<Item> items=new ArrayList<>();
|
||||
protected ArrayList<ChoiceItem> items=new ArrayList<>();
|
||||
protected boolean isMultipleChoice;
|
||||
protected ArrayList<String> selectedIDs=new ArrayList<>();
|
||||
protected String accountID;
|
||||
protected Account reportAccount;
|
||||
protected Status reportStatus;
|
||||
protected ProgressBar progressBar;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -61,7 +55,7 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
|
||||
accountID=getArguments().getString("account");
|
||||
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
|
||||
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
@@ -75,122 +69,33 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
|
||||
list=view.findViewById(R.id.list);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
populateItems();
|
||||
Item header=getHeaderItem();
|
||||
ChoiceItem header=getHeaderItem();
|
||||
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
|
||||
TextView title=headerView.findViewById(R.id.title);
|
||||
TextView subtitle=headerView.findViewById(R.id.subtitle);
|
||||
TextView stepCounter=headerView.findViewById(R.id.step_counter);
|
||||
title.setText(header.title);
|
||||
subtitle.setText(header.subtitle);
|
||||
stepCounter.setText(getString(R.string.step_x_of_n, getStepNumber(), 3));
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(new ItemsAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
progressBar=view.findViewById(R.id.top_progress);
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(new ChoiceItemsAdapter(getActivity(), isMultipleChoice, items, list, selectedIDs, btn::setEnabled));
|
||||
list.setAdapter(adapter);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
protected abstract Item getHeaderItem();
|
||||
protected abstract ChoiceItem getHeaderItem();
|
||||
protected abstract void populateItems();
|
||||
protected abstract void onButtonClick();
|
||||
protected abstract int getStepNumber();
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
protected static class Item{
|
||||
public String title, subtitle, id;
|
||||
|
||||
public Item(String title, String subtitle, String id){
|
||||
this.title=title;
|
||||
this.subtitle=subtitle;
|
||||
this.id=id;
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title, subtitle;
|
||||
private final ImageView checkbox;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_report_choice, list);
|
||||
title=findViewById(R.id.title);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item item){
|
||||
title.setText(item.title);
|
||||
if(TextUtils.isEmpty(item.subtitle)){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
subtitle.setText(item.subtitle);
|
||||
}
|
||||
checkbox.setSelected(selectedIDs.contains(item.id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(isMultipleChoice){
|
||||
if(selectedIDs.contains(item.id))
|
||||
selectedIDs.remove(item.id);
|
||||
else
|
||||
selectedIDs.add(item.id);
|
||||
rebind();
|
||||
}else{
|
||||
if(!selectedIDs.contains(item.id)){
|
||||
if(!selectedIDs.isEmpty()){
|
||||
String prev=selectedIDs.remove(0);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof ItemViewHolder ivh && ivh.getItem().id.equals(prev)){
|
||||
ivh.rebind();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedIDs.add(item.id);
|
||||
rebind();
|
||||
}
|
||||
}
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
class ChoiceItem{
|
||||
public String title, subtitle, id;
|
||||
|
||||
public ChoiceItem(String title, String subtitle, String id){
|
||||
this.title=title;
|
||||
this.subtitle=subtitle;
|
||||
this.id=id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
class ChoiceItemViewHolder extends BindableViewHolder<ChoiceItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title, subtitle;
|
||||
private final View checkbox;
|
||||
private final CheckableLinearLayout view;
|
||||
private final boolean isMultipleChoice;
|
||||
private final RecyclerView list;
|
||||
private final ArrayList<String> selectedIDs;
|
||||
private final Consumer<Boolean> buttonEnabledSetter;
|
||||
|
||||
public ChoiceItemViewHolder(Context context, boolean isMultipleChoice, RecyclerView list, ArrayList<String> selectedIDs, Consumer<Boolean> buttonEnabledSetter){
|
||||
super(context, R.layout.item_report_choice, list);
|
||||
this.buttonEnabledSetter=buttonEnabledSetter;
|
||||
this.isMultipleChoice=isMultipleChoice;
|
||||
this.list=list;
|
||||
this.selectedIDs=selectedIDs;
|
||||
title=findViewById(R.id.title);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
CompoundButton cb=isMultipleChoice ? new CheckBox(context) : new RadioButton(context);
|
||||
checkbox.setBackground(cb.getButtonDrawable());
|
||||
view=(CheckableLinearLayout) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ChoiceItem item){
|
||||
title.setText(item.title);
|
||||
if(TextUtils.isEmpty(item.subtitle)){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
view.setMinimumHeight(V.dp(56));
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
subtitle.setText(item.subtitle);
|
||||
view.setMinimumHeight(V.dp(72));
|
||||
}
|
||||
view.setChecked(selectedIDs.contains(item.id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(isMultipleChoice){
|
||||
if(selectedIDs.contains(item.id))
|
||||
selectedIDs.remove(item.id);
|
||||
else
|
||||
selectedIDs.add(item.id);
|
||||
rebind();
|
||||
}else{
|
||||
if(!selectedIDs.contains(item.id)){
|
||||
if(!selectedIDs.isEmpty()){
|
||||
String prev=selectedIDs.remove(0);
|
||||
for(int i=0; i<list.getChildCount(); i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof ChoiceItemViewHolder ivh && ivh.getItem().id.equals(prev)){
|
||||
ivh.rebind();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedIDs.add(item.id);
|
||||
rebind();
|
||||
}
|
||||
}
|
||||
buttonEnabledSetter.accept(!selectedIDs.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
class ChoiceItemsAdapter extends RecyclerView.Adapter<ChoiceItemViewHolder>{
|
||||
|
||||
private final Context context;
|
||||
private final boolean isMultipleChoice;
|
||||
private final ArrayList<ChoiceItem> items;
|
||||
private final RecyclerView list;
|
||||
private final ArrayList<String> selectedIDs;
|
||||
private final Consumer<Boolean> buttonEnabledSetter;
|
||||
|
||||
public ChoiceItemsAdapter(Context context, boolean isMultipleChoice, ArrayList<ChoiceItem> items, RecyclerView list, ArrayList<String> selectedIDs, Consumer<Boolean> buttonEnabledSetter){
|
||||
this.context=context;
|
||||
this.isMultipleChoice=isMultipleChoice;
|
||||
this.items=items;
|
||||
this.list=list;
|
||||
this.selectedIDs=selectedIDs;
|
||||
this.buttonEnabledSetter=buttonEnabledSetter;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ChoiceItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ChoiceItemViewHolder(context, isMultipleChoice, list, selectedIDs, buttonEnabledSetter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ChoiceItemViewHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,11 @@ import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -22,37 +20,34 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private ArrayList<String> selectedIDs=new ArrayList<>();
|
||||
private String accountID;
|
||||
private Account reportAccount;
|
||||
private Status reportStatus;
|
||||
private SparseIntArray knownDisplayItemHeights=new SparseIntArray();
|
||||
private HashSet<String> postsWithKnownNonHeaderHeights=new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -72,13 +67,15 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
accountID=getArguments().getString("account");
|
||||
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
|
||||
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
if(reportStatus!=null)
|
||||
if(reportStatus!=null){
|
||||
selectedIDs.add(reportStatus.id);
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
setTitle(R.string.report_title_post);
|
||||
}else{
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
}
|
||||
loadData();
|
||||
}
|
||||
|
||||
@@ -88,6 +85,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
for(Status s:result){
|
||||
s.sensitive=true;
|
||||
}
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -100,8 +100,10 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
selectedIDs.remove(id);
|
||||
else
|
||||
selectedIDs.add(id);
|
||||
list.invalidate();
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
CheckableHeaderStatusDisplayItem.Holder holder=findHolderOfType(id, CheckableHeaderStatusDisplayItem.Holder.class);
|
||||
if(holder!=null)
|
||||
holder.rebind();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,88 +112,27 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
btn.setOnClickListener(this::onButtonClick);
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Drawable uncheckedIcon=getResources().getDrawable(R.drawable.ic_fluent_radio_button_24_regular, getActivity().getTheme()).mutate();
|
||||
private Drawable checkedIcon=getResources().getDrawable(R.drawable.ic_fluent_checkmark_circle_24_filled, getActivity().getTheme()).mutate();
|
||||
{
|
||||
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary);
|
||||
checkedIcon.setTint(color);
|
||||
uncheckedIcon.setTint(color);
|
||||
checkedIcon.setBounds(0, 0, checkedIcon.getIntrinsicWidth(), checkedIcon.getIntrinsicHeight());
|
||||
uncheckedIcon.setBounds(0, 0, uncheckedIcon.getIntrinsicWidth(), uncheckedIcon.getIntrinsicHeight());
|
||||
}
|
||||
|
||||
@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.getAbsoluteAdapterPosition()==0)
|
||||
if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder)
|
||||
return;
|
||||
outRect.left=V.dp(40);
|
||||
if(holder instanceof AudioStatusDisplayItem.Holder){
|
||||
outRect.bottom=V.dp(16);
|
||||
}else if(holder instanceof LinkCardStatusDisplayItem.Holder){
|
||||
}else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
|
||||
outRect.bottom=V.dp(16);
|
||||
outRect.left+=V.dp(16);
|
||||
outRect.right=V.dp(16);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
// 1st pass: update item heights
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
if(holder instanceof StatusDisplayItem.Holder sdiHolder){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
String id=sdiHolder.getItemID();
|
||||
int height=tmpRect.height();
|
||||
if(!(holder instanceof HeaderStatusDisplayItem.Holder) && !(holder instanceof ReblogOrReplyLineStatusDisplayItem.Holder))
|
||||
postsWithKnownNonHeaderHeights.add(id);
|
||||
knownDisplayItemHeights.put(holder.getAbsoluteAdapterPosition(), height);
|
||||
}
|
||||
}
|
||||
// 2nd pass: draw checkboxes
|
||||
String lastPostID=null;
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sdiHolder){
|
||||
String postID=sdiHolder.getItemID();
|
||||
if(!postID.equals(lastPostID)){
|
||||
lastPostID=postID;
|
||||
if(!postsWithKnownNonHeaderHeights.contains(postID))
|
||||
continue; // We don't know full height of this post yet
|
||||
int postHeight=0;
|
||||
int heightOffset=0;
|
||||
for(int j=holder.getAbsoluteAdapterPosition()-getMainAdapterOffset();j<displayItems.size();j++){
|
||||
StatusDisplayItem item=displayItems.get(j);
|
||||
if(!item.parentID.equals(postID))
|
||||
break;
|
||||
postHeight+=knownDisplayItemHeights.get(j+getMainAdapterOffset());
|
||||
}
|
||||
for(int j=holder.getAbsoluteAdapterPosition()-getMainAdapterOffset()-1;j>=0;j--){
|
||||
StatusDisplayItem item=displayItems.get(j);
|
||||
if(!item.parentID.equals(postID))
|
||||
break;
|
||||
int itemHeight=knownDisplayItemHeights.get(j+getMainAdapterOffset());
|
||||
postHeight+=itemHeight;
|
||||
heightOffset+=itemHeight;
|
||||
}
|
||||
int y=Math.round(child.getY())+postHeight/2-heightOffset;
|
||||
Drawable check=selectedIDs.contains(postID) ? checkedIcon : uncheckedIcon;
|
||||
c.save();
|
||||
c.translate(V.dp(16), y-check.getIntrinsicHeight()/2f);
|
||||
check.draw(c);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ProgressBar topProgress=view.findViewById(R.id.top_progress);
|
||||
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 50 : 33);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,10 +145,8 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
View headerView=getActivity().getLayoutInflater().inflate(R.layout.item_list_header, list, false);
|
||||
TextView title=headerView.findViewById(R.id.title);
|
||||
TextView subtitle=headerView.findViewById(R.id.subtitle);
|
||||
TextView stepCounter=headerView.findViewById(R.id.step_counter);
|
||||
title.setText(R.string.report_choose_posts);
|
||||
subtitle.setText(R.string.report_choose_posts_subtitle);
|
||||
stepCounter.setText(getString(R.string.step_x_of_n, 2, 3));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
@@ -216,17 +155,14 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
tmpRect.offset(0, Math.round(child.getTranslationY()));
|
||||
float y=tmpRect.bottom-V.dp(.5f);
|
||||
paint.setAlpha(Math.round(255*child.getAlpha()));
|
||||
c.drawLine(V.dp(16), y, parent.getWidth()-V.dp(16), y, paint);
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
|
||||
if(reportStatus!=null)
|
||||
args.putBoolean("fromPost", true);
|
||||
if(v.getId()==R.id.btn_next){
|
||||
args.putStringArrayList("statusIDs", selectedIDs);
|
||||
}else{
|
||||
@@ -237,18 +173,13 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
}
|
||||
args.putStringArrayList("ruleIDs", getArguments().getStringArrayList("ruleIDs"));
|
||||
args.putString("reason", getArguments().getString("reason"));
|
||||
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
|
||||
Nav.go(getActivity(), ReportCommentFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -261,4 +192,32 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
protected boolean wantsOverlaySystemNavigation(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
|
||||
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
|
||||
View layout=h.getLayout();
|
||||
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
layout.setClipToOutline(true);
|
||||
View overlay=h.getSensitiveOverlay();
|
||||
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
overlay.setClipToOutline(true);
|
||||
}else if((Object)holder instanceof CheckableHeaderStatusDisplayItem.Holder h){
|
||||
h.setIsChecked(this::isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChecked(CheckableHeaderStatusDisplayItem.Holder holder){
|
||||
return selectedIDs.contains(holder.getItem().parentID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -16,6 +18,7 @@ import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.reports.SendReport;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -28,8 +31,6 @@ import java.util.ArrayList;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
private String accountID;
|
||||
@@ -37,6 +38,8 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private EditText commentEdit;
|
||||
private Switch forwardSwitch;
|
||||
private View forwardBtn;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -54,10 +57,12 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
accountID=getArguments().getString("account");
|
||||
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
if(getArguments().getBoolean("fromPost", false))
|
||||
setTitle(R.string.report_title_post);
|
||||
else
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
}
|
||||
|
||||
|
||||
@@ -67,16 +72,23 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
|
||||
TextView title=view.findViewById(R.id.title);
|
||||
TextView subtitle=view.findViewById(R.id.subtitle);
|
||||
TextView stepCounter=view.findViewById(R.id.step_counter);
|
||||
title.setText(R.string.report_comment_title);
|
||||
subtitle.setVisibility(View.GONE);
|
||||
stepCounter.setText(getString(R.string.step_x_of_n, 3, 3));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(this::onButtonClick);
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
commentEdit=view.findViewById(R.id.text);
|
||||
forwardSwitch=view.findViewById(R.id.forward_switch);
|
||||
forwardBtn=view.findViewById(R.id.forward_report);
|
||||
forwardBtn.setOnClickListener(v->forwardSwitch.toggle());
|
||||
String myDomain=AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
if(!TextUtils.isEmpty(reportAccount.getDomain()) && !myDomain.equalsIgnoreCase(reportAccount.getDomain())){
|
||||
TextView forwardTitle=view.findViewById(R.id.forward_title);
|
||||
forwardTitle.setText(getString(R.string.forward_report_to_server, reportAccount.getDomain()));
|
||||
}else{
|
||||
forwardBtn.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -84,25 +96,21 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
|
||||
|
||||
ProgressBar topProgress=view.findViewById(R.id.top_progress);
|
||||
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 75 : 66);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
|
||||
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
|
||||
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
|
||||
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), true)
|
||||
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardSwitch.isChecked())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
@@ -110,6 +118,8 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
|
||||
args.putString("reason", reason.name());
|
||||
args.putBoolean("fromPost", getArguments().getBoolean("fromPost", false));
|
||||
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
|
||||
Nav.go(getActivity(), ReportDoneFragment.class, args);
|
||||
buttonBar.postDelayed(()->E.post(new FinishReportFragmentsEvent(reportAccount.id)), 500);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.animation.PathInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
@@ -23,10 +25,13 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
@@ -38,6 +43,13 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private ReportReason reason;
|
||||
private TextView unfollowTitle;
|
||||
private TextView muteTitle;
|
||||
private TextView blockTitle;
|
||||
private View unfollowBtn;
|
||||
private View muteBtn;
|
||||
private View blockBtn;
|
||||
private Relationship relationship;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -48,14 +60,17 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
|
||||
accountID=getArguments().getString("account");
|
||||
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
|
||||
reason=ReportReason.valueOf(getArguments().getString("reason"));
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
relationship=Parcels.unwrap(getArguments().getParcelable("relationship"));
|
||||
if(getArguments().getBoolean("fromPost", false))
|
||||
setTitle(R.string.report_title_post);
|
||||
else
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_report_done, container, false);
|
||||
@@ -64,10 +79,18 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
TextView subtitle=view.findViewById(R.id.subtitle);
|
||||
if(reason==ReportReason.PERSONAL){
|
||||
title.setText(R.string.report_personal_title);
|
||||
subtitle.setText(R.string.report_personal_subtitle);
|
||||
if(relationship!=null && relationship.blocking){
|
||||
subtitle.setText(R.string.report_personal_already_blocked);
|
||||
}else{
|
||||
subtitle.setText(R.string.report_personal_subtitle);
|
||||
}
|
||||
}else{
|
||||
title.setText(R.string.report_sent_title);
|
||||
subtitle.setText(getString(R.string.report_sent_subtitle, '@'+reportAccount.acct));
|
||||
if(relationship!=null && relationship.blocking){
|
||||
subtitle.setText(R.string.report_sent_already_blocked);
|
||||
}else{
|
||||
subtitle.setText(getString(R.string.report_sent_subtitle, '@'+reportAccount.acct));
|
||||
}
|
||||
}
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
@@ -76,31 +99,65 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
btn.setText(R.string.done);
|
||||
|
||||
if(reason!=ReportReason.PERSONAL){
|
||||
View doneOverlay=view.findViewById(R.id.reported_overlay);
|
||||
doneOverlay.setOutlineProvider(OutlineProviders.roundedRect(7));
|
||||
TextView stamp=view.findViewById(R.id.reported_stamp);
|
||||
ImageView ava=view.findViewById(R.id.avatar);
|
||||
ava.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
ava.setClipToOutline(true);
|
||||
ViewImageLoader.load(ava, null, new UrlImageLoaderRequest(reportAccount.avatar));
|
||||
doneOverlay.setScaleX(1.5f);
|
||||
doneOverlay.setScaleY(1.5f);
|
||||
doneOverlay.setAlpha(0f);
|
||||
doneOverlay.animate().scaleX(1f).scaleY(1f).alpha(1f).rotation(8.79f).setDuration(300).setStartDelay(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
stamp.setAlpha(0f);
|
||||
stamp.setTranslationX(V.dp(148));
|
||||
stamp.setTranslationY(V.dp(-296));
|
||||
stamp.setScaleX(3.5f);
|
||||
stamp.setScaleY(3.5f);
|
||||
ObjectAnimator alpha=ObjectAnimator.ofFloat(stamp, View.ALPHA, 1f).setDuration(400);
|
||||
alpha.setInterpolator(new PathInterpolator(0.16f, 1, 0.3f, 1));
|
||||
alpha.start();
|
||||
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.TRANSLATION_X, 0f));
|
||||
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.TRANSLATION_Y, 0f));
|
||||
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.SCALE_X, 1f));
|
||||
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.SCALE_Y, 1f));
|
||||
|
||||
}else{
|
||||
view.findViewById(R.id.ava_reported).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
TextView unfollowTitle=view.findViewById(R.id.unfollow_title);
|
||||
TextView muteTitle=view.findViewById(R.id.mute_title);
|
||||
TextView blockTitle=view.findViewById(R.id.block_title);
|
||||
unfollowTitle=view.findViewById(R.id.unfollow_title);
|
||||
muteTitle=view.findViewById(R.id.mute_title);
|
||||
blockTitle=view.findViewById(R.id.block_title);
|
||||
unfollowBtn=view.findViewById(R.id.unfollow_btn);
|
||||
muteBtn=view.findViewById(R.id.mute_btn);
|
||||
blockBtn=view.findViewById(R.id.block_btn);
|
||||
|
||||
unfollowTitle.setText(getString(R.string.unfollow_user, '@'+reportAccount.acct));
|
||||
muteTitle.setText(getString(R.string.mute_user, '@'+reportAccount.acct));
|
||||
blockTitle.setText(getString(R.string.block_user, '@'+reportAccount.acct));
|
||||
setIconToButton(R.drawable.ic_person_remove_20px, unfollowTitle);
|
||||
setIconToButton(R.drawable.ic_block_20px, blockTitle);
|
||||
setIconToButton(R.drawable.ic_volume_off_20px, muteTitle);
|
||||
|
||||
view.findViewById(R.id.unfollow_btn).setOnClickListener(v->onUnfollowClick());
|
||||
view.findViewById(R.id.mute_btn).setOnClickListener(v->onMuteClick());
|
||||
view.findViewById(R.id.block_btn).setOnClickListener(v->onBlockClick());
|
||||
unfollowBtn.setOnClickListener(v->onUnfollowClick());
|
||||
muteBtn.setOnClickListener(v->onMuteClick());
|
||||
blockBtn.setOnClickListener(v->onBlockClick());
|
||||
|
||||
if(relationship!=null){
|
||||
if(relationship.blocking){
|
||||
muteBtn.setVisibility(View.GONE);
|
||||
view.findViewById(R.id.mute_explanation).setVisibility(View.GONE);
|
||||
unfollowBtn.setVisibility(View.GONE);
|
||||
view.findViewById(R.id.unfollow_explanation).setVisibility(View.GONE);
|
||||
blockBtn.setVisibility(View.GONE);
|
||||
view.findViewById(R.id.block_explanation).setVisibility(View.GONE);
|
||||
}else{
|
||||
if(relationship.muting){
|
||||
muteBtn.setVisibility(View.GONE);
|
||||
view.findViewById(R.id.mute_explanation).setVisibility(View.GONE);
|
||||
}
|
||||
if(!relationship.following){
|
||||
unfollowBtn.setVisibility(View.GONE);
|
||||
view.findViewById(R.id.unfollow_explanation).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -108,18 +165,11 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
@@ -131,8 +181,13 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
Nav.finish(ReportDoneFragment.this);
|
||||
E.post(new RemoveAccountPostsEvent(accountID, reportAccount.id, true));
|
||||
unfollowTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
|
||||
unfollowTitle.setText(getString(R.string.unfollowed_user, '@'+reportAccount.acct));
|
||||
setIconToButton(R.drawable.ic_check_24px, unfollowTitle);
|
||||
unfollowBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
|
||||
unfollowBtn.setClickable(false);
|
||||
unfollowBtn.setFocusable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -145,10 +200,50 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private void onMuteClick(){
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->{
|
||||
muteTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
|
||||
muteTitle.setText(getString(R.string.muted_user, '@'+reportAccount.acct));
|
||||
setIconToButton(R.drawable.ic_check_24px, muteTitle);
|
||||
muteBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
|
||||
muteBtn.setClickable(false);
|
||||
muteBtn.setFocusable(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void onBlockClick(){
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->{
|
||||
blockTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
|
||||
blockTitle.setText(getString(R.string.blocked_user, '@'+reportAccount.acct));
|
||||
setIconToButton(R.drawable.ic_check_24px, blockTitle);
|
||||
blockBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
|
||||
blockBtn.setClickable(false);
|
||||
blockBtn.setFocusable(false);
|
||||
if(unfollowBtn.isClickable())
|
||||
unfollowBtn.setEnabled(false);
|
||||
if(muteBtn.isClickable())
|
||||
muteBtn.setEnabled(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNavigationIconDrawableResource(){
|
||||
return R.drawable.ic_baseline_close_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsCustomNavigationIcon(){
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setIconToButton(@DrawableRes int icon, TextView button){
|
||||
Drawable d=getResources().getDrawable(icon, getActivity().getTheme()).mutate();
|
||||
d.setBounds(0, 0, V.dp(18), V.dp(18));
|
||||
d.setTintList(button.getTextColors());
|
||||
button.setCompoundDrawablesRelative(d, null, null, null);
|
||||
}
|
||||
|
||||
private void setupSpringAnimation(SpringAnimation anim){
|
||||
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY).setStiffness(500);
|
||||
anim.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,106 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ReportReason;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ReportReasonChoiceFragment extends StatusListFragment{
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
protected ArrayList<ChoiceItem> items=new ArrayList<>();
|
||||
protected boolean isMultipleChoice;
|
||||
protected ArrayList<String> selectedIDs=new ArrayList<>();
|
||||
protected Account reportAccount;
|
||||
protected Status reportStatus;
|
||||
protected ProgressBar progressBar;
|
||||
private Relationship relationship;
|
||||
|
||||
public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
|
||||
@Override
|
||||
protected Item getHeaderItem(){
|
||||
return new Item(reportStatus!=null ? getString(R.string.report_choose_reason) : getString(R.string.report_choose_reason_account, reportAccount.acct), getString(R.string.report_choose_reason_subtitle), null);
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
setListLayoutId(R.layout.fragment_content_report_posts);
|
||||
setLayout(R.layout.fragment_report_posts);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void populateItems(){
|
||||
items.add(new Item(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
|
||||
items.add(new Item(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
|
||||
public void onDestroy(){
|
||||
E.unregister(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
|
||||
accountID=getArguments().getString("account");
|
||||
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
|
||||
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
if(reportStatus!=null){
|
||||
Status hiddenStatus=reportStatus.clone();
|
||||
hiddenStatus.spoilerText=getString(R.string.post_hidden);
|
||||
onDataLoaded(Collections.singletonList(hiddenStatus));
|
||||
setTitle(R.string.report_title_post);
|
||||
}else{
|
||||
onDataLoaded(Collections.emptyList());
|
||||
setTitle(getString(R.string.report_title, reportAccount.acct));
|
||||
}
|
||||
relationship=Parcels.unwrap(getArguments().getParcelable("relationship"));
|
||||
if(relationship==null && reportStatus==null)
|
||||
loadRelationships(Collections.singleton(reportAccount.id));
|
||||
|
||||
items.add(new ChoiceItem(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
|
||||
items.add(new ChoiceItem(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
|
||||
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
if(inst!=null && inst.rules!=null && !inst.rules.isEmpty()){
|
||||
items.add(new Item(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
|
||||
items.add(new ChoiceItem(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
|
||||
}
|
||||
items.add(new Item(getString(R.string.report_reason_other), getString(R.string.report_reason_other_subtitle), ReportReason.OTHER.name()));
|
||||
items.add(new ChoiceItem(getString(R.string.report_reason_other), getString(R.string.report_reason_other_subtitle), ReportReason.OTHER.name()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onButtonClick(){
|
||||
ReportReason reason=ReportReason.valueOf(selectedIDs.get(0));
|
||||
Bundle args=new Bundle();
|
||||
@@ -38,6 +108,8 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
|
||||
args.putParcelable("status", Parcels.wrap(reportStatus));
|
||||
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
|
||||
args.putString("reason", reason.name());
|
||||
args.putBoolean("fromPost", reportStatus!=null);
|
||||
args.putParcelable("relationship", Parcels.wrap(relationship));
|
||||
switch(reason){
|
||||
case PERSONAL -> {
|
||||
Nav.go(getActivity(), ReportDoneFragment.class, args);
|
||||
@@ -48,14 +120,146 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getStepNumber(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
|
||||
if(ev.reportAccountID.equals(reportAccount.id))
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
|
||||
LayoutInflater inflater=getActivity().getLayoutInflater();
|
||||
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
|
||||
TextView title=headerView.findViewById(R.id.title);
|
||||
TextView subtitle=headerView.findViewById(R.id.subtitle);
|
||||
title.setText(reportStatus!=null ? getString(R.string.report_choose_reason) : getString(R.string.report_choose_reason_account, reportAccount.acct));
|
||||
subtitle.setText(getString(R.string.report_choose_reason_subtitle));
|
||||
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
adapter.addAdapter(new ChoiceItemsAdapter(getActivity(), isMultipleChoice, items, list, selectedIDs, btn::setEnabled));
|
||||
|
||||
return mergeAdapter=adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
progressBar=view.findViewById(R.id.top_progress);
|
||||
progressBar.setProgress(5);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
((UsableRecyclerView)list).setIncludeMarginsInItemHitbox(false);
|
||||
|
||||
if(reportStatus!=null){
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@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 LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
|
||||
outRect.left=V.dp(16);
|
||||
outRect.right=V.dp(16);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
{
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
int firstPos=list.getChildAdapterPosition(list.getChildAt(0));
|
||||
int lastPos=-1;
|
||||
for(int i=list.getChildCount()-1;i>=0;i--){
|
||||
lastPos=list.getChildAdapterPosition(list.getChildAt(i));
|
||||
if(lastPos!=-1)
|
||||
break;
|
||||
}
|
||||
int postStart=mergeAdapter.getPositionForAdapter(adapter);
|
||||
if(lastPos<postStart || firstPos>postStart+displayItems.size()){
|
||||
return;
|
||||
}
|
||||
|
||||
float top=V.dp(-12);
|
||||
float bottom=parent.getHeight()+V.dp(12);
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
int pos=parent.getChildAdapterPosition(child);
|
||||
if(pos==postStart)
|
||||
top=child.getY();
|
||||
if(pos==postStart+displayItems.size())
|
||||
bottom=child.getY()-V.dp(16);
|
||||
}
|
||||
|
||||
float off=paint.getStrokeWidth()/2f;
|
||||
c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), 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<?>){
|
||||
outRect.left=outRect.right=V.dp(16);
|
||||
}
|
||||
int index=holder.getAbsoluteAdapterPosition()-mergeAdapter.getPositionForAdapter(adapter);
|
||||
if(index==displayItems.size()){
|
||||
outRect.top=V.dp(32);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsOverlaySystemNavigation(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
|
||||
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
|
||||
View layout=h.getLayout();
|
||||
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
layout.setClipToOutline(true);
|
||||
View overlay=h.getSensitiveOverlay();
|
||||
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
overlay.setClipToOutline(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putRelationship(String id, Relationship rel){
|
||||
super.putRelationship(id, rel);
|
||||
if(id.equals(reportAccount.id))
|
||||
relationship=rel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
@@ -14,8 +15,8 @@ import me.grishka.appkit.Nav;
|
||||
|
||||
public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
|
||||
@Override
|
||||
protected Item getHeaderItem(){
|
||||
return new Item(getString(R.string.report_choose_rule), getString(R.string.report_choose_rule_subtitle), null);
|
||||
protected ChoiceItem getHeaderItem(){
|
||||
return new ChoiceItem(getString(R.string.report_choose_rule), getString(R.string.report_choose_rule_subtitle), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -24,7 +25,7 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
|
||||
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
if(inst!=null && inst.rules!=null){
|
||||
for(Instance.Rule rule:inst.rules){
|
||||
items.add(new Item(rule.text, null, rule.id));
|
||||
items.add(new ChoiceItem(rule.text, null, rule.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,17 +38,19 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
|
||||
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
|
||||
args.putString("reason", getArguments().getString("reason"));
|
||||
args.putStringArrayList("ruleIDs", selectedIDs);
|
||||
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
|
||||
Nav.go(getActivity(), ReportAddPostsChoiceFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getStepNumber(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
|
||||
if(ev.reportAccountID.equals(reportAccount.id))
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
progressBar.setProgress(25);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
|
||||
protected GenericListItemsAdapter<T> itemsAdapter;
|
||||
protected String accountID;
|
||||
|
||||
public BaseSettingsFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
public BaseSettingsFragment(int perPage){
|
||||
super(perPage);
|
||||
}
|
||||
|
||||
public BaseSettingsFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
accountID=getArguments().getString("account");
|
||||
setRefreshEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof SimpleListItemViewHolder ivh && ivh.getItem().dividerAfter));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
}
|
||||
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void toggleCheckableItem(CheckableListItem<T> item){
|
||||
item.toggle();
|
||||
rebindItem(item);
|
||||
}
|
||||
|
||||
protected void rebindItem(ListItem<T> item){
|
||||
if(list==null)
|
||||
return;
|
||||
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||
holder.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.filters.CreateFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.DeleteFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.UpdateFilter;
|
||||
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
|
||||
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
|
||||
private static final int WORDS_RESULT=370;
|
||||
private static final int CONTEXT_RESULT=651;
|
||||
|
||||
private Filter filter;
|
||||
private ListItem<Void> durationItem, wordsItem, contextItem;
|
||||
private CheckableListItem<Void> cwItem;
|
||||
private FloatingHintEditTextLayout titleEditLayout;
|
||||
private EditText titleEdit;
|
||||
|
||||
private Instant endsAt;
|
||||
private ArrayList<FilterKeyword> keywords=new ArrayList<>();
|
||||
private ArrayList<String> deletedWordIDs=new ArrayList<>();
|
||||
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
|
||||
private boolean dirty;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
filter=Parcels.unwrap(getArguments().getParcelable("filter"));
|
||||
setTitle(filter==null ? R.string.settings_add_filter : R.string.settings_edit_filter);
|
||||
onDataLoaded(List.of(
|
||||
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
|
||||
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
|
||||
contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
|
||||
cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem))
|
||||
));
|
||||
|
||||
if(filter!=null){
|
||||
endsAt=filter.expiresAt;
|
||||
keywords.addAll(filter.keywords);
|
||||
context=filter.context;
|
||||
data.add(new ListItem<>(R.string.settings_delete_filter, 0, this::onDeleteClick, R.attr.colorM3Error, false));
|
||||
}
|
||||
|
||||
updateDurationItem();
|
||||
updateWordsItem();
|
||||
updateContextItem();
|
||||
setHasOptionsMenu(true);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, list, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.settings_filter_title);
|
||||
titleEditLayout.updateHint();
|
||||
if(filter!=null)
|
||||
titleEdit.setText(filter.title);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void onDurationClick(){
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600
|
||||
};
|
||||
ArrayList<String> options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList<String>::new));
|
||||
options.add(0, getString(R.string.filter_duration_forever));
|
||||
options.add(getString(R.string.filter_duration_custom));
|
||||
Instant[] newEnd={null};
|
||||
boolean[] isCustom={false};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_filter_duration_title)
|
||||
.setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
|
||||
.setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
|
||||
AlertDialog a=(AlertDialog) dlg;
|
||||
if(item==options.size()-1){ // custom
|
||||
showCustomDurationAlert(isCustom[0] ? newEnd[0] : null, date->{
|
||||
if(date==null){
|
||||
a.getListView().setItemChecked(item, false);
|
||||
}else{
|
||||
isCustom[0]=true;
|
||||
newEnd[0]=date;
|
||||
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
isCustom[0]=false;
|
||||
if(item==0){
|
||||
newEnd[0]=null;
|
||||
}else{
|
||||
newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
|
||||
}
|
||||
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
if(!Objects.equals(endsAt, newEnd[0])){
|
||||
endsAt=newEnd[0];
|
||||
updateDurationItem();
|
||||
dirty=true;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void showCustomDurationAlert(Instant currentValue, Consumer<Instant> callback){
|
||||
DatePicker picker=new DatePicker(getActivity());
|
||||
picker.setMinDate(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond()*1000L);
|
||||
if(currentValue!=null){
|
||||
ZonedDateTime dt=currentValue.atZone(ZoneId.systemDefault());
|
||||
picker.updateDate(dt.getYear(), dt.getMonthValue()-1, dt.getDayOfMonth());
|
||||
}
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setView(picker)
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
((AlertDialog)dlg).setOnDismissListener(null);
|
||||
callback.accept(LocalDate.of(picker.getYear(), picker.getMonth()+1, picker.getDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.setOnDismissListener(dialog->callback.accept(null));
|
||||
}
|
||||
|
||||
private void onWordsClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
|
||||
}
|
||||
|
||||
private void onContextClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("context", context);
|
||||
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
|
||||
}
|
||||
|
||||
private void onDeleteClick(){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.settings_delete_filter_title, filter.title))
|
||||
.setMessage(R.string.settings_delete_filter_confirmation)
|
||||
.setPositiveButton(R.string.delete, (dlg, item)->deleteFilter())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
}
|
||||
|
||||
private void updateDurationItem(){
|
||||
if(endsAt==null){
|
||||
durationItem.subtitle=getString(R.string.filter_duration_forever);
|
||||
}else{
|
||||
durationItem.subtitle=getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false));
|
||||
}
|
||||
rebindItem(durationItem);
|
||||
}
|
||||
|
||||
private void updateWordsItem(){
|
||||
wordsItem.subtitle=getResources().getQuantityString(R.plurals.settings_x_muted_words, keywords.size(), keywords.size());
|
||||
rebindItem(wordsItem);
|
||||
}
|
||||
|
||||
private void updateContextItem(){
|
||||
List<String> values=context.stream().map(c->getString(c.getDisplayNameRes())).collect(Collectors.toList());
|
||||
contextItem.subtitle=switch(values.size()){
|
||||
case 0 -> null;
|
||||
case 1 -> values.get(0);
|
||||
case 2 -> getString(R.string.selection_2_options, values.get(0), values.get(1));
|
||||
case 3 -> getString(R.string.selection_3_options, values.get(0), values.get(1), values.get(2));
|
||||
default -> getString(R.string.selection_4_or_more, values.get(0), values.get(1), values.size()-2);
|
||||
};
|
||||
rebindItem(contextItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_edit_filter, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.save){
|
||||
saveFilter();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveFilter(){
|
||||
if(titleEdit.length()==0){
|
||||
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||
return;
|
||||
}
|
||||
MastodonAPIRequest<Filter> req;
|
||||
if(filter==null){
|
||||
req=new CreateFilter(titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords);
|
||||
}else{
|
||||
req=new UpdateFilter(filter.id, titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords, deletedWordIDs);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Filter result){
|
||||
E.post(new SettingsFilterCreatedOrUpdatedEvent(accountID, result));
|
||||
Nav.finish(EditFilterFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void deleteFilter(){
|
||||
new DeleteFilter(filter.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
E.post(new SettingsFilterDeletedEvent(accountID, filter.id));
|
||||
Nav.finish(EditFilterFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(success){
|
||||
if(reqCode==CONTEXT_RESULT){
|
||||
EnumSet<FilterContext> context=(EnumSet<FilterContext>) result.getSerializable("context");
|
||||
if(!context.equals(this.context)){
|
||||
this.context=context;
|
||||
dirty=true;
|
||||
updateContextItem();
|
||||
}
|
||||
}else if(reqCode==WORDS_RESULT){
|
||||
ArrayList<FilterKeyword> old=new ArrayList<>(keywords);
|
||||
keywords.clear();
|
||||
result.getParcelableArrayList("words").stream().map(p->(FilterKeyword)Parcels.unwrap(p)).forEach(keywords::add);
|
||||
if(!old.equals(keywords)){
|
||||
dirty=true;
|
||||
updateWordsItem();
|
||||
}
|
||||
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDirty(){
|
||||
return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title)) || (filter!=null && (filter.filterAction==FilterAction.WARN)!=cwItem.checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isDirty()){
|
||||
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
||||
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
|
||||
private EnumSet<FilterContext> context;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filter_context);
|
||||
context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
|
||||
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
|
||||
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
|
||||
item.parentObject=c;
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->toggleCheckableItem(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
context=EnumSet.noneOf(FilterContext.class);
|
||||
for(ListItem<FilterContext> item:data){
|
||||
if(((CheckableListItem<FilterContext>) item).checked)
|
||||
context.add(item.parentObject);
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putSerializable("context", context);
|
||||
setResult(true, args);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.IntEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.AlertDialog;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
|
||||
private ImageButton fab;
|
||||
private ActionMode actionMode;
|
||||
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
|
||||
private ArrayList<String> deletedItemIDs=new ArrayList<>();
|
||||
private MenuItem deleteItem;
|
||||
|
||||
public FilterWordsFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filter_muted_words);
|
||||
onDataLoaded(getArguments().getParcelableArrayList("words").stream().map(p->{
|
||||
FilterKeyword word=Parcels.unwrap(p);
|
||||
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onWordClick(ListItem<FilterKeyword> item){
|
||||
showAlertForWord(item.parentObject);
|
||||
}
|
||||
|
||||
private void onSelectionModeWordClick(CheckableListItem<FilterKeyword> item){
|
||||
if(selectedItems.remove(item)){
|
||||
item.checked=false;
|
||||
}else{
|
||||
item.checked=true;
|
||||
selectedItems.add(item);
|
||||
}
|
||||
rebindItem(item);
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
Bundle result=new Bundle();
|
||||
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
result.putStringArrayList("deleted", deletedItemIDs);
|
||||
setResult(true, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setImageResource(R.drawable.ic_add_24px);
|
||||
fab.setContentDescription(getString(R.string.add_muted_word));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
int fabInset=0;
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
fabInset=insets.getSystemWindowInsetBottom();
|
||||
}
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_filter_words, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
enterSelectionMode(item.getItemId()==R.id.select_all);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
if(actionMode!=null)
|
||||
return UiUtils.isDarkTheme();
|
||||
return super.wantsLightStatusBar();
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
showAlertForWord(null);
|
||||
}
|
||||
|
||||
private void showAlertForWord(FilterKeyword word){
|
||||
AlertDialog.Builder bldr=new M3AlertDialogBuilder(getActivity())
|
||||
.setHelpText(R.string.filter_add_word_help)
|
||||
.setTitle(word==null ? R.string.add_muted_word : R.string.edit_muted_word)
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
|
||||
FloatingHintEditTextLayout editWrap=(FloatingHintEditTextLayout) bldr.getContext().getSystemService(LayoutInflater.class).inflate(R.layout.floating_hint_edit_text, null);
|
||||
EditText edit=editWrap.findViewById(R.id.edit);
|
||||
edit.setHint(R.string.filter_word_or_phrase);
|
||||
edit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
|
||||
editWrap.updateHint();
|
||||
bldr.setView(editWrap)
|
||||
.setPositiveButton(word==null ? R.string.add : R.string.save, null);
|
||||
|
||||
if(word!=null){
|
||||
edit.setText(word.keyword);
|
||||
bldr.setNeutralButton(R.string.delete, null);
|
||||
}
|
||||
AlertDialog alert=bldr.show();
|
||||
if(word!=null){
|
||||
Button deleteBtn=alert.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
deleteBtn.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
deleteBtn.setOnClickListener(v->confirmDeleteWords(Collections.singletonList(word), alert::dismiss));
|
||||
}
|
||||
Button saveBtn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
saveBtn.setEnabled(false);
|
||||
saveBtn.setOnClickListener(v->{
|
||||
String input=edit.getText().toString();
|
||||
for(ListItem<FilterKeyword> item:data){
|
||||
if(item.parentObject.keyword.equalsIgnoreCase(input)){
|
||||
editWrap.setErrorState(getString(R.string.filter_word_already_in_list));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(word==null){
|
||||
FilterKeyword w=new FilterKeyword();
|
||||
w.wholeWord=true;
|
||||
w.keyword=input;
|
||||
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
data.add(item);
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}else{
|
||||
word.keyword=input;
|
||||
word.wholeWord=true;
|
||||
for(ListItem<FilterKeyword> item:data){
|
||||
if(item.parentObject==word){
|
||||
rebindItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert.dismiss();
|
||||
});
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(e->saveBtn.setEnabled(e.length()>0)));
|
||||
}
|
||||
|
||||
private void confirmDeleteWords(List<FilterKeyword> words, Runnable onConfirmed){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(words.size()==1 ? getString(R.string.settings_delete_filter_word, words.get(0).keyword) : getResources().getQuantityString(R.plurals.settings_delete_x_filter_words, words.size(), words.size()))
|
||||
// .setMessage(R.string.settings_delete_filter_confirmation)
|
||||
.setPositiveButton(R.string.delete, (dlg, item)->{
|
||||
if(onConfirmed!=null)
|
||||
onConfirmed.run();
|
||||
removeWords(words);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
}
|
||||
|
||||
private void removeWords(List<FilterKeyword> words){
|
||||
ArrayList<Integer> indexes=new ArrayList<>();
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(words.contains(data.get(i).parentObject)){
|
||||
indexes.add(0, i);
|
||||
}
|
||||
}
|
||||
for(int index:indexes){
|
||||
data.remove(index);
|
||||
itemsAdapter.notifyItemRemoved(index);
|
||||
}
|
||||
for(FilterKeyword w:words){
|
||||
if(w.id!=null)
|
||||
deletedItemIDs.add(w.id);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterSelectionMode(boolean selectAll){
|
||||
if(actionMode!=null)
|
||||
return;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
|
||||
actionMode=getActivity().startActionMode(new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", elevationOnScrollListener.getCurrentStatusBarColor(), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
|
||||
for(int i=0;i<menu.size();i++){
|
||||
Drawable icon=menu.getItem(i).getIcon().mutate();
|
||||
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnPrimary));
|
||||
menu.getItem(i).setIcon(icon);
|
||||
}
|
||||
deleteItem=menu.findItem(R.id.delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
if(item.getItemId()==R.id.delete){
|
||||
confirmDeleteWords(selectedItems.stream().map(i->i.parentObject).collect(Collectors.toList()), ()->leaveSelectionMode(false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
leaveSelectionMode(true);
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
getActivity().getWindow().setStatusBarColor(0);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
|
||||
}
|
||||
});
|
||||
|
||||
selectedItems.clear();
|
||||
for(int i=0;i<data.size();i++){
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onSelectionModeWordClick(newItem);
|
||||
newItem.parentObject=item.parentObject;
|
||||
if(selectAll)
|
||||
selectedItems.add(newItem);
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
private void leaveSelectionMode(boolean fromActionMode){
|
||||
if(actionMode==null)
|
||||
return;
|
||||
ActionMode actionMode=this.actionMode;
|
||||
this.actionMode=null;
|
||||
if(!fromActionMode)
|
||||
actionMode.finish();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
selectedItems.clear();
|
||||
|
||||
for(int i=0;i<data.size();i++){
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onWordClick(newItem);
|
||||
newItem.parentObject=item.parentObject;
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedItems.size(), selectedItems.size()));
|
||||
deleteItem.setEnabled(!selectedItems.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> mediaCacheItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(getString(R.string.about_app, getString(R.string.app_name)));
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_even_more, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
|
||||
new ListItem<>(R.string.settings_contribute, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
|
||||
));
|
||||
|
||||
updateMediaCacheItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
|
||||
TextView versionInfo=new TextView(getActivity());
|
||||
versionInfo.setSingleLine();
|
||||
versionInfo.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(32)));
|
||||
versionInfo.setTextAppearance(R.style.m3_label_medium);
|
||||
versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
|
||||
versionInfo.setGravity(Gravity.CENTER);
|
||||
versionInfo.setText(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onClearMediaCacheClick(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
activity.runOnUiThread(()->{
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
updateMediaCacheItem();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMediaCacheItem(){
|
||||
long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
|
||||
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
|
||||
mediaCacheItem.isEnabled=size>0;
|
||||
rebindItem(mediaCacheItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> languageItem;
|
||||
private CheckableListItem<Void> altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
|
||||
private Locale postLanguage;
|
||||
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_behavior);
|
||||
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences!=null && s.preferences.postingDefaultLanguage!=null){
|
||||
postLanguage=Locale.forLanguageTag(s.preferences.postingDefaultLanguage);
|
||||
}
|
||||
|
||||
onDataLoaded(List.of(
|
||||
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
|
||||
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, ()->toggleCheckableItem(altTextItem)),
|
||||
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, ()->toggleCheckableItem(playGifsItem)),
|
||||
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, ()->toggleCheckableItem(customTabsItem)),
|
||||
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, ()->toggleCheckableItem(confirmUnfollowItem)),
|
||||
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, ()->toggleCheckableItem(confirmBoostItem)),
|
||||
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, ()->toggleCheckableItem(confirmDeleteItem))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onDefaultLanguageClick(){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
.setView(vc.getView())
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
|
||||
if(!opt.locale.equals(postLanguage)){
|
||||
newPostLanguage=opt;
|
||||
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
|
||||
rebindItem(languageItem);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
GlobalUserPreferences.playGifs=playGifsItem.checked;
|
||||
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
|
||||
GlobalUserPreferences.altTextReminders=altTextItem.checked;
|
||||
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
|
||||
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
|
||||
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
|
||||
GlobalUserPreferences.save();
|
||||
if(newPostLanguage!=null){
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences==null)
|
||||
s.preferences=new Preferences();
|
||||
s.preferences.postingDefaultLanguage=newPostLanguage.locale.toLanguageTag();
|
||||
s.savePreferencesLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle("Debug settings");
|
||||
ListItem<Void> selfUpdateItem, resetUpdateItem;
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
|
||||
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
selfUpdateItem.subtitle="Self-updater is unavailable in this build flavor";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onTestEmailConfirmClick(){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}
|
||||
|
||||
private void onForceSelfUpdateClick(){
|
||||
GithubSelfUpdater.forceUpdate=true;
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetUpdaterClick(){
|
||||
GithubSelfUpdater.getInstance().reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetDiscoverBannersClick(){
|
||||
DiscoverInfoBannerHelper.reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void restartUI(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
private ImageView themeTransitionWindowView;
|
||||
private ListItem<Void> themeItem;
|
||||
private CheckableListItem<Void> showCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_display);
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
onDataLoaded(List.of(
|
||||
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
|
||||
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, ()->toggleCheckableItem(showCWsItem)),
|
||||
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, ()->toggleCheckableItem(hideSensitiveMediaItem)),
|
||||
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, ()->toggleCheckableItem(interactionCountsItem)),
|
||||
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, ()->toggleCheckableItem(emojiInNamesItem))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
if(themeTransitionWindowView!=null){
|
||||
// Activity has finished recreating. Remove the overlay.
|
||||
activity.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
|
||||
themeTransitionWindowView=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
lp.showCWs=showCWsItem.checked;
|
||||
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
|
||||
lp.showInteractionCounts=interactionCountsItem.checked;
|
||||
lp.customEmojiInNames=emojiInNamesItem.checked;
|
||||
lp.save();
|
||||
E.post(new StatusDisplaySettingsChangedEvent(accountID));
|
||||
}
|
||||
|
||||
private int getAppearanceValue(){
|
||||
return switch(GlobalUserPreferences.theme){
|
||||
case AUTO -> R.string.theme_auto;
|
||||
case LIGHT -> R.string.theme_light;
|
||||
case DARK -> R.string.theme_dark;
|
||||
};
|
||||
}
|
||||
|
||||
private void onAppearanceClick(){
|
||||
int selected=switch(GlobalUserPreferences.theme){
|
||||
case LIGHT -> 0;
|
||||
case DARK -> 1;
|
||||
case AUTO -> 2;
|
||||
};
|
||||
int[] newSelected={selected};
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_theme)
|
||||
.setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
|
||||
selected, (dlg, item)->newSelected[0]=item)
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
|
||||
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
|
||||
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
|
||||
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
|
||||
};
|
||||
if(pref!=GlobalUserPreferences.theme){
|
||||
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
|
||||
GlobalUserPreferences.theme=pref;
|
||||
GlobalUserPreferences.save();
|
||||
themeItem.subtitleRes=getAppearanceValue();
|
||||
rebindItem(themeItem);
|
||||
maybeApplyNewThemeRightNow(prev);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prev){
|
||||
boolean isCurrentDark=prev==GlobalUserPreferences.ThemePreference.DARK ||
|
||||
(prev==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
|
||||
boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
|
||||
(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
|
||||
if(isCurrentDark!=isNewDark){
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
private void restartActivityToApplyNewTheme(){
|
||||
// Calling activity.recreate() causes a black screen for like half a second.
|
||||
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
|
||||
// As a bonus, we can fade it out to make it even smoother.
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N && Build.VERSION.SDK_INT<Build.VERSION_CODES.S){
|
||||
View activityDecorView=getActivity().getWindow().getDecorView();
|
||||
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
activityDecorView.draw(new Canvas(bitmap));
|
||||
themeTransitionWindowView=new ImageView(MastodonApp.context);
|
||||
themeTransitionWindowView.setImageBitmap(bitmap);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
|
||||
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
|
||||
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
|
||||
lp.token=getActivity().getWindow().getAttributes().token;
|
||||
lp.windowAnimations=R.style.window_fade_out;
|
||||
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
|
||||
}
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.filters.GetFilters;
|
||||
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
|
||||
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filters);
|
||||
loadData();
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetFilters()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
|
||||
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
|
||||
)));
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onFilterClick(ListItem<Filter> filter){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("filter", Parcels.wrap(filter.parentObject));
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAddFilterClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private ListItem<Filter> makeListItem(Filter f){
|
||||
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f);
|
||||
item.onClick=()->onFilterClick(item);
|
||||
item.isEnabled=true;
|
||||
return item;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFilterDeleted(SettingsFilterDeletedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).parentObject.id.equals(ev.filterID)){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFilterCreatedOrUpdated(SettingsFilterCreatedOrUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(ListItem<Filter> item:data){
|
||||
if(item.parentObject.id.equals(ev.filter.id)){
|
||||
item.parentObject=ev.filter;
|
||||
item.title=ev.filter.title;
|
||||
item.subtitle=getString(ev.filter.isActive() ? R.string.filter_active : R.string.filter_inactive);
|
||||
rebindItem(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.add(makeListItem(ev.filter));
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
private boolean loggedOut;
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private Button updateButton1, updateButton2;
|
||||
private TextView updateText;
|
||||
private Runnable updateDownloadProgressUpdater=new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
updateButton1.setText(getString(R.string.downloading_update, Math.round(GithubSelfUpdater.getInstance().getDownloadProgress()*100f)));
|
||||
list.postDelayed(this, 250);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings);
|
||||
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
|
||||
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
|
||||
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
|
||||
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
|
||||
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
|
||||
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
|
||||
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
|
||||
));
|
||||
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
}
|
||||
|
||||
AccountSessionManager.get(accountID).reloadPreferences(null);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!loggedOut)
|
||||
AccountSessionManager.get(accountID).savePreferencesIfPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
updateText=banner.findViewById(R.id.text);
|
||||
TextView bannerTitle=banner.findViewById(R.id.title);
|
||||
ImageView bannerIcon=banner.findViewById(R.id.icon);
|
||||
updateButton1=banner.findViewById(R.id.button);
|
||||
updateButton2=banner.findViewById(R.id.button2);
|
||||
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
|
||||
bannerAdapter.setVisible(false);
|
||||
updateButton1.setOnClickListener(this::onUpdateButtonClick);
|
||||
updateButton2.setOnClickListener(this::onUpdateButtonClick);
|
||||
|
||||
bannerTitle.setText(R.string.app_update_ready);
|
||||
bannerIcon.setImageResource(R.drawable.ic_apk_install_24px);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(bannerAdapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
updateUpdateBanner();
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle makeFragmentArgs(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onBehaviorClick(){
|
||||
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onDisplayClick(){
|
||||
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onServerClick(){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onAboutClick(){
|
||||
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onLogOutClick(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
|
||||
loggedOut=true;
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateBanner();
|
||||
}
|
||||
|
||||
private void updateUpdateBanner(){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.NO_UPDATE || state==GithubSelfUpdater.UpdateState.CHECKING){
|
||||
bannerAdapter.setVisible(false);
|
||||
}else{
|
||||
bannerAdapter.setVisible(true);
|
||||
updateText.setText(getString(R.string.app_update_version, GithubSelfUpdater.getInstance().getUpdateInfo().version));
|
||||
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
|
||||
updateButton2.setVisibility(View.GONE);
|
||||
updateButton1.setEnabled(true);
|
||||
updateButton1.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), GithubSelfUpdater.getInstance().getUpdateInfo().size, true)));
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
updateButton2.setVisibility(View.VISIBLE);
|
||||
updateButton2.setText(R.string.cancel);
|
||||
updateButton1.setEnabled(false);
|
||||
list.removeCallbacks(updateDownloadProgressUpdater);
|
||||
updateDownloadProgressUpdater.run();
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
updateButton2.setVisibility(View.GONE);
|
||||
updateButton1.setEnabled(true);
|
||||
updateButton1.setText(R.string.install_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onUpdateButtonClick(View v){
|
||||
if(v.getId()==R.id.button){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
|
||||
GithubSelfUpdater.getInstance().downloadUpdate();
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
GithubSelfUpdater.getInstance().installUpdate(getActivity());
|
||||
}
|
||||
}else if(v.getId()==R.id.button2){
|
||||
GithubSelfUpdater.getInstance().cancelDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
private PushSubscription pushSubscription;
|
||||
private CheckableListItem<Void> pauseItem;
|
||||
private ListItem<Void> policyItem;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private ImageView bannerIcon;
|
||||
private TextView bannerText;
|
||||
private Button bannerButton;
|
||||
|
||||
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
|
||||
private List<CheckableListItem<Void>> typeItems;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean notificationsAllowed=true;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_notifications);
|
||||
|
||||
getPushSubscription();
|
||||
|
||||
onDataLoaded(List.of(
|
||||
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, ()->onPauseNotificationsClick(false)),
|
||||
policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_group_24px, this::onNotificationsPolicyClick),
|
||||
|
||||
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, ()->toggleCheckableItem(mentionsItem)),
|
||||
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, ()->toggleCheckableItem(boostsItem)),
|
||||
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, ()->toggleCheckableItem(favoritesItem)),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, ()->toggleCheckableItem(followersItem)),
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, ()->toggleCheckableItem(pollsItem))
|
||||
));
|
||||
|
||||
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
|
||||
pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
|
||||
updatePolicyItem(null);
|
||||
updatePauseItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
PushSubscription ps=getPushSubscription();
|
||||
needUpdateNotificationSettings|=mentionsItem.checked!=ps.alerts.mention
|
||||
|| boostsItem.checked!=ps.alerts.reblog
|
||||
|| favoritesItem.checked!=ps.alerts.favourite
|
||||
|| followersItem.checked!=ps.alerts.follow
|
||||
|| pollsItem.checked!=ps.alerts.poll;
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
ps.alerts.mention=mentionsItem.checked;
|
||||
ps.alerts.reblog=boostsItem.checked;
|
||||
ps.alerts.favourite=favoritesItem.checked;
|
||||
ps.alerts.follow=followersItem.checked;
|
||||
ps.alerts.poll=pollsItem.checked;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
boolean allowed=areNotificationsAllowed();
|
||||
PushSubscription ps=getPushSubscription();
|
||||
if(allowed!=notificationsAllowed){
|
||||
notificationsAllowed=allowed;
|
||||
updateBanner();
|
||||
pauseItem.isEnabled=allowed;
|
||||
policyItem.isEnabled=allowed;
|
||||
rebindItem(pauseItem);
|
||||
rebindItem(policyItem);
|
||||
for(CheckableListItem<Void> item:typeItems){
|
||||
item.isEnabled=allowed && ps.policy!=PushSubscription.Policy.NONE;
|
||||
rebindItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
bannerText=banner.findViewById(R.id.text);
|
||||
bannerIcon=banner.findViewById(R.id.icon);
|
||||
bannerButton=banner.findViewById(R.id.button);
|
||||
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
|
||||
bannerAdapter.setVisible(false);
|
||||
banner.findViewById(R.id.button2).setVisibility(View.GONE);
|
||||
banner.findViewById(R.id.title).setVisibility(View.GONE);
|
||||
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(bannerAdapter);
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return mergeAdapter.getPositionForAdapter(itemsAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
private boolean areNotificationsAllowed(){
|
||||
return Build.VERSION.SDK_INT<Build.VERSION_CODES.N || getActivity().getSystemService(NotificationManager.class).areNotificationsEnabled();
|
||||
}
|
||||
|
||||
private PushSubscription getPushSubscription(){
|
||||
if(pushSubscription!=null)
|
||||
return pushSubscription;
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(session.pushSubscription==null){
|
||||
pushSubscription=new PushSubscription();
|
||||
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
|
||||
}else{
|
||||
pushSubscription=session.pushSubscription.clone();
|
||||
}
|
||||
return pushSubscription;
|
||||
}
|
||||
|
||||
private String getPauseItemSubtitle(){
|
||||
return getString(R.string.pause_notifications_off);
|
||||
}
|
||||
|
||||
private void resumePausedNotifications(){
|
||||
AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(0);
|
||||
updatePauseItem();
|
||||
}
|
||||
|
||||
private void openSystemNotificationSettings(){
|
||||
Intent intent;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
|
||||
intent=new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null));
|
||||
}else{
|
||||
intent=new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getActivity().getPackageName());
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void onPauseNotificationsClick(boolean fromSwitch){
|
||||
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(time>System.currentTimeMillis() && fromSwitch){
|
||||
resumePausedNotifications();
|
||||
return;
|
||||
}
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600
|
||||
};
|
||||
int[] selectedOption={0};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.pause_all_notifications_title)
|
||||
.setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
|
||||
.setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
|
||||
if(selectedOption[0]==0){
|
||||
((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
selectedOption[0]=durationOptions[item];
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.setOnDismissListener(dialog->updatePauseItem());
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyClick(){
|
||||
String[] items=Stream.of(
|
||||
R.string.notifications_policy_anyone,
|
||||
R.string.notifications_policy_followed,
|
||||
R.string.notifications_policy_follower,
|
||||
R.string.notifications_policy_no_one
|
||||
).map(this::getString).toArray(String[]::new);
|
||||
int[] selectedItem={getPushSubscription().policy.ordinal()};
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_notifications_policy)
|
||||
.setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
PushSubscription.Policy prevValue=getPushSubscription().policy;
|
||||
PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
|
||||
if(prevValue==newValue)
|
||||
return;
|
||||
getPushSubscription().policy=newValue;
|
||||
updatePolicyItem(prevValue);
|
||||
needUpdateNotificationSettings=true;
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updatePolicyItem(PushSubscription.Policy prevValue){
|
||||
policyItem.subtitleRes=switch(getPushSubscription().policy){
|
||||
case ALL -> R.string.notifications_policy_anyone;
|
||||
case FOLLOWED -> R.string.notifications_policy_followed;
|
||||
case FOLLOWER -> R.string.notifications_policy_follower;
|
||||
case NONE -> R.string.notifications_policy_no_one;
|
||||
};
|
||||
rebindItem(policyItem);
|
||||
if(pushSubscription.policy==PushSubscription.Policy.NONE || prevValue==PushSubscription.Policy.NONE){
|
||||
for(CheckableListItem<Void> item:typeItems){
|
||||
item.checked=item.isEnabled=prevValue==PushSubscription.Policy.NONE;
|
||||
rebindItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePauseItem(){
|
||||
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(time<System.currentTimeMillis()){
|
||||
pauseItem.subtitle=getString(R.string.pause_notifications_off);
|
||||
pauseItem.checked=false;
|
||||
}else{
|
||||
pauseItem.subtitle=getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false));
|
||||
pauseItem.checked=true;
|
||||
}
|
||||
rebindItem(pauseItem);
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
private void updateBanner(){
|
||||
if(bannerAdapter==null)
|
||||
return;
|
||||
long pauseTime=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(!areNotificationsAllowed()){
|
||||
bannerAdapter.setVisible(true);
|
||||
bannerIcon.setImageResource(R.drawable.ic_app_badging_24px);
|
||||
bannerText.setText(R.string.notifications_disabled_in_system);
|
||||
bannerButton.setText(R.string.open_system_notification_settings);
|
||||
bannerButton.setOnClickListener(v->openSystemNotificationSettings());
|
||||
}else if(pauseTime>System.currentTimeMillis()){
|
||||
bannerAdapter.setVisible(true);
|
||||
bannerIcon.setImageResource(R.drawable.ic_notifications_paused_24px);
|
||||
bannerText.setText(getString(R.string.pause_notifications_banner, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(pauseTime), false)));
|
||||
bannerButton.setText(R.string.resume_notifications_now);
|
||||
bannerButton.setOnClickListener(v->resumePausedNotifications());
|
||||
}else{
|
||||
bannerAdapter.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user