diff --git a/README.md b/README.md index be7fc1a3d..178a56d06 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Translation status](https://hosted.weblate.org/widgets/fedilab/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/fedilab/)    [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -# Fedilab is a multi-accounts client for Mastodon, Pleroma, Peertube, GNU Social, Friendica and Pixelfed +# Fedilab is a multi-accounts client for Mastodon, Pleroma, Friendica and Pixelfed ## Donate @@ -16,7 +16,7 @@ [WIKI](https://fedilab.app/wiki/home/) -[Release notes](https://framagit.org/tom79/fedilab/tags) +[Release notes](https://codeberg.org/tom79/Fedilab/tags) Lead developer: [toot.fedilab.app/@apps](https://toot.fedilab.app/@apps) diff --git a/app/build.gradle b/app/build.gradle index dc9ea8795..4899929e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { minSdk 21 targetSdk 31 - versionCode 396 - versionName "3.0.6" + versionCode 399 + versionName "3.0.9" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } flavorDimensions "default" @@ -85,11 +85,9 @@ dependencies { implementation 'com.github.GrenderG:Toasty:1.5.2' implementation 'org.framagit.tom79:SparkButton:1.0.13' implementation "com.github.bumptech.glide:glide:4.12.0" + implementation "com.github.bumptech.glide:okhttp3-integration:4.12.0" + implementation 'com.github.mergehez:ArgPlayer:v3.1' - implementation ("com.github.bumptech.glide:recyclerview-integration:4.12.0") { - // Excludes the support library because it's already included by Glide. - transitive = false - } implementation project(path: ':mytransl') implementation project(path: ':ratethisapp') @@ -99,8 +97,7 @@ dependencies { implementation project(path: ':cropper') annotationProcessor "com.github.bumptech.glide:compiler:4.12.0" implementation 'jp.wasabeef:glide-transformations:4.3.0' - implementation 'com.github.penfeizhou.android.animation:apng:2.22.0' - implementation 'com.github.penfeizhou.android.animation:gif:2.22.0' + implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.22.0' implementation 'com.google.android.exoplayer:exoplayer:2.16.1' implementation "androidx.viewpager2:viewpager2:1.0.0" implementation 'com.github.piasy:rxandroidaudio:1.7.0' diff --git a/app/src/main/assets/languages/iso_639_1.json b/app/src/main/assets/languages/iso_639_1.json new file mode 100644 index 000000000..00199b74c --- /dev/null +++ b/app/src/main/assets/languages/iso_639_1.json @@ -0,0 +1,730 @@ +[ + { + "code": "aa", + "language": "Afaraf" + }, + { + "code": "ab", + "language": "аҧсуа бызшәа" + }, + { + "code": "ae", + "language": "avesta" + }, + { + "code": "af", + "language": "Afrikaans" + }, + { + "code": "ak", + "language": "Akan" + }, + { + "code": "am", + "language": "አማርኛ" + }, + { + "code": "an", + "language": "aragonés" + }, + { + "code": "ar", + "language": "اللغة العربية" + }, + { + "code": "as", + "language": "অসমীয়া" + }, + { + "code": "av", + "language": "авар мацӀ" + }, + { + "code": "ay", + "language": "aymar aru" + }, + { + "code": "az", + "language": "azərbaycan dili" + }, + { + "code": "ba", + "language": "башҡорт теле" + }, + { + "code": "be", + "language": "беларуская мова" + }, + { + "code": "bg", + "language": "български език" + }, + { + "code": "bh", + "language": "भोजपुरी" + }, + { + "code": "bi", + "language": "Bislama" + }, + { + "code": "bm", + "language": "bamanankan" + }, + { + "code": "bn", + "language": "বাংলা" + }, + { + "code": "bo", + "language": "བོད་ཡིག" + }, + { + "code": "br", + "language": "brezhoneg" + }, + { + "code": "bs", + "language": "bosanski jezik" + }, + { + "code": "ca", + "language": "Català" + }, + { + "code": "ce", + "language": "нохчийн мотт" + }, + { + "code": "ch", + "language": "Chamoru" + }, + { + "code": "co", + "language": "corsu" + }, + { + "code": "cr", + "language": "ᓀᐦᐃᔭᐍᐏᐣ" + }, + { + "code": "cs", + "language": "čeština" + }, + { + "code": "cu", + "language": "ѩзыкъ словѣньскъ" + }, + { + "code": "cv", + "language": "чӑваш чӗлхи" + }, + { + "code": "cy", + "language": "Cymraeg" + }, + { + "code": "da", + "language": "dansk" + }, + { + "code": "de", + "language": "Deutsch" + }, + { + "code": "dv", + "language": "Dhivehi" + }, + { + "code": "dz", + "language": "རྫོང་ཁ" + }, + { + "code": "ee", + "language": "Eʋegbe" + }, + { + "code": "el", + "language": "Ελληνικά" + }, + { + "code": "en", + "language": "English" + }, + { + "code": "eo", + "language": "Esperanto" + }, + { + "code": "es", + "language": "Español" + }, + { + "code": "et", + "language": "eesti" + }, + { + "code": "eu", + "language": "euskara" + }, + { + "code": "fa", + "language": "فارسی" + }, + { + "code": "ff", + "language": "Fulfulde" + }, + { + "code": "fi", + "language": "suomi" + }, + { + "code": "fj", + "language": "Vakaviti" + }, + { + "code": "fo", + "language": "føroyskt" + }, + { + "code": "fr", + "language": "Français" + }, + { + "code": "fy", + "language": "Frysk" + }, + { + "code": "ga", + "language": "Gaeilge" + }, + { + "code": "gd", + "language": "Gàidhlig" + }, + { + "code": "gl", + "language": "galego" + }, + { + "code": "gu", + "language": "ગુજરાતી" + }, + { + "code": "gv", + "language": "Gaelg" + }, + { + "code": "ha", + "language": "هَوُسَ" + }, + { + "code": "he", + "language": "עברית" + }, + { + "code": "hi", + "language": "हिन्दी" + }, + { + "code": "ho", + "language": "Hiri Motu" + }, + { + "code": "hr", + "language": "Hrvatski" + }, + { + "code": "ht", + "language": "Kreyòl ayisyen" + }, + { + "code": "hu", + "language": "magyar" + }, + { + "code": "hy", + "language": "Հայերեն" + }, + { + "code": "hz", + "language": "Otjiherero" + }, + { + "code": "ia", + "language": "Interlingua" + }, + { + "code": "id", + "language": "Bahasa Indonesia" + }, + { + "code": "ie", + "language": "Interlingue" + }, + { + "code": "ig", + "language": "Asụsụ Igbo" + }, + { + "code": "ii", + "language": "ꆈꌠ꒿ Nuosuhxop" + }, + { + "code": "ik", + "language": "Iñupiaq" + }, + { + "code": "io", + "language": "Ido" + }, + { + "code": "is", + "language": "Íslenska" + }, + { + "code": "it", + "language": "Italiano" + }, + { + "code": "iu", + "language": "ᐃᓄᒃᑎᑐᑦ" + }, + { + "code": "ja", + "language": "日本語" + }, + { + "code": "jv", + "language": "basa Jawa" + }, + { + "code": "ka", + "language": "ქართული" + }, + { + "code": "kg", + "language": "Kikongo" + }, + { + "code": "ki", + "language": "Gĩkũyũ" + }, + { + "code": "kj", + "language": "Kuanyama" + }, + { + "code": "kk", + "language": "қазақ тілі" + }, + { + "code": "kl", + "language": "kalaallisut" + }, + { + "code": "km", + "language": "ខេមរភាសា" + }, + { + "code": "kn", + "language": "ಕನ್ನಡ" + }, + { + "code": "ko", + "language": "한국어" + }, + { + "code": "kr", + "language": "Kanuri" + }, + { + "code": "ks", + "language": "कश्मीरी" + }, + { + "code": "ku", + "language": "Kurmancî" + }, + { + "code": "kv", + "language": "коми кыв" + }, + { + "code": "kw", + "language": "Kernewek" + }, + { + "code": "ky", + "language": "Кыргызча" + }, + { + "code": "la", + "language": "latine" + }, + { + "code": "lb", + "language": "Lëtzebuergesch" + }, + { + "code": "lg", + "language": "Luganda" + }, + { + "code": "li", + "language": "Limburgs" + }, + { + "code": "ln", + "language": "Lingála" + }, + { + "code": "lo", + "language": "ພາສາ" + }, + { + "code": "lt", + "language": "lietuvių kalba" + }, + { + "code": "lu", + "language": "Tshiluba" + }, + { + "code": "lv", + "language": "latviešu valoda" + }, + { + "code": "mg", + "language": "fiteny malagasy" + }, + { + "code": "mh", + "language": "Kajin M̧ajeļ" + }, + { + "code": "mi", + "language": "te reo Māori" + }, + { + "code": "mk", + "language": "македонски јазик" + }, + { + "code": "ml", + "language": "മലയാളം" + }, + { + "code": "mn", + "language": "Монгол хэл" + }, + { + "code": "mr", + "language": "मराठी" + }, + { + "code": "ms", + "language": "Bahasa Melayu" + }, + { + "code": "mt", + "language": "Malti" + }, + { + "code": "my", + "language": "ဗမာစာ" + }, + { + "code": "na", + "language": "Ekakairũ Naoero" + }, + { + "code": "nb", + "language": "Norsk bokmål" + }, + { + "code": "nd", + "language": "isiNdebele" + }, + { + "code": "ne", + "language": "नेपाली" + }, + { + "code": "ng", + "language": "Owambo" + }, + { + "code": "nl", + "language": "Nederlands" + }, + { + "code": "nn", + "language": "Norsk Nynorsk" + }, + { + "code": "no", + "language": "Norsk" + }, + { + "code": "nr", + "language": "isiNdebele" + }, + { + "code": "nv", + "language": "Diné bizaad" + }, + { + "code": "ny", + "language": "chiCheŵa" + }, + { + "code": "oc", + "language": "occitan" + }, + { + "code": "oj", + "language": "ᐊᓂᔑᓈᐯᒧᐎᓐ" + }, + { + "code": "om", + "language": "Afaan Oromoo" + }, + { + "code": "or", + "language": "ଓଡ଼ିଆ" + }, + { + "code": "os", + "language": "ирон æвзаг" + }, + { + "code": "pa", + "language": "ਪੰਜਾਬੀ" + }, + { + "code": "pi", + "language": "पाऴि" + }, + { + "code": "pl", + "language": "Polski" + }, + { + "code": "ps", + "language": "پښتو" + }, + { + "code": "pt", + "language": "Português" + }, + { + "code": "qu", + "language": "Runa Simi" + }, + { + "code": "rm", + "language": "rumantsch grischun" + }, + { + "code": "rn", + "language": "Ikirundi" + }, + { + "code": "ro", + "language": "Română" + }, + { + "code": "ru", + "language": "Русский" + }, + { + "code": "rw", + "language": "Ikinyarwanda" + }, + { + "code": "sa", + "language": "संस्कृतम्" + }, + { + "code": "sc", + "language": "sardu" + }, + { + "code": "sd", + "language": "सिन्धी" + }, + { + "code": "se", + "language": "Davvisámegiella" + }, + { + "code": "sg", + "language": "yângâ tî sängö" + }, + { + "code": "si", + "language": "සිංහල" + }, + { + "code": "sk", + "language": "slovenčina" + }, + { + "code": "sl", + "language": "slovenščina" + }, + { + "code": "sn", + "language": "chiShona" + }, + { + "code": "so", + "language": "Soomaaliga" + }, + { + "code": "sq", + "language": "Shqip" + }, + { + "code": "sr", + "language": "српски језик" + }, + { + "code": "ss", + "language": "SiSwati" + }, + { + "code": "st", + "language": "Sesotho" + }, + { + "code": "su", + "language": "Basa Sunda" + }, + { + "code": "sv", + "language": "Svenska" + }, + { + "code": "sw", + "language": "Kiswahili" + }, + { + "code": "ta", + "language": "தமிழ்" + }, + { + "code": "te", + "language": "తెలుగు" + }, + { + "code": "tg", + "language": "тоҷикӣ" + }, + { + "code": "th", + "language": "ไทย" + }, + { + "code": "ti", + "language": "ትግርኛ" + }, + { + "code": "tk", + "language": "Türkmen" + }, + { + "code": "tl", + "language": "Wikang Tagalog" + }, + { + "code": "tn", + "language": "Setswana" + }, + { + "code": "to", + "language": "faka Tonga" + }, + { + "code": "tr", + "language": "Türkçe" + }, + { + "code": "ts", + "language": "Xitsonga" + }, + { + "code": "tt", + "language": "татар теле" + }, + { + "code": "tw", + "language": "Twi" + }, + { + "code": "ty", + "language": "Reo Tahiti" + }, + { + "code": "ug", + "language": "ئۇيغۇرچە‎" + }, + { + "code": "uk", + "language": "Українська" + }, + { + "code": "ur", + "language": "اردو" + }, + { + "code": "uz", + "language": "Ўзбек" + }, + { + "code": "ve", + "language": "Tshivenḓa" + }, + { + "code": "vi", + "language": "Tiếng Việt" + }, + { + "code": "vo", + "language": "Volapük" + }, + { + "code": "wa", + "language": "walon" + }, + { + "code": "wo", + "language": "Wollof" + }, + { + "code": "xh", + "language": "isiXhosa" + }, + { + "code": "yi", + "language": "ייִדיש" + }, + { + "code": "yo", + "language": "Yorùbá" + }, + { + "code": "za", + "language": "Saɯ cueŋƅ" + }, + { + "code": "zh", + "language": "中文" + }, + { + "code": "zu", + "language": "isiZulu" + } +] \ No newline at end of file diff --git a/app/src/main/assets/themes/cyanea_themes.json b/app/src/main/assets/themes/cyanea_themes.json index db8eff2b1..ae30ea312 100644 --- a/app/src/main/assets/themes/cyanea_themes.json +++ b/app/src/main/assets/themes/cyanea_themes.json @@ -8,7 +8,7 @@ "accent": "#FF2b90d9", "accent_dark": "#FF1b80c9", "accent_light": "#FF772b90d9", - "background": "#FF121212", + "background": "#FF272727", "background_dark": "#FF282c37", "background_light": "#FF282c37", "should_tint_statusbar": true, diff --git a/app/src/main/java/app/fedilab/android/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/BaseMainActivity.java index 343bbcc5d..6a490eb22 100644 --- a/app/src/main/java/app/fedilab/android/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/BaseMainActivity.java @@ -537,7 +537,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24); new Thread(() -> { try { - List accounts = new Account(BaseMainActivity.this).getAll(); + List accounts = new Account(BaseMainActivity.this).getCrossAccounts(); Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { binding.navView.getMenu().clear(); @@ -772,7 +772,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt currentAccount.mastodon_account.display_name = currentAccount.mastodon_account.acct; } headerMainBinding.accountName.setText(currentAccount.mastodon_account.display_name); - Helper.loadPP(headerMainBinding.accountProfilePicture, currentAccount); + Helper.loadPP(headerMainBinding.accountProfilePicture, currentAccount, false); MastodonHelper.loadProfileMediaMastodon(headerMainBinding.backgroundImage, currentAccount.mastodon_account, MastodonHelper.MediaAccountType.HEADER); /* * Some general data are loaded when the app starts such; @@ -786,7 +786,12 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt new ViewModelProvider(BaseMainActivity.this).get(InstancesVM.class).getEmoji(currentInstance); //Retrieve instance info new ViewModelProvider(BaseMainActivity.this).get(InstancesVM.class).getInstance(currentInstance) - .observe(BaseMainActivity.this, instance -> instanceInfo = instance.info); + .observe(BaseMainActivity.this, instance -> { + instanceInfo = instance.info; + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(getString(R.string.INSTANCE_INFO) + MainActivity.currentInstance, Instance.serialize(instanceInfo)); + editor.apply(); + }); //Retrieve filters new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class).getFilters(currentInstance, currentToken) .observe(BaseMainActivity.this, filters -> mainFilters = filters); @@ -850,7 +855,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE)); //For receiving data from other activities LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA)); - if (emojis == null || !emojis.containsKey(BaseMainActivity.currentInstance)) { + if (emojis == null || !emojis.containsKey(BaseMainActivity.currentInstance) || emojis.get(BaseMainActivity.currentInstance) == null) { new Thread(() -> { try { emojis.put(currentInstance, new EmojiInstance(BaseMainActivity.this).getEmojiList(BaseMainActivity.currentInstance)); @@ -1059,11 +1064,6 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } - @Override - protected void onResume() { - super.onResume(); - } - public void redrawPinned(List mastodonLists) { int currentItem = binding.viewPager.getCurrentItem(); new ViewModelProvider(BaseMainActivity.this).get(TopBarVM.class).getDBPinned() diff --git a/app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java b/app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java index 8421d924a..b31c8194e 100644 --- a/app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java @@ -22,8 +22,6 @@ import android.content.SharedPreferences; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -46,6 +44,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -105,14 +104,7 @@ public class AdminAccountActivity extends BaseActivity { } binding.toolbar.setPopupTheme(Helper.popupStyle()); if (account != null) { - new Thread(() -> { - account = SpannableHelper.convertAccount(AdminAccountActivity.this, account); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> initializeView(account); - mainHandler.post(myRunnable); - - }).start(); - + initializeView(account); } else { Toasty.error(AdminAccountActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); finish(); @@ -314,7 +306,10 @@ public class AdminAccountActivity extends BaseActivity { } - binding.accountDn.setText(account.span_display_name != null ? account.span_display_name : account.display_name, TextView.BufferType.SPANNABLE); + binding.accountDn.setText( + account.getSpanDisplayName(AdminAccountActivity.this, + new WeakReference<>(binding.accountDn)), + TextView.BufferType.SPANNABLE); binding.accountUn.setText(String.format("@%s", account.acct)); binding.accountUn.setOnLongClickListener(v -> { ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); diff --git a/app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java b/app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java index 701dbd07f..e3c97445e 100644 --- a/app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java @@ -22,8 +22,6 @@ import android.content.SharedPreferences; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -46,6 +44,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -105,14 +104,7 @@ public class AdminReportActivity extends BaseActivity { } binding.toolbar.setPopupTheme(Helper.popupStyle()); if (account != null) { - new Thread(() -> { - account = SpannableHelper.convertAccount(AdminReportActivity.this, account); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> initializeView(account); - mainHandler.post(myRunnable); - - }).start(); - + initializeView(account); } else { Toasty.error(AdminReportActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); finish(); @@ -331,8 +323,10 @@ public class AdminReportActivity extends BaseActivity { binding.accountMoved.setMovementMethod(LinkMovementMethod.getInstance()); } - - binding.accountDn.setText(account.span_display_name != null ? account.span_display_name : account.display_name, TextView.BufferType.SPANNABLE); + binding.accountDn.setText( + account.getSpanDisplayName(AdminReportActivity.this, + new WeakReference<>(binding.accountDn)), + TextView.BufferType.SPANNABLE); binding.accountUn.setText(String.format("@%s", account.acct)); binding.accountUn.setOnLongClickListener(v -> { ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); diff --git a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java index 58c28c852..5df7503a2 100644 --- a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java @@ -26,6 +26,7 @@ import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.drawable.ColorDrawable; import android.net.Uri; @@ -48,6 +49,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; @@ -69,10 +71,12 @@ import app.fedilab.android.R; import app.fedilab.android.client.entities.api.Attachment; import app.fedilab.android.client.entities.api.Context; import app.fedilab.android.client.entities.api.EmojiInstance; +import app.fedilab.android.client.entities.api.Instance; import app.fedilab.android.client.entities.api.Mention; import app.fedilab.android.client.entities.api.ScheduledStatus; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.client.entities.app.BaseAccount; +import app.fedilab.android.client.entities.app.Languages; import app.fedilab.android.client.entities.app.StatusDraft; import app.fedilab.android.databinding.ActivityPaginationBinding; import app.fedilab.android.databinding.PopupContactBinding; @@ -81,7 +85,6 @@ import app.fedilab.android.helper.DividerDecorationSimple; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.MediaHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.interfaces.OnDownloadInterface; import app.fedilab.android.jobs.ScheduleThreadWorker; @@ -100,7 +103,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana public static final int REQUEST_AUDIO_PERMISSION_RESULT = 1653; public static final int PICK_MEDIA = 5700; public static final int TAKE_PHOTO = 5600; - + private final Timer timer = new Timer(); private List statusList; private Status statusReply, statusMention; private StatusDraft statusDraft; @@ -234,6 +237,14 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } }).start(); } + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ComposeActivity.this); + if (MainActivity.instanceInfo == null) { + String instanceInfo = sharedpreferences.getString(getString(R.string.INSTANCE_INFO) + instance, null); + if (instanceInfo != null) { + MainActivity.instanceInfo = Instance.restore(instanceInfo); + } + } + StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class); //Empty compose List statusDraftList = new ArrayList<>(); @@ -257,86 +268,70 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } }); } else if (statusDraft != null) {//Restore a draft with all messages - new Thread(() -> { - if (statusDraft.statusReplyList != null) { - statusDraft.statusReplyList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusDraft.statusReplyList); - } - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> { - if (statusDraft.statusReplyList != null) { - statusList.addAll(statusDraft.statusReplyList); - binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); - } - int statusCount = statusList.size(); - statusList.addAll(statusDraft.statusDraftList); - composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility); - composeAdapter.manageDrafts = this; - LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); - binding.recyclerView.setLayoutManager(mLayoutManager); - binding.recyclerView.setAdapter(composeAdapter); - binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); - }; - mainHandler.post(myRunnable); - }).start(); + if (statusDraft.statusReplyList != null) { + statusList.addAll(statusDraft.statusReplyList); + binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); + } + int statusCount = statusList.size(); + statusList.addAll(statusDraft.statusDraftList); + composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility); + composeAdapter.manageDrafts = this; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(composeAdapter); + binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); } else if (statusReply != null) { - new Thread(() -> { - statusReply = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusReply); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> { - statusList.add(statusReply); - int statusCount = statusList.size(); - statusDraftList.get(0).in_reply_to_id = statusReply.id; - //We change order for mentions - //At first place the account that has been mentioned if it's not our - statusDraftList.get(0).mentions = new ArrayList<>(); - if (!statusReply.account.acct.equalsIgnoreCase(currentAccount.mastodon_account.acct)) { - Mention mention = new Mention(); - mention.acct = "@" + statusReply.account.acct; - mention.url = statusReply.account.url; - mention.username = statusReply.account.username; - statusDraftList.get(0).mentions.add(mention); - } + statusList.add(statusReply); + int statusCount = statusList.size(); + statusDraftList.get(0).in_reply_to_id = statusReply.id; + //We change order for mentions + //At first place the account that has been mentioned if it's not our + statusDraftList.get(0).mentions = new ArrayList<>(); + if (statusReply.account.acct != null && !statusReply.account.acct.equalsIgnoreCase(currentAccount.mastodon_account.acct)) { + Mention mention = new Mention(); + mention.acct = "@" + statusReply.account.acct; + mention.url = statusReply.account.url; + mention.username = statusReply.account.username; + statusDraftList.get(0).mentions.add(mention); + } - //There are other mentions to - if (statusReply.mentions != null && statusReply.mentions.size() > 0) { - for (Mention mentionTmp : statusReply.mentions) { - if (!mentionTmp.acct.equalsIgnoreCase(statusReply.account.acct) && !mentionTmp.acct.equalsIgnoreCase(currentAccount.mastodon_account.acct)) { - statusDraftList.get(0).mentions.add(mentionTmp); - } - } + //There are other mentions to + if (statusReply.mentions != null && statusReply.mentions.size() > 0) { + for (Mention mentionTmp : statusReply.mentions) { + if (statusReply.account.acct != null && !mentionTmp.acct.equalsIgnoreCase(statusReply.account.acct) && !mentionTmp.acct.equalsIgnoreCase(currentAccount.mastodon_account.acct)) { + statusDraftList.get(0).mentions.add(mentionTmp); } - if (mentionBooster != null) { - Mention mention = new Mention(); - mention.acct = mentionBooster.acct; - mention.url = mentionBooster.url; - mention.username = mentionBooster.username; - boolean present = false; - for (Mention mentionTmp : statusDraftList.get(0).mentions) { - if (mentionTmp.acct.equalsIgnoreCase(mentionBooster.acct)) { - present = true; - break; - } - } - if (!present) { - statusDraftList.get(0).mentions.add(mention); - } + } + } + if (mentionBooster != null) { + Mention mention = new Mention(); + mention.acct = mentionBooster.acct; + mention.url = mentionBooster.url; + mention.username = mentionBooster.username; + boolean present = false; + for (Mention mentionTmp : statusDraftList.get(0).mentions) { + if (mentionTmp.acct.equalsIgnoreCase(mentionBooster.acct)) { + present = true; + break; } - if (statusReply.spoiler_text != null) { - statusDraftList.get(0).spoiler_text = statusReply.spoiler_text; - } - //StatusDraftList at this point should only have one element - statusList.addAll(statusDraftList); - composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility); - composeAdapter.manageDrafts = this; - LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); - binding.recyclerView.setLayoutManager(mLayoutManager); - binding.recyclerView.setAdapter(composeAdapter); - statusesVM.getContext(currentInstance, BaseMainActivity.currentToken, statusReply.id) - .observe(ComposeActivity.this, this::initializeContextView); - }; - mainHandler.post(myRunnable); - }).start(); + } + if (!present) { + statusDraftList.get(0).mentions.add(mention); + } + } + if (statusReply.spoiler_text != null) { + statusDraftList.get(0).spoiler_text = statusReply.spoiler_text; + } + //StatusDraftList at this point should only have one element + statusList.addAll(statusDraftList); + composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility); + composeAdapter.manageDrafts = this; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(composeAdapter); + statusesVM.getContext(currentInstance, BaseMainActivity.currentToken, statusReply.id) + .observe(ComposeActivity.this, this::initializeContextView); } else { //Compose without replying statusList.addAll(statusDraftList); @@ -348,19 +343,20 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana if (statusMention != null) { composeAdapter.loadMentions(statusMention); } - } MastodonHelper.loadPPMastodon(binding.profilePicture, account.mastodon_account); LocalBroadcastManager.getInstance(this) .registerReceiver(imageReceiver, new IntentFilter(Helper.INTENT_SEND_MODIFIED_IMAGE)); - new Timer().scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - storeDraft(false); - } - }, 0, 10000); + if (timer != null) { + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + storeDraft(false); + } + }, 0, 10000); + } if (sharedUriList != null && sharedUriList.size() > 0) { @@ -395,6 +391,9 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana @Override protected void onDestroy() { super.onDestroy(); + if (timer != null) { + timer.cancel(); + } LocalBroadcastManager.getInstance(this) .unregisterReceiver(imageReceiver); } @@ -579,6 +578,46 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } else { Toasty.info(ComposeActivity.this, getString(R.string.toot_error_no_content), Toasty.LENGTH_SHORT).show(); } + } else if (item.getItemId() == R.id.action_language) { + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ComposeActivity.this); + List languages = Languages.get(ComposeActivity.this); + String[] codesArr = new String[0]; + String[] languagesArr = new String[0]; + + String currentCode = sharedpreferences.getString(getString(R.string.SET_COMPOSE_LANGUAGE) + account.user_id + account.instance, null); + int selection = 0; + + if (languages != null) { + codesArr = new String[languages.size()]; + languagesArr = new String[languages.size()]; + int i = 0; + for (Languages.Language language : languages) { + codesArr[i] = language.code; + languagesArr[i] = language.language; + if (currentCode != null && currentCode.equalsIgnoreCase(language.code)) { + selection = i; + } + i++; + } + } + SharedPreferences.Editor editor = sharedpreferences.edit(); + AlertDialog.Builder builder = new AlertDialog.Builder(ComposeActivity.this, Helper.dialogStyle()); + builder.setTitle(getString(R.string.message_language)); + + builder.setSingleChoiceItems(languagesArr, selection, null); + String[] finalCodesArr = codesArr; + builder.setPositiveButton(R.string.validate, (dialog, which) -> { + int selectedPosition = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + editor.putString(getString(R.string.SET_COMPOSE_LANGUAGE) + account.user_id + account.instance, finalCodesArr[selectedPosition]); + editor.apply(); + dialog.dismiss(); + }); + builder.setNegativeButton(R.string.reset, (dialog, which) -> { + editor.putString(getString(R.string.SET_COMPOSE_LANGUAGE) + account.user_id + account.instance, null); + editor.apply(); + dialog.dismiss(); + }); + builder.create().show(); } return true; } @@ -667,6 +706,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } else { statusReplies.add(status); } + } if (statusDraft == null) { statusDraft = new StatusDraft(ComposeActivity.this); @@ -677,10 +717,12 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } } if (statusReplies.size() > 0) { - statusDraft.statusReplyList = statusReplies; + statusDraft.statusReplyList = new ArrayList<>(); + statusDraft.statusReplyList.addAll(statusReplies); } if (statusDrafts.size() > 0) { - statusDraft.statusDraftList = statusDrafts; + statusDraft.statusDraftList = new ArrayList<>(); + statusDraft.statusDraftList.addAll(statusDrafts); } if (statusDraft.instance == null) { statusDraft.instance = account.instance; @@ -750,6 +792,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); intent.putExtra(Helper.ARG_INSTANCE, instance); intent.putExtra(Helper.ARG_TOKEN, token); + intent.putExtra(Helper.ARG_USER_ID, account.user_id); intent.putExtra(Helper.ARG_SCHEDULED_DATE, scheduledDate); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent); @@ -757,7 +800,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana startService(intent); } } else { - new ThreadMessageService(ComposeActivity.this, instance, token, statusDraft, scheduledDate); + new ThreadMessageService(ComposeActivity.this, instance, account.user_id, token, statusDraft, scheduledDate); } finish(); } diff --git a/app/src/main/java/app/fedilab/android/activities/ContextActivity.java b/app/src/main/java/app/fedilab/android/activities/ContextActivity.java index b763e87f3..0287e4d90 100644 --- a/app/src/main/java/app/fedilab/android/activities/ContextActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ContextActivity.java @@ -43,7 +43,6 @@ import app.fedilab.android.databinding.ActivityConversationBinding; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonContext; import app.fedilab.android.viewmodel.mastodon.StatusesVM; @@ -82,21 +81,14 @@ public class ContextActivity extends BaseActivity { focusedStatus = null; // or other values if (b != null) focusedStatus = (Status) b.getSerializable(Helper.ARG_STATUS); - if (focusedStatus == null && currentAccount == null || currentAccount.mastodon_account == null) { + if (focusedStatus == null || currentAccount == null || currentAccount.mastodon_account == null) { finish(); return; } MastodonHelper.loadPPMastodon(binding.profilePicture, currentAccount.mastodon_account); Bundle bundle = new Bundle(); - new Thread(() -> { - focusedStatus = SpannableHelper.convertStatus(getApplication().getApplicationContext(), focusedStatus); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> { - bundle.putSerializable(Helper.ARG_STATUS, focusedStatus); - currentFragment = Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_content_main, new FragmentMastodonContext(), bundle, null, null); - }; - mainHandler.post(myRunnable); - }).start(); + bundle.putSerializable(Helper.ARG_STATUS, focusedStatus); + currentFragment = Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_content_main, new FragmentMastodonContext(), bundle, null, null); StatusesVM timelinesVM = new ViewModelProvider(ContextActivity.this).get(StatusesVM.class); timelinesVM.getStatus(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, focusedStatus.id).observe(ContextActivity.this, status -> { if (status != null) { @@ -168,10 +160,4 @@ public class ContextActivity extends BaseActivity { return true; } - @Override - protected void onDestroy() { - super.onDestroy(); - binding = null; - currentFragment = null; - } } \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/activities/DraftActivity.java b/app/src/main/java/app/fedilab/android/activities/DraftActivity.java index cfbb235bf..bcf6585f3 100644 --- a/app/src/main/java/app/fedilab/android/activities/DraftActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/DraftActivity.java @@ -205,12 +205,6 @@ public class DraftActivity extends BaseActivity implements StatusDraftAdapter.Dr } } - @Override - public void onDestroy() { - super.onDestroy(); - binding.lvStatus.setAdapter(null); - binding = null; - } @Override public void onAllDeleted() { diff --git a/app/src/main/java/app/fedilab/android/activities/MediaActivity.java b/app/src/main/java/app/fedilab/android/activities/MediaActivity.java index ca275b439..2fe067f2d 100644 --- a/app/src/main/java/app/fedilab/android/activities/MediaActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/MediaActivity.java @@ -294,7 +294,6 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { @Override public void onDestroy() { - binding = null; unregisterReceiver(onDownloadComplete); super.onDestroy(); } diff --git a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java index 1226c83d2..2d8930499 100644 --- a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java @@ -27,11 +27,7 @@ import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Html; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -64,6 +60,7 @@ import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.google.android.material.tabs.TabLayout; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -148,8 +145,6 @@ public class ProfileActivity extends BaseActivity { account_id = b.getString(Helper.ARG_USER_ID, null); mention_str = b.getString(Helper.ARG_MENTION, null); } - - postponeEnterTransition(); //Remove title @@ -165,14 +160,7 @@ public class ProfileActivity extends BaseActivity { binding.toolbar.setPopupTheme(Helper.popupStyle()); accountsVM = new ViewModelProvider(ProfileActivity.this).get(AccountsVM.class); if (account != null) { - new Thread(() -> { - account = SpannableHelper.convertAccount(ProfileActivity.this, account); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> initializeView(account); - mainHandler.post(myRunnable); - - }).start(); - + initializeView(account); } else if (account_id != null) { accountsVM.getAccount(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account_id).observe(ProfileActivity.this, fetchedAccount -> { account = fetchedAccount; @@ -359,11 +347,11 @@ public class ProfileActivity extends BaseActivity { binding.fieldsContainer.setAdapter(fieldAdapter); binding.fieldsContainer.setLayoutManager(new LinearLayoutManager(ProfileActivity.this)); } - if (account.span_display_name == null && account.display_name == null) { - binding.accountDn.setText(account.username); - } else { - binding.accountDn.setText(account.span_display_name != null ? account.span_display_name : account.display_name, TextView.BufferType.SPANNABLE); - } + + binding.accountDn.setText( + account.getSpanDisplayName(ProfileActivity.this, + new WeakReference<>(binding.accountDn)), + TextView.BufferType.SPANNABLE); binding.accountUn.setText(String.format("@%s", account.acct)); binding.accountUn.setOnLongClickListener(v -> { @@ -377,12 +365,10 @@ public class ProfileActivity extends BaseActivity { clipboard.setPrimaryClip(clip); return false; }); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - binding.accountNote.setText(account.span_note != null ? account.span_note : new SpannableString(Html.fromHtml(account.note, Html.FROM_HTML_MODE_COMPACT)), TextView.BufferType.SPANNABLE); - else - binding.accountNote.setText(account.span_note != null ? account.span_note : new SpannableString(Html.fromHtml(account.note)), TextView.BufferType.SPANNABLE); - + binding.accountNote.setText( + account.getSpanNote(ProfileActivity.this, + new WeakReference<>(binding.accountNote)), + TextView.BufferType.SPANNABLE); binding.accountNote.setMovementMethod(LinkMovementMethod.getInstance()); MastodonHelper.loadPPMastodon(binding.accountPp, account); diff --git a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java index 7fee806a8..1cb92672d 100644 --- a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java @@ -53,6 +53,7 @@ public class SearchResultTabActivity extends BaseActivity { private String search; private ActivitySearchResultTabsBinding binding; + private TabLayout.Tab initial; @Override protected void onCreate(Bundle savedInstanceState) { @@ -77,7 +78,8 @@ public class SearchResultTabActivity extends BaseActivity { getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); } setTitle(search); - binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags))); + initial = binding.searchTabLayout.newTab(); + binding.searchTabLayout.addTab(initial.setText(getString(R.string.tags))); binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.accounts))); binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots))); binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.action_cache))); @@ -125,7 +127,6 @@ public class SearchResultTabActivity extends BaseActivity { @Override public boolean onQueryTextSubmit(String query) { InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - assert imm != null; imm.hideSoftInputFromWindow(binding.searchTabLayout.getWindowToken(), 0); query = query.replaceAll("^#+", ""); search = query.trim(); @@ -134,6 +135,7 @@ public class SearchResultTabActivity extends BaseActivity { searchView.clearFocus(); setTitle(search); searchView.setIconified(true); + binding.searchTabLayout.selectTab(initial); return false; } diff --git a/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java b/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java index e6502caa8..44f24efea 100644 --- a/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java @@ -16,6 +16,7 @@ package app.fedilab.android.activities; import static app.fedilab.android.BaseMainActivity.currentAccount; +import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.view.MenuItem; @@ -62,6 +63,7 @@ public class SettingsActivity extends BaseActivity { } canGoBack = false; + binding.setAccount.setOnClickListener(v -> displaySettings(SettingsEnum.ACCOUNT)); binding.setTimelines.setOnClickListener(v -> displaySettings(SettingsEnum.TIMELINES)); binding.setNotifications.setOnClickListener(v -> displaySettings(SettingsEnum.NOTIFICATIONS)); binding.setInterface.setOnClickListener(v -> displaySettings(SettingsEnum.INTERFACE)); @@ -79,61 +81,66 @@ public class SettingsActivity extends BaseActivity { public void displaySettings(SettingsEnum settingsEnum) { - ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = - fragmentManager.beginTransaction(); - String category = ""; - switch (settingsEnum) { - case TIMELINES: - FragmentTimelinesSettings fragmentTimelinesSettings = new FragmentTimelinesSettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentTimelinesSettings); - currentFragment = fragmentTimelinesSettings; - category = getString(R.string.settings_category_label_timelines); - break; - case NOTIFICATIONS: - FragmentNotificationsSettings fragmentNotificationsSettings = new FragmentNotificationsSettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentNotificationsSettings); - currentFragment = fragmentNotificationsSettings; - category = getString(R.string.notifications); - break; - case INTERFACE: - FragmentInterfaceSettings fragmentInterfaceSettings = new FragmentInterfaceSettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentInterfaceSettings); - currentFragment = fragmentInterfaceSettings; - category = getString(R.string.settings_category_label_interface); - break; - case COMPOSE: - FragmentComposeSettings fragmentComposeSettings = new FragmentComposeSettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentComposeSettings); - currentFragment = fragmentComposeSettings; - category = getString(R.string.compose); - break; - case PRIVACY: - FragmentPrivacySettings fragmentPrivacySettings = new FragmentPrivacySettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentPrivacySettings); - currentFragment = fragmentPrivacySettings; - category = getString(R.string.action_privacy); - break; - case THEMING: - FragmentThemingSettings fragmentThemingSettings = new FragmentThemingSettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentThemingSettings); - currentFragment = fragmentThemingSettings; - category = getString(R.string.theming); - break; - case LANGUAGE: - FragmentLanguageSettings fragmentLanguageSettings = new FragmentLanguageSettings(); - fragmentTransaction.replace(R.id.fragment_container, fragmentLanguageSettings); - currentFragment = fragmentLanguageSettings; - category = getString(R.string.languages); - break; + if (settingsEnum == SettingsEnum.ACCOUNT) { + Intent intent = new Intent(SettingsActivity.this, EditProfileActivity.class); + startActivity(intent); + } else { + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + String category = ""; + switch (settingsEnum) { + case TIMELINES: + FragmentTimelinesSettings fragmentTimelinesSettings = new FragmentTimelinesSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentTimelinesSettings); + currentFragment = fragmentTimelinesSettings; + category = getString(R.string.settings_category_label_timelines); + break; + case NOTIFICATIONS: + FragmentNotificationsSettings fragmentNotificationsSettings = new FragmentNotificationsSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentNotificationsSettings); + currentFragment = fragmentNotificationsSettings; + category = getString(R.string.notifications); + break; + case INTERFACE: + FragmentInterfaceSettings fragmentInterfaceSettings = new FragmentInterfaceSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentInterfaceSettings); + currentFragment = fragmentInterfaceSettings; + category = getString(R.string.settings_category_label_interface); + break; + case COMPOSE: + FragmentComposeSettings fragmentComposeSettings = new FragmentComposeSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentComposeSettings); + currentFragment = fragmentComposeSettings; + category = getString(R.string.compose); + break; + case PRIVACY: + FragmentPrivacySettings fragmentPrivacySettings = new FragmentPrivacySettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentPrivacySettings); + currentFragment = fragmentPrivacySettings; + category = getString(R.string.action_privacy); + break; + case THEMING: + FragmentThemingSettings fragmentThemingSettings = new FragmentThemingSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentThemingSettings); + currentFragment = fragmentThemingSettings; + category = getString(R.string.theming); + break; + case LANGUAGE: + FragmentLanguageSettings fragmentLanguageSettings = new FragmentLanguageSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentLanguageSettings); + currentFragment = fragmentLanguageSettings; + category = getString(R.string.languages); + break; - } - String title = String.format(Locale.getDefault(), "%s - %s", getString(R.string.settings), category); - setTitle(title); - canGoBack = true; - fragmentTransaction.commit(); - }); + } + String title = String.format(Locale.getDefault(), "%s - %s", getString(R.string.settings), category); + setTitle(title); + canGoBack = true; + fragmentTransaction.commit(); + }); + } } @@ -156,15 +163,6 @@ public class SettingsActivity extends BaseActivity { } - @Override - protected void onDestroy() { - super.onDestroy(); - if (currentFragment != null) { - currentFragment.onDestroy(); - } - binding = null; - - } @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -177,6 +175,8 @@ public class SettingsActivity extends BaseActivity { public enum SettingsEnum { + @SerializedName("ACCOUNT") + ACCOUNT("ACCOUNT"), @SerializedName("TIMELINES") TIMELINES("TIMELINES"), @SerializedName("NOTIFICATIONS") diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/PleromaAPI.java b/app/src/main/java/app/fedilab/android/client/endpoints/PleromaAPI.java new file mode 100644 index 000000000..d1309bb28 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/endpoints/PleromaAPI.java @@ -0,0 +1,40 @@ +package app.fedilab.android.client.endpoints; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.Header; +import retrofit2.http.PUT; +import retrofit2.http.Path; + +public interface PleromaAPI { + + + @PUT("pleroma/statuses/{id}/reactions/{name}") + Call addReaction( + @Header("Authorization") String app_token, + @Path("id") String id, + @Path("name") String name + ); + + @DELETE("pleroma/statuses/{id}/reactions/{name}") + Call removeReaction( + @Header("Authorization") String app_token, + @Path("id") String id, + @Path("name") String name + ); + +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Account.java b/app/src/main/java/app/fedilab/android/client/entities/api/Account.java index 1905ad42f..4b2392d89 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Account.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Account.java @@ -14,15 +14,20 @@ package app.fedilab.android.client.entities.api; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ +import android.content.Context; import android.text.Spannable; +import android.view.View; import com.google.gson.annotations.SerializedName; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; +import app.fedilab.android.helper.SpannableHelper; + public class Account implements Serializable { @SerializedName("id") @@ -74,11 +79,21 @@ public class Account implements Serializable { @SerializedName("moved") public Account moved; - //Some extra spannable element - They will be filled automatically when fetching the account - public transient Spannable span_display_name; - public transient Spannable span_note; + public synchronized Spannable getSpanDisplayName(Context context, WeakReference viewWeakReference) { + if (display_name == null || display_name.isEmpty()) { + display_name = username; + } + return SpannableHelper.convert(context, display_name, null, this, null, true, viewWeakReference); + } + + + public synchronized Spannable getSpanNote(Context context, WeakReference viewWeakReference) { + return SpannableHelper.convert(context, note, null, this, null, true, viewWeakReference); + } + public transient RelationShip relationShip; + public static class AccountParams implements Serializable { @SerializedName("discoverable") public boolean discoverable; diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java b/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java index 4669833d7..b015313fd 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java @@ -14,13 +14,18 @@ package app.fedilab.android.client.entities.api; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ +import android.content.Context; import android.text.Spannable; +import android.view.View; import com.google.gson.annotations.SerializedName; +import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; +import app.fedilab.android.helper.SpannableHelper; + public class Announcement { @SerializedName("id") public String id; @@ -49,6 +54,9 @@ public class Announcement { @SerializedName("reactions") public List reactions; - //Some extra spannable element - They will be filled automatically when fetching the status - public transient Spannable span_content; + + public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference) { + return SpannableHelper.convert(context, content, null, null, this, true, viewWeakReference); + } + } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Field.java b/app/src/main/java/app/fedilab/android/client/entities/api/Field.java index 55754dc6c..e1e2b3cdb 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Field.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Field.java @@ -14,13 +14,22 @@ package app.fedilab.android.client.entities.api; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ +import android.content.Context; import android.text.Spannable; +import android.text.style.ForegroundColorSpan; +import android.view.View; + +import androidx.core.content.ContextCompat; import com.google.gson.annotations.SerializedName; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.Date; +import app.fedilab.android.R; +import app.fedilab.android.helper.SpannableHelper; + public class Field implements Serializable { @SerializedName("name") public String name; @@ -30,7 +39,19 @@ public class Field implements Serializable { public Date verified_at; //Some extra spannable element - They will be filled automatically when fetching the account - public transient Spannable value_span; + private transient ForegroundColorSpan value_span; + + public synchronized Spannable getValueSpan(Context context, Account account, WeakReference viewWeakReference) { + + if (verified_at != null && value != null) { + value_span = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.verified_text)); + } + Spannable spannable = SpannableHelper.convert(context, value, null, account, null, true, viewWeakReference); + if (value_span != null && spannable != null) { + spannable.setSpan(value_span, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return spannable; + } public static class FieldParams implements Serializable { @SerializedName("name") diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Instance.java b/app/src/main/java/app/fedilab/android/client/entities/api/Instance.java index 3b9547efd..1ea2c7262 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Instance.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Instance.java @@ -1,5 +1,6 @@ package app.fedilab.android.client.entities.api; +import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import java.io.Serializable; @@ -113,6 +114,25 @@ public class Instance implements Serializable { return mimeTypes; } + + public static String serialize(Instance instance) { + Gson gson = new Gson(); + try { + return gson.toJson(instance); + } catch (Exception e) { + return null; + } + } + + public static Instance restore(String serialized) { + Gson gson = new Gson(); + try { + return gson.fromJson(serialized, Instance.class); + } catch (Exception e) { + return null; + } + } + public static class Configuration implements Serializable { @SerializedName("statuses") public StatusesConf statusesConf; diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java b/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java index 6d9072d1c..851c32264 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java @@ -29,6 +29,8 @@ public class Notification { public String type; @SerializedName("created_at") public Date created_at; + @SerializedName("emoji") + public String emoji; @SerializedName("account") public Account account; @SerializedName("status") diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Pleroma.java b/app/src/main/java/app/fedilab/android/client/entities/api/Pleroma.java new file mode 100644 index 000000000..bee1912bf --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Pleroma.java @@ -0,0 +1,25 @@ +package app.fedilab.android.client.entities.api; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class Pleroma implements Serializable { + @SerializedName("emoji_reactions") + public List emoji_reactions; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java b/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java index a0d80fbea..a91a9361e 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java @@ -14,14 +14,19 @@ package app.fedilab.android.client.entities.api; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ +import android.content.Context; import android.text.Spannable; +import android.view.View; import com.google.gson.annotations.SerializedName; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; +import app.fedilab.android.helper.SpannableHelper; + public class Poll implements Serializable { @SerializedName("id") @@ -53,7 +58,11 @@ public class Poll implements Serializable { @SerializedName("votes_count") public int votes_count; - //Some extra spannable element - They will be filled automatically when fetching the poll public transient Spannable span_title; + + public Spannable getSpanTitle(Context context, Status status, WeakReference viewWeakReference) { + span_title = SpannableHelper.convert(context, title, status, null, null, true, viewWeakReference); + return span_title; + } } } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Reaction.java b/app/src/main/java/app/fedilab/android/client/entities/api/Reaction.java index 396355247..028f93ef1 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Reaction.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Reaction.java @@ -16,7 +16,9 @@ package app.fedilab.android.client.entities.api; import com.google.gson.annotations.SerializedName; -public class Reaction { +import java.io.Serializable; + +public class Reaction implements Serializable { @SerializedName("name") public String name; @SerializedName("count") diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java index 967364848..144eea3e7 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java @@ -14,16 +14,21 @@ package app.fedilab.android.client.entities.api; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ +import android.content.Context; import android.text.Spannable; +import android.view.View; import androidx.annotation.NonNull; import com.google.gson.annotations.SerializedName; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; +import app.fedilab.android.helper.SpannableHelper; + public class Status implements Serializable, Cloneable { @SerializedName("id") @@ -84,14 +89,12 @@ public class Status implements Serializable, Cloneable { public Card card; @SerializedName("poll") public Poll poll; + @SerializedName("pleroma") + public Pleroma pleroma; public Attachment art_attachment; - //Some extra spannable element - They will be filled automatically when fetching the status - public transient Spannable span_content; - public transient Spannable span_spoiler_text; - public transient Spannable span_translate; public boolean isExpended = false; public boolean isTruncated = true; public boolean isFetchMore = false; @@ -105,6 +108,24 @@ public class Status implements Serializable, Cloneable { public transient boolean setCursorToEnd = false; public transient int cursorPosition = 0; public transient boolean submitted = false; + //Some extra spannable element - They will be filled automatically when fetching the status + + public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference) { + return SpannableHelper.convert(context, content, this, null, null, true, viewWeakReference); + } + + + public Spannable getSpanContentNitter() { + return SpannableHelper.convertNitter(content); + } + + public synchronized Spannable getSpanSpoiler(Context context, WeakReference viewWeakReference) { + return SpannableHelper.convert(context, spoiler_text, this, null, null, true, viewWeakReference); + } + + public synchronized Spannable getSpanTranslate(Context context, WeakReference viewWeakReference) { + return SpannableHelper.convert(context, translationContent, this, null, null, true, viewWeakReference); + } @NonNull public Object clone() throws CloneNotSupportedException { diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Account.java b/app/src/main/java/app/fedilab/android/client/entities/app/Account.java index 1be6458b4..046bc5c72 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/Account.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Account.java @@ -295,7 +295,7 @@ public class Account extends BaseAccount implements Serializable { } try { Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, null, null, null, null, null, null); - return cursorToListUser(c); + return cursorToListUserWithOwner(c); } catch (Exception e) { return null; } diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Languages.java b/app/src/main/java/app/fedilab/android/client/entities/app/Languages.java new file mode 100644 index 000000000..23445a9a3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Languages.java @@ -0,0 +1,62 @@ +package app.fedilab.android.client.entities.app; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.List; + + +public class Languages implements Serializable { + + @SerializedName("languages") + public List languages; + + public static List get(AppCompatActivity activity) { + try { + InputStream is = activity.getAssets().open("languages/iso_639_1.json"); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + String json = new String(buffer, StandardCharsets.UTF_8); + Gson gson = new Gson(); + try { + return gson.fromJson(json, new TypeToken>() { + }.getType()); + } catch (Exception e) { + return null; + } + } catch (IOException ex) { + ex.printStackTrace(); + return null; + } + } + + public static class Language implements Serializable { + @SerializedName("code") + public String code; + @SerializedName("language") + public String language; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java b/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java index d3f56ce41..26140deba 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java @@ -29,7 +29,6 @@ import app.fedilab.android.client.entities.api.Notification; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.sqlite.Sqlite; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonNotification; @@ -576,7 +575,6 @@ public class QuickLoad { } quickLoad.position = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_POSITION)); //TimelineHelper.filterStatus(_mContext, quickLoad.statuses, TimelineHelper.FilterTimeLineType.PUBLIC); - quickLoad.statuses = SpannableHelper.convertStatus(_mContext, quickLoad.statuses); return quickLoad; } diff --git a/app/src/main/java/app/fedilab/android/client/entities/misskey/MisskeyNote.java b/app/src/main/java/app/fedilab/android/client/entities/misskey/MisskeyNote.java index 438325317..0c5ccaf1f 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/misskey/MisskeyNote.java +++ b/app/src/main/java/app/fedilab/android/client/entities/misskey/MisskeyNote.java @@ -55,7 +55,7 @@ public class MisskeyNote implements Serializable { @SerializedName("emojis") public List emojis; - public static Status convert(MisskeyNote misskeyNote) { + public static Status convert(MisskeyNote misskeyNote, String instance) { Status status = new Status(); status.id = misskeyNote.id; status.in_reply_to_id = misskeyNote.replyId; @@ -64,7 +64,10 @@ public class MisskeyNote implements Serializable { status.spoiler_text = misskeyNote.cw; status.visibility = misskeyNote.visibility; status.created_at = misskeyNote.createdAt; - status.uri = misskeyNote.uri; + if (misskeyNote.url == null) { + misskeyNote.url = "https://" + instance + "/notes/" + misskeyNote.id; + } + status.uri = misskeyNote.uri != null ? misskeyNote.uri : misskeyNote.url; status.url = misskeyNote.url; Account account = new Account(); diff --git a/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java b/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java index afc88ee4d..d53985337 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java +++ b/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java @@ -105,8 +105,8 @@ public class Nitter implements Serializable { } } Nitter nitterAccount = accounts.get(feedItem.creator); + app.fedilab.android.client.entities.api.Account account = new app.fedilab.android.client.entities.api.Account(); if (nitterAccount != null) { - app.fedilab.android.client.entities.api.Account account = new app.fedilab.android.client.entities.api.Account(); String[] names = nitterAccount.image.title.split("/"); account.id = feedItem.guid; account.acct = names[1].replace("@", ""); @@ -116,6 +116,15 @@ public class Nitter implements Serializable { account.avatar_static = nitterAccount.image.url; account.url = nitterAccount.image.link; status.account = account; + } else { + account.id = feedItem.guid; + account.acct = feedItem.creator.replace("@", ""); + account.username = feedItem.creator.replace("@", ""); + account.display_name = feedItem.creator.replace("@", ""); + account.avatar = ""; + account.avatar_static = ""; + account.url = feedItem.link; + status.account = account; } if (feedItem.description != null) { diff --git a/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java b/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java index 1b617e55d..97b7fdc10 100644 --- a/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java @@ -149,7 +149,7 @@ public class CrossActionHelper { } }); } else if (targetedStatus != null) { - searchVM.search(ownerAccount.instance, ownerAccount.token, targetedStatus.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(ownerAccount.instance, ownerAccount.token, targetedStatus.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status status = results.statuses.get(0); @@ -278,8 +278,6 @@ public class CrossActionHelper { if (results != null) { if (results.statuses == null) { results.statuses = new ArrayList<>(); - } else { - results.statuses = SpannableHelper.convertStatus(context, results.statuses); } if (results.accounts == null) { results.accounts = new ArrayList<>(); @@ -308,15 +306,15 @@ public class CrossActionHelper { /** * Fetch and federate the remote status */ - public static void fetchRemoteAccount(@NonNull Context context, @NonNull BaseAccount ownerAccount, app.fedilab.android.client.entities.api.Account targetedAccount, Callback callback) { + public static void fetchRemoteAccount(@NonNull Context context, @NonNull BaseAccount ownerAccount, String targetedAcct, Callback callback) { MastodonSearchService mastodonSearchService = init(context, BaseMainActivity.currentInstance); String search; - if (targetedAccount.acct.contains("@")) { //Not from same instance - search = targetedAccount.acct; + if (targetedAcct.contains("@")) { //Not from same instance + search = targetedAcct; } else { - search = targetedAccount.acct + "@" + BaseMainActivity.currentInstance; + search = targetedAcct + "@" + BaseMainActivity.currentInstance; } new Thread(() -> { Call resultsCall = mastodonSearchService.search(ownerAccount.token, search, null, "accounts", false, true, false, 0, null, null, 1); @@ -329,8 +327,6 @@ public class CrossActionHelper { if (results != null) { if (results.statuses == null) { results.statuses = new ArrayList<>(); - } else { - results.statuses = SpannableHelper.convertStatus(context, results.statuses); } if (results.accounts == null) { results.accounts = new ArrayList<>(); @@ -374,8 +370,6 @@ public class CrossActionHelper { if (results != null) { if (results.statuses == null) { results.statuses = new ArrayList<>(); - } else { - results.statuses = SpannableHelper.convertStatus(context, results.statuses); } if (results.accounts == null) { results.accounts = new ArrayList<>(); diff --git a/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java b/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java new file mode 100644 index 000000000..d32304750 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java @@ -0,0 +1,106 @@ +package app.fedilab.android.helper; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.text.style.ReplacementSpan; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; + +import java.lang.ref.WeakReference; + +import app.fedilab.android.R; + + +public class CustomEmoji extends ReplacementSpan { + private final float scale; + private final WeakReference viewWeakReference; + private Drawable imageDrawable; + + + CustomEmoji(WeakReference viewWeakReference) { + Context mContext = viewWeakReference.get().getContext(); + this.viewWeakReference = viewWeakReference; + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + scale = sharedpreferences.getFloat(mContext.getString(R.string.SET_FONT_SCALE), 1.0f); + } + + @Override + public int getSize(@NonNull Paint paint, CharSequence charSequence, int i, int i1, @Nullable Paint.FontMetricsInt fontMetricsInt) { + if (fontMetricsInt != null) { + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + fontMetricsInt.top = (int) fontMetrics.top; + fontMetricsInt.ascent = (int) fontMetrics.ascent; + fontMetricsInt.descent = (int) fontMetrics.descent; + fontMetricsInt.bottom = (int) fontMetrics.bottom; + } + return (int) (paint.getTextSize() * scale); + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence charSequence, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + if (imageDrawable != null) { + canvas.save(); + int emojiSize = (int) (paint.getTextSize() * scale); + Drawable drawable = imageDrawable; + drawable.setBounds(0, 0, emojiSize, emojiSize); + int transY = bottom - drawable.getBounds().bottom; + transY -= paint.getFontMetrics().descent / 2; + canvas.translate(x, (float) transY); + drawable.draw(canvas); + canvas.restore(); + } + } + + public Target getTarget(boolean animate) { + return new CustomTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + View view = viewWeakReference.get(); + if (animate && resource instanceof Animatable) { + Drawable.Callback callback = resource.getCallback(); + resource.setCallback(new Drawable.Callback() { + @Override + public void invalidateDrawable(@NonNull Drawable drawable) { + if (callback != null) { + callback.invalidateDrawable(drawable); + } + view.invalidate(); + } + + @Override + public void scheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable, long l) { + if (callback != null) { + callback.scheduleDrawable(drawable, runnable, l); + } + } + + @Override + public void unscheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable) { + if (callback != null) { + callback.unscheduleDrawable(drawable, runnable); + } + } + }); + ((Animatable) resource).start(); + } + imageDrawable = resource; + view.invalidate(); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + } + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/Helper.java b/app/src/main/java/app/fedilab/android/helper/Helper.java index 8029b84a7..99be394d8 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -87,8 +87,10 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.PreferenceManager; import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.RequestOptions; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -119,7 +121,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -352,9 +354,9 @@ public class Helper { public static int counter = 1; static { - Map aMap = new HashMap<>(); - aMap.put(PatternType.MENTION, mentionPattern); + LinkedHashMap aMap = new LinkedHashMap<>(); aMap.put(PatternType.MENTION_LONG, mentionLongPattern); + aMap.put(PatternType.MENTION, mentionPattern); aMap.put(PatternType.TAG, hashtagPattern); aMap.put(PatternType.GROUP, groupPattern); patternHashMap = Collections.unmodifiableMap(aMap); @@ -1043,6 +1045,7 @@ public class Helper { } + /** * Load a profile picture for the account * @@ -1050,25 +1053,39 @@ public class Helper { * @param account - {@link Account} */ public static void loadPP(ImageView view, BaseAccount account) { + loadPP(view, account, false); + } + + /** + * Load a profile picture for the account + * + * @param view ImageView - the view where the image will be loaded + * @param account - {@link Account} + */ + public static void loadPP(ImageView view, BaseAccount account, boolean crop) { Context context = view.getContext(); SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); String targetedUrl = disableGif ? account.mastodon_account.avatar_static : account.mastodon_account.avatar; if (targetedUrl != null) { if (disableGif || (!targetedUrl.endsWith(".gif"))) { - Glide.with(view.getContext()) + RequestBuilder requestBuilder = Glide.with(view.getContext()) .asDrawable() .load(targetedUrl) - .thumbnail(0.1f) - .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))) - .into(view); + .thumbnail(0.1f); + if (crop) { + requestBuilder = requestBuilder.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))); + } + requestBuilder.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))).into(view); } else { - Glide.with(view.getContext()) + RequestBuilder requestBuilder = Glide.with(view.getContext()) .asGif() .load(targetedUrl) - .thumbnail(0.1f) - .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))) - .into(view); + .thumbnail(0.1f); + if (crop) { + requestBuilder = requestBuilder.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))); + } + requestBuilder.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))).into(view); } } else { Glide.with(view.getContext()) @@ -1722,4 +1739,13 @@ public class Helper { }).start(); } + + public static T getKeyByValue(Map map, E value) { + for (Map.Entry entry : map.entrySet()) { + if (Objects.equals(value, entry.getValue())) { + return entry.getKey(); + } + } + return null; + } } diff --git a/app/src/main/java/app/fedilab/android/helper/MyAppGlideModule.java b/app/src/main/java/app/fedilab/android/helper/MyAppGlideModule.java new file mode 100644 index 000000000..5b7b2b5fd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/MyAppGlideModule.java @@ -0,0 +1,12 @@ +package app.fedilab.android.helper; + +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; + +@GlideModule +public final class MyAppGlideModule extends AppGlideModule { + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java index 60b3b7c1d..5fb0e16e6 100644 --- a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java @@ -115,11 +115,6 @@ public class NotificationsHelper { if (notifications.notifications.size() > 0) { since_ids.put(slug, notifications.notifications.get(0).id); } - for (Notification notification : notifications.notifications) { - if (notification != null && notification.status != null) { - notification.status = SpannableHelper.convertStatus(context.getApplicationContext(), notification.status); - } - } } notifications.pagination = MastodonHelper.getPagination(notificationsResponse.headers()); } diff --git a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java index b9d1df8cf..c9c45c7b0 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -17,7 +17,6 @@ package app.fedilab.android.helper; import static app.fedilab.android.BaseMainActivity.currentAccount; import static app.fedilab.android.helper.Helper.USER_AGENT; -import static app.fedilab.android.helper.Helper.convertDpToPixel; import static app.fedilab.android.helper.Helper.urlPattern; import static app.fedilab.android.helper.ThemeHelper.linkColor; @@ -26,7 +25,6 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -38,7 +36,6 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.style.ClickableSpan; -import android.text.style.ImageSpan; import android.text.style.URLSpan; import android.util.Patterns; import android.view.LayoutInflater; @@ -50,14 +47,9 @@ import androidx.appcompat.app.AlertDialog; import androidx.preference.PreferenceManager; import com.bumptech.glide.Glide; -import com.bumptech.glide.request.FutureTarget; -import com.github.penfeizhou.animation.apng.APNGDrawable; -import com.github.penfeizhou.animation.apng.decode.APNGParser; -import com.github.penfeizhou.animation.gif.GifDrawable; -import com.github.penfeizhou.animation.gif.decode.GifParser; -import java.io.File; import java.io.IOException; +import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -65,7 +57,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -79,9 +70,7 @@ import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.api.Announcement; import app.fedilab.android.client.entities.api.Attachment; import app.fedilab.android.client.entities.api.Emoji; -import app.fedilab.android.client.entities.api.Field; import app.fedilab.android.client.entities.api.Mention; -import app.fedilab.android.client.entities.api.Poll; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.databinding.PopupLinksBinding; import es.dmoral.toasty.Toasty; @@ -90,125 +79,79 @@ public class SpannableHelper { public static final String CLICKABLE_SPAN = "CLICKABLE_SPAN"; + public static Spannable convert(Context context, String text, + Status status, Account account, Announcement announcement, + boolean convertHtml, + WeakReference viewWeakReference) { + - private static Spannable convert(@NonNull Context context, @NonNull Status status, String text) { - return convert(context, status, text, true); - } - /** - * Convert HTML content to text. Also, it handles click on link and transform emoji - * This needs to be run asynchronously - * - * @param context {@link Context} - * @param status {@link Status} - Status concerned by the spannable transformation - * @param text String - text to convert, it can be content, spoiler, poll items, etc. - * @param convertHtml boolean - text need to be converted in html first - * @return Spannable string - */ - private static Spannable convert(@NonNull Context context, @NonNull Status status, String text, boolean convertHtml) { SpannableString initialContent; if (text == null) { return null; } - Matcher matcherALink = Helper.aLink.matcher(text); - //We stock details - HashMap urlDetails = new HashMap<>(); - while (matcherALink.find()) { - String urlText = matcherALink.group(3); - String url = matcherALink.group(2); - if (urlText != null) { - urlText = urlText.substring(1); - } - if (url != null && urlText != null && !url.equals(urlText) && !urlText.contains(" mentionList = null; + List emojiList = null; + if (status != null) { + mentionList = status.mentions; + emojiList = status.emojis; + } else if (account != null) { + emojiList = account.emojis; + } else if (announcement != null) { + emojiList = announcement.emojis; } - if(convertHtml) { + HashMap urlDetails = new HashMap<>(); + if (convertHtml) { + Matcher matcherALink = Helper.aLink.matcher(text); + //We stock details + while (matcherALink.find()) { + String urlText = matcherALink.group(3); + String url = matcherALink.group(2); + if (urlText != null) { + urlText = urlText.substring(1); + } + if (url != null && urlText != null && !url.equals(urlText) && !urlText.contains("= Build.VERSION_CODES.N) initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); else initialContent = new SpannableString(Html.fromHtml(text)); + + content = new SpannableStringBuilder(initialContent); + URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); + for (URLSpan span : urls) { + content.removeSpan(span); + } + //Make tags, mentions, groups + interaction(context, content, mentionList); + //Make all links + linkify(context, content, urlDetails); } else { - initialContent = new SpannableString(text); + content = new SpannableStringBuilder(text); } - - SpannableStringBuilder content = new SpannableStringBuilder(initialContent); - URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); - for (URLSpan span : urls) { - content.removeSpan(span); - } - - //--- EMOJI ---- - SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); - List emojiList = status.reblog != null ? status.reblog.emojis : status.emojis; - //Will convert emoji if asked if (emojiList != null && emojiList.size() > 0) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean animate = !sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); for (Emoji emoji : emojiList) { - if (Helper.isValidContextForGlide(context)) { - FutureTarget futureTarget = Glide.with(context) - .asFile() - .load(disableGif ? emoji.static_url : emoji.url) - .submit(); - try { - File file = futureTarget.get(); - final String targetedEmoji = ":" + emoji.shortcode + ":"; - if (content.toString().contains(targetedEmoji)) { - //emojis can be used several times so we have to loop - for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { - final int endPosition = startPosition + targetedEmoji.length(); - if (endPosition <= content.toString().length() && endPosition >= startPosition) { - ImageSpan imageSpan; - if (APNGParser.isAPNG(file.getAbsolutePath())) { - APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath()); - try { - apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - apngDrawable.setVisible(true, true); - imageSpan = new ImageSpan(apngDrawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } else if (GifParser.isGif(file.getAbsolutePath())) { - GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath()); - try { - gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - gifDrawable.setVisible(true, true); - imageSpan = new ImageSpan(gifDrawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } else { - Drawable drawable = Drawable.createFromPath(file.getAbsolutePath()); - try { - drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - drawable.setVisible(true, true); - imageSpan = new ImageSpan(drawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } - } - } - } - } catch (ExecutionException | InterruptedException e) { - e.printStackTrace(); - } + Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) + .matcher(content); + while (matcher.find()) { + CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); + content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); + Glide.with(view) + .asDrawable() + .load(animate ? emoji.url : emoji.static_url) + .into(customEmoji.getTarget(animate)); } } } + return trimSpannable(new SpannableStringBuilder(content)); + } + private static void linkify(Context context, SpannableStringBuilder content, HashMap urlDetails) { //--- URLs ---- Matcher matcherLink = Patterns.WEB_URL.matcher(content); int offSetTruncate = 0; @@ -222,7 +165,20 @@ public class SpannableHelper { if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) { continue; } - final String url = content.toString().substring(matchStart, matchEnd); + String url_temp = content.toString().substring(matchStart, matchEnd).replace("…", ""); + if (urlDetails.containsValue(url_temp + "…")) { + String originalURL = Helper.getKeyByValue(urlDetails, url_temp + "…"); + if (originalURL != null) { + content.replace(matchStart, matchEnd, originalURL); + offSetTruncate += (originalURL.length() - (url_temp.length() + 1)); + matchEnd = matchStart + originalURL.length(); + url_temp = originalURL; + } + } + final String url = url_temp; + /* if (!url.startsWith("http")) { + continue; + }*/ String newURL = Helper.transformURL(context, url); //If URL has been transformed if (newURL.compareTo(url) != 0) { @@ -230,10 +186,12 @@ public class SpannableHelper { offSetTruncate += (newURL.length() - url.length()); matchEnd = matchStart + newURL.length(); //The transformed URL was in the list of URLs having a different names + if (urlDetails.containsKey(url)) { urlDetails.put(newURL, urlDetails.get(url)); } } + //Truncate URL if needed //TODO: add an option to disable truncated URLs String urlText = newURL; @@ -252,6 +210,7 @@ public class SpannableHelper { } } + if (matchEnd <= content.length() && matchEnd >= matchStart) { content.setSpan(new LongClickableSpan() { @Override @@ -401,14 +360,12 @@ public class SpannableHelper { @Override public void federatedAccount(Account account) { - } }); } else {//It's an account - CrossActionHelper.fetchRemoteAccount(context, currentAccount, status.account, new CrossActionHelper.Callback() { + CrossActionHelper.fetchRemoteAccount(context, currentAccount, matcherLink.group(2) + "@" + matcherLink.group(1), new CrossActionHelper.Callback() { @Override public void federatedStatus(Status status) { - } @Override @@ -437,21 +394,27 @@ public class SpannableHelper { }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } } + } + private static void interaction(Context context, Spannable content, List mentions) { // --- For all patterns defined in Helper class --- for (Map.Entry entry : Helper.patternHashMap.entrySet()) { Helper.PatternType patternType = entry.getKey(); Pattern pattern = entry.getValue(); Matcher matcher = pattern.matcher(content); - while (matcher.find()) { + if (pattern == Helper.mentionPattern && mentions == null) { + continue; + } else if (pattern == Helper.mentionLongPattern && mentions == null) { + continue; + } + while (matcher.find()) { int matchStart = matcher.start(); int matchEnd = matcher.end(); String word = content.toString().substring(matchStart, matchEnd); if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class); content.removeSpan(span); - content.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull View textView) { @@ -472,25 +435,28 @@ public class SpannableHelper { b = new Bundle(); Mention targetedMention = null; HashMap countUsername = new HashMap<>(); - for (Mention mention : status.mentions) { - Integer count = countUsername.get(mention.username); - if (count == null) { - count = 0; + + if (mentions != null) { + for (Mention mention : mentions) { + Integer count = countUsername.get(mention.username); + if (count == null) { + count = 0; + } + if (countUsername.containsKey(mention.username)) { + countUsername.put(mention.username, count + 1); + } else { + countUsername.put(mention.username, 1); + } } - if (countUsername.containsKey(mention.username)) { - countUsername.put(mention.username, count + 1); - } else { - countUsername.put(mention.username, 1); - } - } - for (Mention mention : status.mentions) { - Integer count = countUsername.get(mention.username); - if (count == null) { - count = 0; - } - if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) { - targetedMention = mention; - break; + for (Mention mention : mentions) { + Integer count = countUsername.get(mention.username); + if (count == null) { + count = 0; + } + if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) { + targetedMention = mention; + break; + } } } if (targetedMention != null) { @@ -498,6 +464,7 @@ public class SpannableHelper { } else { b.putString(Helper.ARG_MENTION, word.trim()); } + intent.putExtras(b); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); @@ -506,10 +473,12 @@ public class SpannableHelper { intent = new Intent(context, ProfileActivity.class); b = new Bundle(); targetedMention = null; - for (Mention mention : status.mentions) { - if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) { - targetedMention = mention; - break; + if (mentions != null) { + for (Mention mention : mentions) { + if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) { + targetedMention = mention; + break; + } } } if (targetedMention != null) { @@ -534,6 +503,17 @@ public class SpannableHelper { } } } + } + + /** + * Convert HTML content to text. Also, it handles click on link + * This needs to be run asynchronously + * + * @param status {@link Status} - Status concerned by the spannable transformation + * @param content String - text to convert, it can be content, spoiler, poll items, etc. + * @return Spannable string + */ + private static void convertOuich(@NonNull Status status, SpannableStringBuilder content) { Matcher matcher = Helper.ouichesPattern.matcher(content); while (matcher.find()) { @@ -560,431 +540,28 @@ public class SpannableHelper { status.media_attachments.add(attachment); } } - return trimSpannable(new SpannableStringBuilder(content)); } - /** - * Convert HTML content to text. Also, it handles click on link and transform emoji + * Convert HTML content to text. Also, it handles click on link * This needs to be run asynchronously * - * @param context {@link Context} - * @param announcement {@link Announcement} - Announcement concerned by the spannable transformation - * @param text String - text to convert, it can be content, spoiler, poll items, etc. + * @param text String - text to convert, it can be content, spoiler, poll items, etc. * @return Spannable string */ - private static Spannable convert(@NonNull Context context, @NonNull Announcement announcement, String text) { + public static Spannable convertNitter(String text) { SpannableString initialContent; if (text == null) { return null; } - Matcher matcherALink = Helper.aLink.matcher(text); - //We stock details - HashMap urlDetails = new HashMap<>(); - while (matcherALink.find()) { - String urlText = matcherALink.group(3); - String url = matcherALink.group(2); - if (urlText != null) { - urlText = urlText.substring(1); - } - if (url != null && urlText != null && !url.equals(urlText) && !urlText.contains("= Build.VERSION_CODES.N) initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); else initialContent = new SpannableString(Html.fromHtml(text)); - - SpannableStringBuilder content = new SpannableStringBuilder(initialContent); - URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); - for (URLSpan span : urls) { - content.removeSpan(span); - } - - //--- EMOJI ---- - SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); - List emojiList = announcement.emojis; - //Will convert emoji if asked - if (emojiList != null && emojiList.size() > 0) { - for (Emoji emoji : emojiList) { - if (Helper.isValidContextForGlide(context)) { - FutureTarget futureTarget = Glide.with(context) - .asFile() - .load(disableGif ? emoji.static_url : emoji.url) - .submit(); - try { - File file = futureTarget.get(); - final String targetedEmoji = ":" + emoji.shortcode + ":"; - if (content.toString().contains(targetedEmoji)) { - //emojis can be used several times so we have to loop - for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { - final int endPosition = startPosition + targetedEmoji.length(); - if (endPosition <= content.toString().length() && endPosition >= startPosition) { - ImageSpan imageSpan; - if (APNGParser.isAPNG(file.getAbsolutePath())) { - APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath()); - try { - apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - apngDrawable.setVisible(true, true); - imageSpan = new ImageSpan(apngDrawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } else if (GifParser.isGif(file.getAbsolutePath())) { - GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath()); - try { - gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - gifDrawable.setVisible(true, true); - imageSpan = new ImageSpan(gifDrawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } else { - Drawable drawable = Drawable.createFromPath(file.getAbsolutePath()); - try { - drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - drawable.setVisible(true, true); - imageSpan = new ImageSpan(drawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } - } - } - } - } catch (ExecutionException | InterruptedException e) { - e.printStackTrace(); - } - } - } - } - - //--- URLs ---- - Matcher matcherLink = Patterns.WEB_URL.matcher(content); - int offSetTruncate = 0; - while (matcherLink.find()) { - int matchStart = matcherLink.start() - offSetTruncate; - int matchEnd = matchStart + matcherLink.group().length(); - if (matchEnd > content.toString().length()) { - matchEnd = content.toString().length(); - } - - if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) { - continue; - } - final String url = content.toString().substring(matchStart, matchEnd); - String newURL = Helper.transformURL(context, url); - //If URL has been transformed - if (newURL.compareTo(url) != 0) { - content.replace(matchStart, matchEnd, newURL); - offSetTruncate += (newURL.length() - url.length()); - matchEnd = matchStart + newURL.length(); - //The transformed URL was in the list of URLs having a different names - if (urlDetails.containsKey(url)) { - urlDetails.put(newURL, urlDetails.get(url)); - } - } - //Truncate URL if needed - //TODO: add an option to disable truncated URLs - String urlText = newURL; - if (newURL.length() > 30 && !urlDetails.containsKey(urlText)) { - urlText = urlText.substring(0, 30); - urlText += "…"; - content.replace(matchStart, matchEnd, urlText); - matchEnd = matchStart + 31; - offSetTruncate += (newURL.length() - urlText.length()); - } else if (urlDetails.containsKey(urlText) && urlDetails.get(urlText) != null) { - urlText = urlDetails.get(urlText); - if (urlText != null) { - content.replace(matchStart, matchEnd, urlText); - matchEnd = matchStart + urlText.length(); - offSetTruncate += (newURL.length() - urlText.length()); - } - } - - if (matchEnd <= content.length() && matchEnd >= matchStart) { - content.setSpan(new LongClickableSpan() { - @Override - public void onLongClick(View view) { - Context mContext = view.getContext(); - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); - PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context)); - dialogBuilder.setView(popupLinksBinding.getRoot()); - AlertDialog alertDialog = dialogBuilder.create(); - alertDialog.show(); - - popupLinksBinding.displayFullLink.setOnClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); - builder.setMessage(url); - builder.setTitle(context.getString(R.string.display_full_link)); - builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) - .show(); - alertDialog.dismiss(); - }); - popupLinksBinding.shareLink.setOnClickListener(v -> { - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); - sendIntent.putExtra(Intent.EXTRA_TEXT, url); - sendIntent.setType("text/plain"); - sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with)); - intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intentChooser); - alertDialog.dismiss(); - }); - - popupLinksBinding.openOtherApp.setOnClickListener(v -> { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - context.startActivity(intent); - } catch (Exception e) { - Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); - } - alertDialog.dismiss(); - }); - - popupLinksBinding.copyLink.setOnClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, url); - if (clipboard != null) { - clipboard.setPrimaryClip(clip); - Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); - } - alertDialog.dismiss(); - }); - - popupLinksBinding.checkRedirect.setOnClickListener(v -> { - try { - - URL finalUrlCheck = new URL(url); - new Thread(() -> { - try { - String redirect = null; - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection(); - httpsURLConnection.setConnectTimeout(10 * 1000); - httpsURLConnection.setRequestProperty("http.keepAlive", "false"); - httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT); - httpsURLConnection.setRequestMethod("HEAD"); - if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) { - Map> map = httpsURLConnection.getHeaderFields(); - for (Map.Entry> entry : map.entrySet()) { - if (entry.toString().toLowerCase().startsWith("location")) { - Matcher matcher = urlPattern.matcher(entry.toString()); - if (matcher.find()) { - redirect = matcher.group(1); - } - } - } - } - httpsURLConnection.getInputStream().close(); - if (redirect != null && redirect.compareTo(url) != 0) { - URL redirectURL = new URL(redirect); - String host = redirectURL.getHost(); - String protocol = redirectURL.getProtocol(); - if (protocol == null || host == null) { - redirect = null; - } - } - Handler mainHandler = new Handler(context.getMainLooper()); - String finalRedirect = redirect; - Runnable myRunnable = () -> { - AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle()); - if (finalRedirect != null) { - builder1.setMessage(context.getString(R.string.redirect_detected, url, finalRedirect)); - builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> { - ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect); - if (clipboard1 != null) { - clipboard1.setPrimaryClip(clip1); - Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); - } - dialog.dismiss(); - }); - builder1.setNeutralButton(R.string.share_link, (dialog, which) -> { - Intent sendIntent1 = new Intent(Intent.ACTION_SEND); - sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); - sendIntent1.putExtra(Intent.EXTRA_TEXT, url); - sendIntent1.setType("text/plain"); - context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with))); - dialog.dismiss(); - }); - } else { - builder1.setMessage(R.string.no_redirect); - } - builder1.setTitle(context.getString(R.string.check_redirect)); - builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) - .show(); - - }; - mainHandler.post(myRunnable); - } catch (IOException e) { - e.printStackTrace(); - } - - }).start(); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - - alertDialog.dismiss(); - }); - - } - - @Override - public void onClick(@NonNull View textView) { - textView.setTag(CLICKABLE_SPAN); - Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$"); - Matcher matcherLink = link.matcher(url); - if (matcherLink.find() && !url.contains("medium.com")) { - if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot - CrossActionHelper.fetchRemoteStatus(context, currentAccount, url, new CrossActionHelper.Callback() { - @Override - public void federatedStatus(Status status) { - Intent intent = new Intent(context, ContextActivity.class); - intent.putExtra(Helper.ARG_STATUS, status); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - @Override - public void federatedAccount(Account account) { - - } - }); - } - } else { - Helper.openBrowser(context, newURL); - } - - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - ds.setColor(linkColor); - } - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } - } - - // --- For all patterns defined in Helper class --- - for (Map.Entry entry : Helper.patternHashMap.entrySet()) { - Helper.PatternType patternType = entry.getKey(); - Pattern pattern = entry.getValue(); - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - - int matchStart = matcher.start(); - int matchEnd = matcher.end(); - String word = content.toString().substring(matchStart, matchEnd); - if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { - URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class); - content.removeSpan(span); - - content.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View textView) { - textView.setTag(CLICKABLE_SPAN); - switch (patternType) { - case TAG: - Intent intent = new Intent(context, HashTagActivity.class); - Bundle b = new Bundle(); - b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim()); - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - case GROUP: - break; - case MENTION: - intent = new Intent(context, ProfileActivity.class); - b = new Bundle(); - Mention targetedMention = null; - HashMap countUsername = new HashMap<>(); - for (Mention mention : announcement.mentions) { - Integer count = countUsername.get(mention.username); - if (count == null) { - count = 0; - } - if (countUsername.containsKey(mention.username)) { - countUsername.put(mention.username, count + 1); - } else { - countUsername.put(mention.username, 1); - } - } - for (Mention mention : announcement.mentions) { - Integer count = countUsername.get(mention.username); - if (count == null) { - count = 0; - } - if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) { - targetedMention = mention; - break; - } - } - if (targetedMention != null) { - b.putString(Helper.ARG_USER_ID, targetedMention.id); - } else { - b.putString(Helper.ARG_MENTION, word.trim()); - } - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - case MENTION_LONG: - intent = new Intent(context, ProfileActivity.class); - b = new Bundle(); - targetedMention = null; - for (Mention mention : announcement.mentions) { - if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) { - targetedMention = mention; - break; - } - } - if (targetedMention != null) { - b.putString(Helper.ARG_USER_ID, targetedMention.id); - } else { - b.putString(Helper.ARG_MENTION, word.trim()); - } - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - } - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - ds.setColor(linkColor); - } - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } - } - } - return trimSpannable(new SpannableStringBuilder(content)); + return initialContent; } + /** * Remove extra carriage returns at the bottom due to

tags in toots * @@ -1009,317 +586,6 @@ public class SpannableHelper { return spannable.delete(0, trimStart).delete(spannable.length() - trimEnd, spannable.length()); } - public static List convertStatus(Context context, List statuses) { - if (statuses != null) { - for (Status status : statuses) { - convertStatus(context, status); - } - } - return statuses; - } - - public static List convertNitterStatus(Context context, List statuses) { - if (statuses != null) { - for (Status status : statuses) { - convertNitterStatus(context, status); - } - } - return statuses; - } - - public static Status convertNitterStatus(Context context, Status status) { - if (status != null) { - status.span_content = SpannableHelper.convertNitter(context, status.content); - } - return status; - } - - /** - * Convert HTML content to text. Also, it handles click on link and transform emoji - * This needs to be run asynchronously - * - * @param context {@link Context} - * @param text String - text to convert, it can be content, spoiler, poll items, etc. - * @return Spannable string - */ - private static Spannable convertNitter(@NonNull Context context, String text) { - SpannableString initialContent; - if (text == null) { - return null; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); - else - initialContent = new SpannableString(Html.fromHtml(text)); - return initialContent; - } - - public static List convertAnnouncement(Context context, List announcements) { - if (announcements != null) { - for (Announcement announcement : announcements) { - convertAnnouncement(context, announcement); - } - } - return announcements; - } - - - public static Announcement convertAnnouncement(Context context, Announcement announcement) { - if (announcement != null) { - announcement.span_content = SpannableHelper.convert(context, announcement, announcement.content); - } - return announcement; - } - - public static Status convertStatus(Context context, Status status) { - if (status != null) { - status.span_content = SpannableHelper.convert(context, status, status.content); - status.span_spoiler_text = SpannableHelper.convert(context, status, status.spoiler_text); - if (status.translationContent != null) { - status.span_translate = SpannableHelper.convert(context, status, status.translationContent); - } - if (status.account == null) { - return status; - } - status.account.span_display_name = SpannableHelper.convertA(context, status.account, status.account.display_name, true); - if (status.poll != null) { - for (Poll.PollItem pollItem : status.poll.options) { - pollItem.span_title = SpannableHelper.convert(context, status, pollItem.title, false); - } - } - if (status.reblog != null) { - status.reblog.span_content = SpannableHelper.convert(context, status, status.reblog.content); - if (status.reblog.translationContent != null) { - status.reblog.span_translate = SpannableHelper.convert(context, status, status.reblog.translationContent); - } - status.reblog.span_spoiler_text = SpannableHelper.convert(context, status, status.reblog.spoiler_text); - status.reblog.account.span_display_name = SpannableHelper.convertA(context, status.reblog.account, status.reblog.account.display_name, true); - if (status.reblog.poll != null) { - for (Poll.PollItem pollItem : status.reblog.poll.options) { - pollItem.span_title = SpannableHelper.convert(context, status, pollItem.title, false); - } - } - } - } - return status; - } - - - public static List convertAccounts(Context context, List accounts) { - if (accounts != null) { - for (Account account : accounts) { - convertAccount(context, account); - } - } - return accounts; - } - - public static Account convertAccount(Context context, Account account) { - if (account != null) { - account.span_display_name = SpannableHelper.convertA(context, account, account.display_name, true); - account.span_note = SpannableHelper.convertA(context, account, account.note, false); - if (account.fields != null && account.fields.size() > 0) { - List fields = new ArrayList<>(); - for (Field field : account.fields) { - field.value_span = SpannableHelper.convertA(context, account, field.value, false); - fields.add(field); - } - account.fields = fields; - } - } - return account; - } - - - /** - * Convert HTML content to text. Also, it handles click on link and transform emoji - * This needs to be run asynchronously - * - * @param context {@link Context} - * @param account {@link Account} - Account concerned by the spannable transformation - * @param text String - text to convert, it can be display name or bio - * @return Spannable string - */ - private static Spannable convertA(@NonNull Context context, @NonNull Account account, String text, boolean limitedToDisplayName) { - SpannableString initialContent; - if (text == null) { - return null; - } - if (!limitedToDisplayName) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); - else - initialContent = new SpannableString(Html.fromHtml(text)); - } else { - initialContent = new SpannableString(text); - } - SpannableStringBuilder content = new SpannableStringBuilder(initialContent); - URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); - for (URLSpan span : urls) - content.removeSpan(span); - //--- EMOJI ---- - SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); - //Will convert emoji if asked - if (account.emojis != null && account.emojis.size() > 0) { - for (Emoji emoji : account.emojis) { - if (Helper.isValidContextForGlide(context)) { - FutureTarget futureTarget = Glide.with(context) - .asFile() - .load(disableGif ? emoji.static_url : emoji.url) - .submit(); - try { - File file = futureTarget.get(); - final String targetedEmoji = ":" + emoji.shortcode + ":"; - if (content.toString().contains(targetedEmoji)) { - //emojis can be used several times so we have to loop - for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { - final int endPosition = startPosition + targetedEmoji.length(); - if (endPosition <= content.toString().length() && endPosition >= startPosition) { - ImageSpan imageSpan; - if (APNGParser.isAPNG(file.getAbsolutePath())) { - APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath()); - try { - apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - apngDrawable.setVisible(true, true); - imageSpan = new ImageSpan(apngDrawable); - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - - } catch (Exception ignored) { - } - } else if (GifParser.isGif(file.getAbsolutePath())) { - GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath()); - try { - gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - gifDrawable.setVisible(true, true); - imageSpan = new ImageSpan(gifDrawable); - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } catch (Exception ignored) { - } - } else { - try { - Drawable drawable = Drawable.createFromPath(file.getAbsolutePath()); - drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); - drawable.setVisible(true, true); - imageSpan = new ImageSpan(drawable); - if (endPosition <= content.length()) { - content.setSpan( - imageSpan, startPosition, - endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - } - } catch (Exception ignored) { - } - } - } - } - } - } catch (ExecutionException | InterruptedException e) { - e.printStackTrace(); - } - } - } - } - if (limitedToDisplayName) { - return content; - } - //--- URLs ---- - Matcher matcherALink = Patterns.WEB_URL.matcher(content); - - int offSetTruncate = 0; - while (matcherALink.find()) { - int matchStart = matcherALink.start() - offSetTruncate; - int matchEnd = matchStart + matcherALink.group().length(); - //Get real URL - if (matcherALink.start(1) > matcherALink.end(1) || matcherALink.end() > content.length()) { - continue; - } - final String url = content.toString().substring(matchStart, matchEnd); - //Truncate URL if needed - //TODO: add an option to disable truncated URLs - String urlText = url; - if (url.length() > 30 && matchStart < matchEnd) { - urlText = urlText.substring(0, 30); - urlText += "…"; - content.replace(matchStart, matchEnd, urlText); - matchEnd = matcherALink.end() - (url.length() - urlText.length()); - offSetTruncate += (url.length() - urlText.length()); - } - if (!urlText.startsWith("http")) { - continue; - } - if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) - content.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View textView) { - textView.setTag(CLICKABLE_SPAN); - Helper.openBrowser(context, url); - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - ds.setColor(linkColor); - } - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } - - // --- For all patterns defined in Helper class --- - for (Map.Entry entry : Helper.patternHashMap.entrySet()) { - Helper.PatternType patternType = entry.getKey(); - Pattern pattern = entry.getValue(); - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - int matchStart = matcher.start(); - int matchEnd = matcher.end(); - if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { - URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class); - content.removeSpan(span); - String word = content.toString().substring(matchStart, matchEnd); - content.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View textView) { - textView.setTag(CLICKABLE_SPAN); - switch (patternType) { - case TAG: - Intent intent = new Intent(context, HashTagActivity.class); - Bundle b = new Bundle(); - b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim()); - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - case GROUP: - break; - case MENTION_LONG: - case MENTION: - intent = new Intent(context, ProfileActivity.class); - b = new Bundle(); - b.putString(Helper.ARG_MENTION, word.trim()); - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - } - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - ds.setColor(linkColor); - } - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } - } - } - return trimSpannable(new SpannableStringBuilder(content)); - } - /** * Makes the move to account clickable * diff --git a/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java index b37f899f4..766f8f18c 100644 --- a/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java @@ -107,6 +107,18 @@ public class TimelineHelper { Matcher m = p.matcher(content); if (m.find()) { statusesToRemove.add(status); + continue; + } + if (status.spoiler_text != null) { + String spoilerText; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + spoilerText = Html.fromHtml(status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); + else + spoilerText = Html.fromHtml(status.spoiler_text).toString(); + Matcher ms = p.matcher(spoilerText); + if (ms.find()) { + statusesToRemove.add(status); + } } } } else { @@ -118,6 +130,18 @@ public class TimelineHelper { content = Html.fromHtml(status.content).toString(); if (content.contains(filter.phrase)) { statusesToRemove.add(status); + continue; + } + + if (status.spoiler_text != null) { + String spoilerText; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + spoilerText = Html.fromHtml(status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); + else + spoilerText = Html.fromHtml(status.spoiler_text).toString(); + if (spoilerText.contains(filter.phrase)) { + statusesToRemove.add(status); + } } } } diff --git a/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java b/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java index 620941170..c9566e9c7 100644 --- a/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java +++ b/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java @@ -75,6 +75,7 @@ public class NotificationsWorker extends Worker { String instance = getInputData().getString(Helper.ARG_INSTANCE); String token = getInputData().getString(Helper.ARG_TOKEN); String statusDraftId = getInputData().getString(Helper.ARG_STATUS_DRAFT_ID); + String userId = getInputData().getString(Helper.ARG_USER_ID); StatusDraft statusDraft; try { statusDraft = new StatusDraft(getApplicationContext()).geStatusDraft(statusDraftId); @@ -82,6 +83,7 @@ public class NotificationsWorker extends Worker { intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); intent.putExtra(Helper.ARG_INSTANCE, instance); intent.putExtra(Helper.ARG_TOKEN, token); + intent.putExtra(Helper.ARG_USER_ID, userId); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { getApplicationContext().startForegroundService(intent); } else { diff --git a/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java b/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java index 5f1275901..5a6da3ba0 100644 --- a/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java +++ b/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java @@ -74,6 +74,7 @@ public class ScheduleThreadWorker extends Worker { Data outputData; String instance = getInputData().getString(Helper.ARG_INSTANCE); String token = getInputData().getString(Helper.ARG_TOKEN); + String userId = getInputData().getString(Helper.ARG_USER_ID); String statusDraftId = getInputData().getString(Helper.ARG_STATUS_DRAFT_ID); StatusDraft statusDraft; try { @@ -82,6 +83,7 @@ public class ScheduleThreadWorker extends Worker { intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); intent.putExtra(Helper.ARG_INSTANCE, instance); intent.putExtra(Helper.ARG_TOKEN, token); + intent.putExtra(Helper.ARG_USER_ID, userId); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { getApplicationContext().startForegroundService(intent); } else { diff --git a/app/src/main/java/app/fedilab/android/services/PostMessageService.java b/app/src/main/java/app/fedilab/android/services/PostMessageService.java index a4a73ab20..f622a3ae7 100644 --- a/app/src/main/java/app/fedilab/android/services/PostMessageService.java +++ b/app/src/main/java/app/fedilab/android/services/PostMessageService.java @@ -202,9 +202,10 @@ public class PostMessageService extends IntentService { if (error) { return; } + String language = sharedPreferences.getString(context.getString(R.string.SET_COMPOSE_LANGUAGE) + dataPost.userId + dataPost.instance, null); if (dataPost.scheduledDate == null) { statusCall = mastodonStatusesService.createStatus(null, dataPost.token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in, - poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), statuses.get(i).language); + poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), language); try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { @@ -339,11 +340,13 @@ public class PostMessageService extends IntentService { StatusDraft statusDraft = null; String token = null, instance = null; String scheduledDate = null; + String userId = null; if (intent != null && intent.getExtras() != null) { Bundle b = intent.getExtras(); statusDraft = (StatusDraft) b.getSerializable(Helper.ARG_STATUS_DRAFT); token = b.getString(Helper.ARG_TOKEN); instance = b.getString(Helper.ARG_INSTANCE); + userId = b.getString(Helper.ARG_USER_ID); scheduledDate = b.getString(Helper.ARG_SCHEDULED_DATE); } //Should not be null, but a simple security @@ -356,6 +359,7 @@ public class PostMessageService extends IntentService { DataPost dataPost = new DataPost(); dataPost.instance = instance; dataPost.token = token; + dataPost.userId = userId; dataPost.statusDraft = statusDraft; dataPost.scheduledDate = scheduledDate; dataPost.notificationBuilder = notificationBuilder; @@ -367,6 +371,7 @@ public class PostMessageService extends IntentService { static class DataPost { String instance; String token; + String userId; StatusDraft statusDraft; int messageToSend; int messageSent; diff --git a/app/src/main/java/app/fedilab/android/services/ThreadMessageService.java b/app/src/main/java/app/fedilab/android/services/ThreadMessageService.java index aba6d9777..a6fa0596a 100644 --- a/app/src/main/java/app/fedilab/android/services/ThreadMessageService.java +++ b/app/src/main/java/app/fedilab/android/services/ThreadMessageService.java @@ -22,9 +22,10 @@ import app.fedilab.android.client.entities.app.StatusDraft; public class ThreadMessageService { - public ThreadMessageService(Context context, String instance, String token, StatusDraft statusDraft, String scheduledDate) { + public ThreadMessageService(Context context, String instance, String userId, String token, StatusDraft statusDraft, String scheduledDate) { PostMessageService.DataPost dataPost = new PostMessageService.DataPost(); dataPost.instance = instance; + dataPost.userId = userId; dataPost.token = token; dataPost.scheduledDate = scheduledDate; dataPost.statusDraft = statusDraft; diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java index 6f6e053d3..872980879 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java @@ -37,6 +37,7 @@ import androidx.lifecycle.ViewModelStoreOwner; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; +import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; @@ -224,9 +225,15 @@ public class AccountAdapter extends RecyclerView.Adapter(accountViewHolder.binding.displayName)), + TextView.BufferType.SPANNABLE); accountViewHolder.binding.username.setText(String.format("@%s", account.acct)); - accountViewHolder.binding.bio.setText(account.span_note, TextView.BufferType.SPANNABLE); + accountViewHolder.binding.bio.setText( + account.getSpanNote(context, + new WeakReference<>(accountViewHolder.binding.bio)), + TextView.BufferType.SPANNABLE); } public int getCount() { diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java index 77accd35b..356d880cb 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java @@ -27,6 +27,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import androidx.recyclerview.widget.RecyclerView; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -78,7 +79,10 @@ public class AccountListAdapter extends RecyclerView.Adapter(holder.binding.displayName)), + TextView.BufferType.SPANNABLE); holder.binding.username.setText(String.format("@%s", account.acct)); if (searchList != null) { diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java index 4983cbda0..abf0c05f5 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java @@ -37,6 +37,7 @@ import com.vanniktech.emoji.EmojiManager; import com.vanniktech.emoji.EmojiPopup; import com.vanniktech.emoji.one.EmojiOneProvider; +import java.lang.ref.WeakReference; import java.util.List; import app.fedilab.android.BaseMainActivity; @@ -75,19 +76,23 @@ public class AnnouncementAdapter extends RecyclerView.Adapter 0) { ReactionAdapter reactionAdapter = new ReactionAdapter(announcement.id, announcement.reactions); - holder.binding.reactionsView.setAdapter(reactionAdapter); + holder.binding.layoutReactions.reactionsView.setAdapter(reactionAdapter); LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false); - holder.binding.reactionsView.setLayoutManager(layoutManager); + holder.binding.layoutReactions.reactionsView.setLayoutManager(layoutManager); } else { - holder.binding.reactionsView.setAdapter(null); + holder.binding.layoutReactions.reactionsView.setAdapter(null); } - holder.binding.content.setText(announcement.span_content, TextView.BufferType.SPANNABLE); + holder.binding.content.setText( + announcement.getSpanContent(context, + new WeakReference<>(holder.binding.content)), + TextView.BufferType.SPANNABLE); if (announcement.starts_at != null) { String dateIni; String dateEnd; @@ -104,11 +109,11 @@ public class AnnouncementAdapter extends RecyclerView.Adapter { + holder.binding.layoutReactions.statusEmoji.setOnClickListener(v -> { EmojiManager.install(new EmojiOneProvider()); - final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(holder.binding.statusEmoji).setOnEmojiPopupDismissListener(() -> { + final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(holder.binding.layoutReactions.statusEmoji).setOnEmojiPopupDismissListener(() -> { InputMethodManager imm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(holder.binding.statusEmoji.getWindowToken(), 0); + imm.hideSoftInputFromWindow(holder.binding.layoutReactions.statusEmoji.getWindowToken(), 0); }).setOnEmojiClickListener((emoji, imageView) -> { String emojiStr = imageView.getUnicode(); boolean alreadyAdded = false; @@ -138,10 +143,10 @@ public class AnnouncementAdapter extends RecyclerView.Adapter { + holder.binding.layoutReactions.statusAddCustomEmoji.setOnClickListener(v -> { final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); int paddingPixel = 15; float density = context.getResources().getDisplayMetrics().density; diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java index 4851b42a5..db2aba5e7 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java @@ -73,6 +73,7 @@ import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import java.io.File; +import java.lang.ref.WeakReference; import java.text.Normalizer; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -455,13 +456,18 @@ public class ComposeAdapter extends RecyclerView.Adapter attachmentList = statusList.get(position).media_attachments; if (attachmentList != null && attachmentList.size() > 0) { holder.binding.sensitiveMedia.setVisibility(View.VISIBLE); - if (currentAccount.mastodon_account.source != null) { - holder.binding.sensitiveMedia.setChecked(currentAccount.mastodon_account.source.sensitive); - statusList.get(position).sensitive = currentAccount.mastodon_account.source.sensitive; - } else { - statusList.get(position).sensitive = false; + if (!statusList.get(position).sensitive) { + if (currentAccount.mastodon_account.source != null) { + holder.binding.sensitiveMedia.setChecked(currentAccount.mastodon_account.source.sensitive); + statusList.get(position).sensitive = currentAccount.mastodon_account.source.sensitive; + } else { + statusList.get(position).sensitive = false; + } } - holder.binding.sensitiveMedia.setOnCheckedChangeListener((buttonView, isChecked) -> statusList.get(position).sensitive = isChecked); + + holder.binding.sensitiveMedia.setOnCheckedChangeListener((buttonView, isChecked) -> { + statusList.get(position).sensitive = isChecked; + }); int mediaPosition = 0; for (Attachment attachment : attachmentList) { ComposeAttachmentItemBinding composeAttachmentItemBinding = ComposeAttachmentItemBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); @@ -664,6 +670,7 @@ public class ComposeAdapter extends RecyclerView.Adapter emojisList = new ArrayList<>(); /** * Initialize text watcher for content writing * It will allow to complete autocomplete edit text while starting words with @, #, : etc. @@ -672,7 +679,6 @@ public class ComposeAdapter extends RecyclerView.Adapter[] emojis = new List[]{null}; String pattern = "(.|\\s)*(@[\\w_-]+@[a-z0-9.\\-]+|@[\\w_-]+)"; final Pattern mentionPattern = Pattern.compile(pattern); @@ -958,13 +964,13 @@ public class ComposeAdapter extends RecyclerView.Adapter { List emojisToDisplay = new ArrayList<>(); try { - if (emojis[0] == null) { - emojis[0] = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); + if (emojisList == null || emojisList.size() == 0) { + emojisList = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); } - if (emojis[0] == null) { + if (emojis == null) { return; } - for (Emoji emoji : emojis[0]) { + for (Emoji emoji : emojisList) { if (shortcode != null && emoji.shortcode.contains(shortcode)) { emojisToDisplay.add(emoji); if (emojisToDisplay.size() >= 10) { @@ -983,7 +989,7 @@ public class ComposeAdapter extends RecyclerView.Adapter 0) { final String search = searchA[searchA.length - 1]; holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { - String shortcodeSelected = emojis[0].get(position).shortcode; + String shortcodeSelected = emojisToDisplay.get(position).shortcode; String deltaSearch = ""; int searchLength = searchDeep; if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position @@ -1032,21 +1038,40 @@ public class ComposeAdapter extends RecyclerView.Adapter(holder.binding.statusContent)), + TextView.BufferType.SPANNABLE); MastodonHelper.loadPPMastodon(holder.binding.avatar, status.account); - holder.binding.displayName.setText(status.account.span_display_name, TextView.BufferType.SPANNABLE); + holder.binding.displayName.setText( + status.account.getSpanDisplayName(context, + new WeakReference<>(holder.binding.displayName)), + TextView.BufferType.SPANNABLE); holder.binding.username.setText(String.format("@%s", status.account.acct)); if (status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { - holder.binding.spoiler.setVisibility(View.VISIBLE); - holder.binding.spoiler.setText(status.span_spoiler_text, TextView.BufferType.SPANNABLE); + holder.binding.spoiler.setText( + status.getSpanSpoiler(context, + new WeakReference<>(holder.binding.spoiler)), + TextView.BufferType.SPANNABLE); } else { holder.binding.spoiler.setVisibility(View.GONE); holder.binding.spoiler.setText(null); } + if (theme_statuses_color != -1) { + holder.binding.cardviewContainer.setBackgroundColor(theme_statuses_color); + } else { + holder.binding.cardviewContainer.setBackgroundColor(ContextCompat.getColor(context, R.color.cyanea_primary_dark_reference)); + } } else if (getItemViewType(position) == TYPE_COMPOSE) { Status statusDraft = statusList.get(position); @@ -1058,7 +1083,11 @@ public class ComposeAdapter extends RecyclerView.Adapter { if (instanceInfo.configuration.media_attachments.supported_mime_types != null) { if (instanceInfo.getMimeTypeAudio().size() == 0) { diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java index c074b16a3..94e1d748e 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java @@ -36,6 +36,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Timer; @@ -137,14 +138,20 @@ public class ConversationAdapter extends RecyclerView.Adapter(holder.binding.spoiler)), + TextView.BufferType.SPANNABLE); } else { holder.binding.spoiler.setVisibility(View.GONE); holder.binding.spoilerExpand.setVisibility(View.GONE); holder.binding.spoiler.setText(null); } //--- MAIN CONTENT --- - holder.binding.statusContent.setText(conversation.last_status.span_content, TextView.BufferType.SPANNABLE); + holder.binding.statusContent.setText( + conversation.last_status.getSpanContent(context, + new WeakReference<>(holder.binding.statusContent)), + TextView.BufferType.SPANNABLE); //--- DATE --- holder.binding.lastMessageDate.setText(Helper.dateDiff(context, conversation.last_status.created_at)); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java index 785c38368..7a3819ca3 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java @@ -16,26 +16,16 @@ package app.fedilab.android.ui.drawer; import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.transition.Transition; -import com.github.penfeizhou.animation.apng.APNGDrawable; -import com.github.penfeizhou.animation.apng.decode.APNGParser; -import com.github.penfeizhou.animation.gif.GifDrawable; -import com.github.penfeizhou.animation.gif.decode.GifParser; -import java.io.File; import java.util.List; import app.fedilab.android.R; @@ -52,7 +42,7 @@ public class EmojiAdapter extends BaseAdapter { } public int getCount() { - return emojiList.size(); + return emojiList == null ? 0 : emojiList.size(); } public Emoji getItem(int position) { @@ -76,28 +66,8 @@ public class EmojiAdapter extends BaseAdapter { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(holder.view.getContext()); boolean disableAnimatedEmoji = sharedpreferences.getBoolean(parent.getContext().getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); Glide.with(holder.binding.imgCustomEmoji.getContext()) - .asFile() .load(!disableAnimatedEmoji ? emoji.url : emoji.static_url) - .into(new CustomTarget() { - @Override - public void onResourceReady(@NonNull File resource, @Nullable Transition transition) { - if (APNGParser.isAPNG(resource.getAbsolutePath())) { - APNGDrawable apngDrawable = APNGDrawable.fromFile(resource.getAbsolutePath()); - holder.binding.imgCustomEmoji.setImageDrawable(apngDrawable); - } else if (GifParser.isGif(resource.getAbsolutePath())) { - GifDrawable gifDrawable = GifDrawable.fromFile(resource.getAbsolutePath()); - holder.binding.imgCustomEmoji.setImageDrawable(gifDrawable); - } else { - Drawable drawable = Drawable.createFromPath(resource.getAbsolutePath()); - holder.binding.imgCustomEmoji.setImageDrawable(drawable); - } - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - - } - }); + .into(holder.binding.imgCustomEmoji); return holder.view; } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java index df8bbe270..b4b39fc10 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java @@ -16,9 +16,7 @@ package app.fedilab.android.ui.drawer; import android.content.Context; -import android.text.Spannable; import android.text.method.LinkMovementMethod; -import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.TextView; @@ -27,9 +25,11 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; +import java.lang.ref.WeakReference; import java.util.List; import app.fedilab.android.R; +import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.api.Field; import app.fedilab.android.databinding.DrawerFieldBinding; @@ -38,6 +38,7 @@ public class FieldAdapter extends RecyclerView.Adapter fields; private Context context; + private Account account; public FieldAdapter(List fields) { this.fields = fields; @@ -66,9 +67,11 @@ public class FieldAdapter extends RecyclerView.Adapter(holder.binding.value)), + TextView.BufferType.SPANNABLE); holder.binding.value.setMovementMethod(LinkMovementMethod.getInstance()); holder.binding.label.setText(field.name); } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java index 7a121691c..dc354fd58 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java @@ -24,6 +24,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.app.ActivityOptionsCompat; @@ -31,6 +32,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import androidx.recyclerview.widget.RecyclerView; +import java.lang.ref.WeakReference; import java.util.List; import java.util.Locale; @@ -58,6 +60,7 @@ public class NotificationAdapter extends RecyclerView.Adapter - Not null when calling from notification adapter + * @param id String - Current status + * @return int - position in real time + */ + public static int getPositionAsync(List notificationList, String id) { + int position = 0; + if (notificationList != null) { + for (Notification notification : notificationList) { + if (notification.status != null && notification.status.id.compareTo(id) == 0) { + break; + } + position++; + } + } + return position; + } + @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { Notification notification = notificationList.get(position); if (getItemViewType(position) == TYPE_FOLLOW || getItemViewType(position) == TYPE_FOLLOW_REQUEST) { ViewHolderFollow holderFollow = (ViewHolderFollow) viewHolder; MastodonHelper.loadPPMastodon(holderFollow.binding.avatar, notification.account); - holderFollow.binding.displayName.setText(notification.account.display_name); + holderFollow.binding.displayName.setText( + notification.account.getSpanDisplayName(context, + new WeakReference<>(holderFollow.binding.displayName)), + TextView.BufferType.SPANNABLE); holderFollow.binding.username.setText(String.format("@%s", notification.account.acct)); if (getItemViewType(position) == TYPE_FOLLOW_REQUEST) { holderFollow.binding.rejectButton.setVisibility(View.VISIBLE); @@ -172,6 +200,8 @@ public class NotificationAdapter extends RecyclerView.Adapter(holderStatus.bindingNotification.status.displayName)), + TextView.BufferType.SPANNABLE); holderStatus.bindingNotification.status.username.setText(String.format("@%s", notification.account.acct)); holderStatus.bindingNotification.containerTransparent.setAlpha(.1f); if (notification.status != null && notification.status.visibility.equalsIgnoreCase("direct")) { @@ -255,13 +295,19 @@ public class NotificationAdapter extends RecyclerView.Adapter(holderStatus.bindingNotification.status.displayName)), + TextView.BufferType.SPANNABLE); + holderStatus.bindingNotification.status.displayName.setText(title, TextView.BufferType.SPANNABLE); holderStatus.bindingNotification.status.username.setText(String.format("@%s", notification.account.acct)); holderStatus.bindingNotification.status.actionButtons.setVisibility(View.GONE); } } } + public long getItemId(int position) { return position; } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java index 34c970a06..628f39d8e 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java @@ -15,6 +15,8 @@ package app.fedilab.android.ui.drawer; * see . */ +import static android.content.Context.INPUT_METHOD_SERVICE; +import static app.fedilab.android.BaseMainActivity.emojis; import static app.fedilab.android.BaseMainActivity.regex_home; import static app.fedilab.android.BaseMainActivity.regex_local; import static app.fedilab.android.BaseMainActivity.regex_public; @@ -35,10 +37,7 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.CountDownTimer; -import android.os.Handler; -import android.os.Looper; import android.text.Html; -import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; @@ -49,7 +48,9 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; import android.widget.CheckBox; +import android.widget.GridView; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RelativeLayout; @@ -69,9 +70,11 @@ import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; @@ -80,14 +83,16 @@ import com.github.stom79.mytransl.client.HttpsConnectionException; import com.github.stom79.mytransl.client.Results; import com.github.stom79.mytransl.translate.Params; import com.github.stom79.mytransl.translate.Translate; +import com.vanniktech.emoji.EmojiManager; +import com.vanniktech.emoji.EmojiPopup; +import com.vanniktech.emoji.one.EmojiOneProvider; import com.varunest.sparkbutton.SparkButton; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; -import java.util.Timer; -import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -96,6 +101,7 @@ import app.fedilab.android.R; import app.fedilab.android.activities.ComposeActivity; import app.fedilab.android.activities.ContextActivity; import app.fedilab.android.activities.CustomSharingActivity; +import app.fedilab.android.activities.MainActivity; import app.fedilab.android.activities.MediaActivity; import app.fedilab.android.activities.ProfileActivity; import app.fedilab.android.activities.ReportActivity; @@ -103,7 +109,9 @@ import app.fedilab.android.activities.StatusInfoActivity; import app.fedilab.android.client.entities.api.Attachment; import app.fedilab.android.client.entities.api.Notification; import app.fedilab.android.client.entities.api.Poll; +import app.fedilab.android.client.entities.api.Reaction; import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.app.Account; import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.client.entities.app.StatusDraft; import app.fedilab.android.client.entities.app.Timeline; @@ -127,6 +135,7 @@ import app.fedilab.android.ui.fragment.timeline.FragmentMastodonContext; import app.fedilab.android.viewmodel.mastodon.AccountsVM; import app.fedilab.android.viewmodel.mastodon.SearchVM; import app.fedilab.android.viewmodel.mastodon.StatusesVM; +import app.fedilab.android.viewmodel.pleroma.ActionsVM; import es.dmoral.toasty.Toasty; import jp.wasabeef.glide.transformations.BlurTransformation; @@ -253,6 +262,7 @@ public class StatusAdapter extends RecyclerView.Adapter statusToDeal.bookmarked = statusReturned.bookmarked; statusToDeal.reblogs_count = statusReturned.reblogs_count; statusToDeal.favourites_count = statusReturned.favourites_count; + //Update status in cache if not a remote instance if (!remote) { new Thread(() -> { @@ -315,10 +325,120 @@ public class StatusAdapter extends RecyclerView.Adapter boolean fullAttachement = sharedpreferences.getBoolean(context.getString(R.string.SET_FULL_PREVIEW), false); boolean displayBookmark = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_BOOKMARK), false); + if (MainActivity.currentAccount != null && MainActivity.currentAccount.api == Account.API.PLEROMA) { + holder.binding.layoutReactions.getRoot().setVisibility(View.VISIBLE); + if (status.pleroma != null && status.pleroma.emoji_reactions != null && status.pleroma.emoji_reactions.size() > 0) { + ReactionAdapter reactionAdapter = new ReactionAdapter(status.id, status.pleroma.emoji_reactions); + holder.binding.layoutReactions.reactionsView.setAdapter(reactionAdapter); + LinearLayoutManager layoutManager + = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false); + holder.binding.layoutReactions.reactionsView.setLayoutManager(layoutManager); + } else { + holder.binding.layoutReactions.reactionsView.setAdapter(null); + } + holder.binding.layoutReactions.statusEmoji.setOnClickListener(v -> { + EmojiManager.install(new EmojiOneProvider()); + final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(holder.binding.layoutReactions.statusEmoji).setOnEmojiPopupDismissListener(() -> { + InputMethodManager imm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(holder.binding.layoutReactions.statusEmoji.getWindowToken(), 0); + }).setOnEmojiClickListener((emoji, imageView) -> { + String emojiStr = imageView.getUnicode(); + boolean alreadyAdded = false; + for (Reaction reaction : status.pleroma.emoji_reactions) { + if (reaction.name.compareTo(emojiStr) == 0) { + alreadyAdded = true; + reaction.count = (reaction.count - 1); + if (reaction.count == 0) { + status.pleroma.emoji_reactions.remove(reaction); + } + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + break; + } + } + if (!alreadyAdded) { + Reaction reaction = new Reaction(); + reaction.me = true; + reaction.count = 1; + reaction.name = emojiStr; + status.pleroma.emoji_reactions.add(0, reaction); + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + } + ActionsVM actionVM = new ViewModelProvider((ViewModelStoreOwner) context).get(ActionsVM.class); + if (alreadyAdded) { + actionVM.removeReaction(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, emojiStr); + } else { + actionVM.addReaction(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, emojiStr); + } + }) + .build(holder.binding.layoutReactions.fakeEdittext); + emojiPopup.toggle(); + }); + holder.binding.layoutReactions.statusAddCustomEmoji.setOnClickListener(v -> { + + final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + int paddingPixel = 15; + float density = context.getResources().getDisplayMetrics().density; + int paddingDp = (int) (paddingPixel * density); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.setTitle(R.string.insert_emoji); + AlertDialog alertDialogEmoji = null; + if (emojis != null && emojis.size() > 0 && emojis.get(BaseMainActivity.currentInstance) != null) { + GridView gridView = new GridView(context); + gridView.setAdapter(new EmojiAdapter(emojis.get(BaseMainActivity.currentInstance))); + gridView.setNumColumns(5); + AlertDialog finalAlertDialogEmoji = alertDialogEmoji; + gridView.setOnItemClickListener((parent, view, index, id) -> { + String emojiStr = emojis.get(BaseMainActivity.currentInstance).get(index).shortcode; + String url = emojis.get(BaseMainActivity.currentInstance).get(index).url; + String static_url = emojis.get(BaseMainActivity.currentInstance).get(index).static_url; + boolean alreadyAdded = false; + for (Reaction reaction : status.pleroma.emoji_reactions) { + if (reaction.name.compareTo(emojiStr) == 0) { + alreadyAdded = true; + reaction.count = (reaction.count - 1); + if (reaction.count == 0) { + status.pleroma.emoji_reactions.remove(reaction); + } + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + break; + } + } + if (!alreadyAdded) { + Reaction reaction = new Reaction(); + reaction.me = true; + reaction.count = 1; + reaction.name = emojiStr; + reaction.url = url; + reaction.static_url = static_url; + status.pleroma.emoji_reactions.add(0, reaction); + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + } + ActionsVM actionsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(ActionsVM.class); + if (alreadyAdded) { + actionsVM.removeReaction(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, emojiStr); + } else { + actionsVM.addReaction(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, emojiStr); + } + if (finalAlertDialogEmoji != null) { + finalAlertDialogEmoji.dismiss(); + } + }); + gridView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); + builder.setView(gridView); + } else { + TextView textView = new TextView(context); + textView.setText(context.getString(R.string.no_emoji)); + textView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); + builder.setView(textView); + } + alertDialogEmoji = builder.show(); + }); + } + int truncate_toots_size = sharedpreferences.getInt(context.getString(R.string.SET_TRUNCATE_TOOTS_SIZE), 0); - boolean display_video_preview = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_VIDEO_PREVIEWS), true); - boolean isModerator = sharedpreferences.getBoolean(Helper.PREF_IS_MODERATOR, false); - boolean isAdmin = sharedpreferences.getBoolean(Helper.PREF_IS_ADMINISTRATOR, false); + // boolean display_video_preview = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_VIDEO_PREVIEWS), true); + // boolean isModerator = sharedpreferences.getBoolean(Helper.PREF_IS_MODERATOR, false); + // boolean isAdmin = sharedpreferences.getBoolean(Helper.PREF_IS_ADMINISTRATOR, false); int theme_icons_color = -1; int theme_statuses_color = -1; int theme_boost_header_color = -1; @@ -400,6 +520,9 @@ public class StatusAdapter extends RecyclerView.Adapter if (theme_statuses_color != -1) { holder.binding.cardviewContainer.setBackgroundColor(theme_statuses_color); holder.binding.translationLabel.setBackgroundColor(theme_statuses_color); + } else { + holder.binding.cardviewContainer.setBackgroundColor(ContextCompat.getColor(context, R.color.cyanea_primary_dark_reference)); + holder.binding.translationLabel.setBackgroundColor(ContextCompat.getColor(context, R.color.cyanea_primary_dark_reference)); } if (theme_boost_header_color != -1 && status.reblog != null) { holder.binding.statusBoosterInfo.setBackgroundColor(theme_boost_header_color); @@ -471,6 +594,35 @@ public class StatusAdapter extends RecyclerView.Adapter } else { holder.binding.card.setVisibility(View.GONE); } + if (!canBeFederated) { + holder.binding.actionShareContainer.setVisibility(View.VISIBLE); + holder.binding.actionShare.setOnClickListener(v -> { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); + String url; + if (statusToDeal.uri.startsWith("http")) + url = status.uri; + else + url = status.url; + String extra_text; + if (share_details) { + extra_text = statusToDeal.account.acct; + if (extra_text.split("@").length == 1) + extra_text = "@" + extra_text + "@" + BaseMainActivity.currentInstance; + else + extra_text = "@" + extra_text; + extra_text += " \uD83D\uDD17 " + url + "\r\n-\n"; + extra_text += statusToDeal.text; + } else { + extra_text = url; + } + sendIntent.putExtra(Intent.EXTRA_TEXT, extra_text); + sendIntent.setType("text/plain"); + context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.share_with))); + }); + } else { + holder.binding.actionShareContainer.setVisibility(View.GONE); + } if (minified || !canBeFederated) { holder.binding.actionButtons.setVisibility(View.GONE); } else { @@ -491,14 +643,12 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.actionButtonBookmark.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = statusList.get(0); statusesVM.bookmark(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.BOOKMARK_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.BOOKMARK_ACTION, statusToDeal, _status, true)); } else { Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); } @@ -506,16 +656,11 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (statusToDeal.bookmarked) { statusesVM.unBookmark(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNBOOKMARK_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNBOOKMARK_ACTION, statusToDeal, _status, false)); } else { ((SparkButton) v).playAnimation(); statusesVM.bookmark(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.BOOKMARK_ACTION, statusToDeal, _status, remote); - - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.BOOKMARK_ACTION, statusToDeal, _status, false)); } } }); @@ -523,7 +668,7 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.statusUserInfo.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); @@ -550,10 +695,10 @@ public class StatusAdapter extends RecyclerView.Adapter context.startActivity(intent, options.toBundle()); } }); - holder.binding.statusBoosterAvatar.setOnClickListener(v -> { + holder.binding.statusBoosterInfo.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); @@ -599,14 +744,12 @@ public class StatusAdapter extends RecyclerView.Adapter alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id, null) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, true)); } else { Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); } @@ -614,15 +757,11 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (statusToDeal.reblogged) { statusesVM.unReblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNREBLOG_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNREBLOG_ACTION, statusToDeal, _status, false)); } else { ((SparkButton) v).playAnimation(); statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, null) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, false)); } } dialog.dismiss(); @@ -633,14 +772,12 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id, null) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, true)); } else { Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); } @@ -648,15 +785,11 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (statusToDeal.reblogged) { statusesVM.unReblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNREBLOG_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNREBLOG_ACTION, statusToDeal, _status, false)); } else { ((SparkButton) v).playAnimation(); statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, null) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, statusToDeal, _status, false)); } } } @@ -681,14 +814,12 @@ public class StatusAdapter extends RecyclerView.Adapter alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, true)); } else { Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); } @@ -696,15 +827,11 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (status.favourited) { statusesVM.unFavourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNFAVOURITE_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNFAVOURITE_ACTION, statusToDeal, _status, false)); } else { ((SparkButton) v).playAnimation(); statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, false)); } } dialog.dismiss(); @@ -715,14 +842,12 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, true)); } else { Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); } @@ -730,15 +855,11 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (statusToDeal.favourited) { statusesVM.unFavourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNFAVOURITE_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.UNFAVOURITE_ACTION, statusToDeal, _status, false)); } else { ((SparkButton) v).playAnimation(); statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) - .observe((LifecycleOwner) context, _status -> { - manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, remote); - }); + .observe((LifecycleOwner) context, _status -> manageAction(context, adapter, statusList, notificationList, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, statusToDeal, _status, false)); } } } @@ -748,11 +869,11 @@ public class StatusAdapter extends RecyclerView.Adapter //--- ACCOUNT INFO --- MastodonHelper.loadPPMastodon(holder.binding.avatar, statusToDeal.account); - Spannable span_display_name = statusToDeal.account.span_display_name; - if (span_display_name == null || span_display_name.toString().trim().length() == 0) { - span_display_name = new SpannableString(statusToDeal.account.username); - } - holder.binding.displayName.setText(span_display_name, TextView.BufferType.SPANNABLE); + + holder.binding.displayName.setText( + statusToDeal.account.getSpanDisplayName(context, + new WeakReference<>(holder.binding.displayName)), + TextView.BufferType.SPANNABLE); if (theme_text_header_1_line != -1) { holder.binding.displayName.setTextColor(theme_text_header_1_line); } @@ -772,16 +893,27 @@ public class StatusAdapter extends RecyclerView.Adapter } if (statusToDeal.account.bot) { - holder.binding.botIcon.setVisibility(View.VISIBLE); } else { holder.binding.botIcon.setVisibility(View.GONE); } - if (statusToDeal.in_reply_to_id != null) { + if (statusToDeal.in_reply_to_id != null && timelineType != Timeline.TimeLineEnum.UNKNOWN) { holder.binding.replyIcon.setVisibility(View.VISIBLE); } else { holder.binding.replyIcon.setVisibility(View.GONE); } + int ressource = R.drawable.ic_baseline_public_24; + switch (status.visibility) { + case "unlisted": + ressource = R.drawable.ic_baseline_lock_open_24; + break; + case "private": + ressource = R.drawable.ic_baseline_lock_24; + break; + case "direct": + ressource = R.drawable.ic_baseline_mail_24; + break; + } if (status.isFocused) { holder.binding.statusInfo.setVisibility(View.VISIBLE); holder.binding.reblogsCount.setText(String.valueOf(status.reblogs_count)); @@ -789,23 +921,14 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.time.setText(Helper.longDateToString(status.created_at)); holder.binding.time.setVisibility(View.VISIBLE); holder.binding.dateShort.setVisibility(View.GONE); - int ressource = R.drawable.ic_baseline_public_24; - switch (status.visibility) { - case "unlisted": - ressource = R.drawable.ic_baseline_lock_open_24; - break; - case "private": - ressource = R.drawable.ic_baseline_lock_24; - break; - case "direct": - ressource = R.drawable.ic_baseline_mail_24; - break; - } holder.binding.visibility.setImageResource(ressource); holder.binding.dateShort.setVisibility(View.GONE); + holder.binding.visibilitySmall.setVisibility(View.GONE); } else { + holder.binding.visibilitySmall.setImageResource(ressource); holder.binding.statusInfo.setVisibility(View.GONE); holder.binding.dateShort.setVisibility(View.VISIBLE); + holder.binding.visibilitySmall.setVisibility(View.VISIBLE); holder.binding.dateShort.setText(Helper.dateDiff(context, status.created_at)); holder.binding.time.setVisibility(View.GONE); Helper.absoluteDateTimeReveal(context, holder.binding.dateShort, status.created_at); @@ -817,7 +940,10 @@ public class StatusAdapter extends RecyclerView.Adapter if (expand_cw || expand) { holder.binding.spoilerExpand.setVisibility(View.VISIBLE); holder.binding.spoiler.setVisibility(View.VISIBLE); - holder.binding.spoiler.setText(statusToDeal.span_spoiler_text, TextView.BufferType.SPANNABLE); + holder.binding.spoiler.setText( + statusToDeal.getSpanSpoiler(context, + new WeakReference<>(holder.binding.spoiler)), + TextView.BufferType.SPANNABLE); statusToDeal.isExpended = true; statusToDeal.isMediaDisplayed = true; } else { @@ -828,7 +954,11 @@ public class StatusAdapter extends RecyclerView.Adapter }); holder.binding.spoilerExpand.setVisibility(View.VISIBLE); holder.binding.spoiler.setVisibility(View.VISIBLE); - holder.binding.spoiler.setText(statusToDeal.span_spoiler_text, TextView.BufferType.SPANNABLE); + + holder.binding.spoiler.setText( + statusToDeal.getSpanSpoiler(context, + new WeakReference<>(holder.binding.spoiler)), + TextView.BufferType.SPANNABLE); } if (statusToDeal.isExpended) { holder.binding.spoilerExpand.setText(context.getString(R.string.hide_content)); @@ -844,13 +974,13 @@ public class StatusAdapter extends RecyclerView.Adapter //--- BOOSTER INFO --- if (status.reblog != null) { MastodonHelper.loadPPMastodon(holder.binding.statusBoosterAvatar, status.account); - Spannable span_display_name_boost = status.account.span_display_name; - if (span_display_name_boost == null || span_display_name_boost.toString().trim().length() == 0) { - span_display_name_boost = new SpannableString(status.account.username); - } - holder.binding.statusBoosterDisplayName.setText(span_display_name_boost, TextView.BufferType.SPANNABLE); + + holder.binding.statusBoosterDisplayName.setText( + status.account.getSpanDisplayName(context, + new WeakReference<>(holder.binding.statusBoosterDisplayName)), + TextView.BufferType.SPANNABLE); + holder.binding.statusBoosterInfo.setVisibility(View.VISIBLE); - holder.binding.boosterDivider.setVisibility(View.VISIBLE); if (theme_text_header_1_line != -1) { holder.binding.statusBoosterDisplayName.setTextColor(theme_text_header_1_line); } @@ -860,7 +990,6 @@ public class StatusAdapter extends RecyclerView.Adapter } } else { holder.binding.statusBoosterInfo.setVisibility(View.GONE); - holder.binding.boosterDivider.setVisibility(View.GONE); } //--- BOOST VISIBILITY --- switch (statusToDeal.visibility) { @@ -880,7 +1009,10 @@ public class StatusAdapter extends RecyclerView.Adapter break; } //--- MAIN CONTENT --- - holder.binding.statusContent.setText(statusToDeal.span_content, TextView.BufferType.SPANNABLE); + holder.binding.statusContent.setText( + statusToDeal.getSpanContent(context, + new WeakReference<>(holder.binding.statusContent)), + TextView.BufferType.SPANNABLE); if (truncate_toots_size > 0) { holder.binding.statusContent.setMaxLines(truncate_toots_size); holder.binding.statusContent.setEllipsize(TextUtils.TruncateAt.END); @@ -911,7 +1043,10 @@ public class StatusAdapter extends RecyclerView.Adapter } if (statusToDeal.translationContent != null) { holder.binding.containerTrans.setVisibility(View.VISIBLE); - holder.binding.statusContentTranslated.setText(statusToDeal.span_translate, TextView.BufferType.SPANNABLE); + holder.binding.statusContentTranslated.setText( + statusToDeal.getSpanTranslate(context, + new WeakReference<>(holder.binding.statusContentTranslated)), + TextView.BufferType.SPANNABLE); } else { holder.binding.containerTrans.setVisibility(View.GONE); } @@ -925,6 +1060,7 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.statusContent.setVisibility(View.GONE); holder.binding.mediaContainer.setVisibility(View.GONE); } + LayoutInflater inflater = ((Activity) context).getLayoutInflater(); //--- MEDIA ATTACHMENT --- if (statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { @@ -994,17 +1130,19 @@ public class StatusAdapter extends RecyclerView.Adapter if (!mediaObfuscated(statusToDeal) || expand_media) { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); - Glide.with(layoutMediaBinding.media.getContext()) + RequestBuilder requestBuilder = Glide.with(layoutMediaBinding.media.getContext()) .load(statusToDeal.media_attachments.get(0).preview_url) - .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) - .into(layoutMediaBinding.media); + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))); + if (!fullAttachement) { + requestBuilder = requestBuilder.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))); + } + requestBuilder.into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); Glide.with(layoutMediaBinding.media.getContext()) .load(statusToDeal.media_attachments.get(0).preview_url) .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) - .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } layoutMediaBinding.viewHide.setOnClickListener(v -> { @@ -1046,17 +1184,18 @@ public class StatusAdapter extends RecyclerView.Adapter lp.setMargins(0, 0, (int) Helper.convertDpToPixel(5, context), 0); if (!mediaObfuscated(statusToDeal) || expand_media) { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); - Glide.with(layoutMediaBinding.media.getContext()) + RequestBuilder requestBuilder = Glide.with(layoutMediaBinding.media.getContext()) .load(attachment.preview_url) - .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) - .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) - .into(layoutMediaBinding.media); + .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))); + if (!fullAttachement) { + requestBuilder = requestBuilder.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))); + } + requestBuilder.into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); Glide.with(layoutMediaBinding.media.getContext()) .load(attachment.preview_url) .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) - .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) .into(layoutMediaBinding.media); } @@ -1096,7 +1235,7 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.reblogInfo.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); @@ -1123,7 +1262,7 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.favouriteInfo.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); @@ -1174,7 +1313,10 @@ public class StatusAdapter extends RecyclerView.Adapter pollItemBinding.pollItemPercent.setTextColor(theme_text_color); pollItemBinding.pollItemText.setTextColor(theme_text_color); } - pollItemBinding.pollItemText.setText(pollItem.span_title, TextView.BufferType.SPANNABLE); + pollItemBinding.pollItemText.setText( + pollItem.getSpanTitle(context, statusToDeal, + new WeakReference<>(pollItemBinding.pollItemText)), + TextView.BufferType.SPANNABLE); pollItemBinding.pollItemValue.setProgress((int) value); if (pollItem.votes_count == greaterValue) { pollItemBinding.pollItemPercent.setTypeface(null, Typeface.BOLD); @@ -1202,7 +1344,10 @@ public class StatusAdapter extends RecyclerView.Adapter for (Poll.PollItem pollOption : statusToDeal.poll.options) { CheckBox cb = new CheckBox(context); cb.setButtonTintList(ThemeHelper.getButtonColorStateList(context)); - cb.setText(pollOption.span_title, TextView.BufferType.SPANNABLE); + cb.setText( + pollOption.getSpanTitle(context, statusToDeal, + new WeakReference<>(cb)), + TextView.BufferType.SPANNABLE); holder.binding.poll.multipleChoice.addView(cb); } holder.binding.poll.multipleChoice.setVisibility(View.VISIBLE); @@ -1213,7 +1358,11 @@ public class StatusAdapter extends RecyclerView.Adapter for (Poll.PollItem pollOption : statusToDeal.poll.options) { RadioButton rb = new RadioButton(context); rb.setButtonTintList(ThemeHelper.getButtonColorStateList(context)); - rb.setText(pollOption.span_title, TextView.BufferType.SPANNABLE); + rb.setText( + pollOption.getSpanTitle(context, statusToDeal, + new WeakReference<>(rb)), + TextView.BufferType.SPANNABLE); + holder.binding.poll.singleChoiceRadioGroup.addView(rb); } holder.binding.poll.singleChoiceRadioGroup.setVisibility(View.VISIBLE); @@ -1256,7 +1405,7 @@ public class StatusAdapter extends RecyclerView.Adapter //Vote on the poll if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); @@ -1322,9 +1471,7 @@ public class StatusAdapter extends RecyclerView.Adapter return false; }); if (!minified && canBeFederated) { - holder.binding.mainContainer.setOnClickListener(v -> { - holder.binding.statusContent.callOnClick(); - }); + holder.binding.mainContainer.setOnClickListener(v -> holder.binding.statusContent.callOnClick()); holder.binding.statusContent.setOnClickListener(v -> { if (status.isFocused || v.getTag() == SpannableHelper.CLICKABLE_SPAN) { if (v.getTag() == SpannableHelper.CLICKABLE_SPAN) { @@ -1340,7 +1487,7 @@ public class StatusAdapter extends RecyclerView.Adapter } else { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = results.statuses.get(0); @@ -1446,11 +1593,11 @@ public class StatusAdapter extends RecyclerView.Adapter builderInner.show(); } else if (itemId == R.id.action_schedule_boost) { MastodonHelper.scheduleBoost(context, MastodonHelper.ScheduleType.BOOST, statusToDeal, null, null); - } else if (itemId == R.id.action_admin) { - /* Intent intent = new Intent(context, AccountReportActivity.class); + } /*else if (itemId == R.id.action_admin) { + Intent intent = new Intent(context, AccountReportActivity.class); intent.putExtra(Helper.ARG_ACCOUNT, statusToDeal.account); - context.startActivity(intent);*/ - } else if (itemId == R.id.action_open_browser) { + context.startActivity(intent); + } */ else if (itemId == R.id.action_open_browser) { Helper.openBrowser(context, statusToDeal.url); } else if (itemId == R.id.action_remove) { AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); @@ -1540,12 +1687,7 @@ public class StatusAdapter extends RecyclerView.Adapter if (translate.getTranslatedContent() != null) { statusToDeal.translationShown = true; statusToDeal.translationContent = translate.getTranslatedContent(); - new Thread(() -> { - SpannableHelper.convertStatus(context.getApplicationContext(), statusToDeal); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); - mainHandler.post(myRunnable); - }).start(); + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); } else { Toasty.error(context, context.getString(R.string.toast_error_translate), Toast.LENGTH_LONG).show(); } @@ -1631,7 +1773,7 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.actionButtonReply.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.uri, null, "statuses", false, true, false, 0, null, null, 1) .observe((LifecycleOwner) context, results -> { if (results.statuses != null && results.statuses.size() > 0) { Status fetchedStatus = statusList.get(0); @@ -1657,6 +1799,7 @@ public class StatusAdapter extends RecyclerView.Adapter holder.bindingReport.checkbox.setChecked(status.isChecked); holder.bindingReport.checkbox.setOnClickListener(v -> status.isChecked = !status.isChecked); } + } private static boolean mediaObfuscated(Status status) { @@ -1723,6 +1866,34 @@ public class StatusAdapter extends RecyclerView.Adapter return position; } + /** + * Will manage the current position of the element in the adapter. Action is async, and position might have changed + * + * @param notificationList List - Not null when calling from notification adapter + * @param statusList ist statusList - Not null when calling from status adapter + * @param id String - Current status + * @return int - position in real time + */ + public static int getPositionAsync(List notificationList, List statusList, String id) { + int position = 0; + if (statusList != null) { + for (Status _status : statusList) { + if (id != null && ((_status.id != null && _status.id.compareTo(id) == 0) || (_status.reblog != null && _status.reblog.id != null && _status.reblog.id.compareTo(id) == 0))) { + break; + } + position++; + } + } else if (notificationList != null) { + for (Notification notification : notificationList) { + if (notification.status != null && notification.status.id.compareTo(id) == 0) { + break; + } + position++; + } + } + return position; + } + @Override public int getItemViewType(int position) { if (timelineType == Timeline.TimeLineEnum.ART) { @@ -1783,36 +1954,6 @@ public class StatusAdapter extends RecyclerView.Adapter StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); statusManagement(context, statusesVM, searchVM, holder, this, statusList, null, status, timelineType, minified, canBeFederated); - if (holder.timer != null) { - holder.timer.cancel(); - holder.timer = null; - } - if (holder.dateTimer != null) { - holder.dateTimer.cancel(); - holder.dateTimer = null; - } - if (status.emojis != null && status.emojis.size() > 0) { - holder.timer = new Timer(); - holder.timer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> holder.binding.statusContent.invalidate(); - mainHandler.post(myRunnable); - - } - }, 100, 100); - } - holder.dateTimer = new Timer(); - holder.dateTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> holder.binding.dateShort.setText(Helper.dateDiff(context, status.created_at)); - mainHandler.post(myRunnable); - - } - }, 100, 10000); } else if (viewHolder.getItemViewType() == STATUS_ART) { StatusViewHolder holder = (StatusViewHolder) viewHolder; MastodonHelper.loadPPMastodon(holder.bindingArt.artPp, status.account); @@ -1820,7 +1961,10 @@ public class StatusAdapter extends RecyclerView.Adapter .load(status.art_attachment.preview_url) .apply(new RequestOptions().transform(new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) .into(holder.bindingArt.artMedia); - holder.bindingArt.artAcct.setText(status.account.span_display_name, TextView.BufferType.SPANNABLE); + holder.bindingArt.artAcct.setText( + status.account.getSpanDisplayName(context, + new WeakReference<>(holder.bindingArt.artAcct)), + TextView.BufferType.SPANNABLE); holder.bindingArt.artUsername.setText(String.format(Locale.getDefault(), "@%s", status.account.acct)); holder.bindingArt.artPp.setOnClickListener(v -> { Intent intent = new Intent(context, ProfileActivity.class); @@ -1878,12 +2022,6 @@ public class StatusAdapter extends RecyclerView.Adapter @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); - if (holder instanceof StatusViewHolder && ((StatusViewHolder) holder).timer != null) { - ((StatusViewHolder) holder).timer.cancel(); - } - if (holder instanceof StatusViewHolder && ((StatusViewHolder) holder).dateTimer != null) { - ((StatusViewHolder) holder).dateTimer.cancel(); - } } public interface FetchMoreCallBack { @@ -1899,8 +2037,6 @@ public class StatusAdapter extends RecyclerView.Adapter DrawerFetchMoreBinding bindingFetchMore; DrawerStatusNotificationBinding bindingNotification; DrawerStatusArtBinding bindingArt; - Timer timer; - Timer dateTimer; StatusViewHolder(DrawerStatusBinding itemView) { super(itemView.getRoot()); diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java index 08fee2707..3395cd594 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java @@ -122,7 +122,7 @@ public class FragmentMedia extends Fragment { binding.mediaPicture.setVisibility(View.VISIBLE); binding.mediaPicture.setTransitionName(attachment.url); - if (Helper.isValidContextForGlide(requireActivity())) { + if (Helper.isValidContextForGlide(requireActivity()) && isAdded()) { Glide.with(requireActivity()) .asBitmap() .dontTransform() @@ -142,7 +142,7 @@ public class FragmentMedia extends Fragment { binding.mediaPicture.setVisibility(View.VISIBLE); binding.pbarInf.setIndeterminate(true); binding.loader.setVisibility(View.VISIBLE); - if (Helper.isValidContextForGlide(requireActivity())) { + if (Helper.isValidContextForGlide(requireActivity()) && isAdded()) { Glide.with(requireActivity()) .asBitmap() .dontTransform() @@ -312,7 +312,6 @@ public class FragmentMedia extends Fragment { timer.cancel(); timer = null; } - binding = null; } @Override diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java index 7e1961227..1f99ccaf9 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java @@ -14,11 +14,14 @@ package app.fedilab.android.ui.fragment.settings; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ +import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.os.Bundle; import androidx.preference.ListPreference; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; import app.fedilab.android.R; import app.fedilab.android.helper.Helper; @@ -31,16 +34,28 @@ public class FragmentLanguageSettings extends PreferenceFragmentCompat implement createPref(); } + @SuppressLint("ApplySharedPref") private void createPref() { ListPreference SET_DEFAULT_LOCALE_NEW = findPreference(getString(R.string.SET_DEFAULT_LOCALE_NEW)); if (SET_DEFAULT_LOCALE_NEW != null) { SET_DEFAULT_LOCALE_NEW.getContext().setTheme(Helper.dialogStyle()); } + + Preference SET_TRANSLATE_VALUES_RESET = findPreference(getString(R.string.SET_TRANSLATE_VALUES_RESET)); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + if (SET_TRANSLATE_VALUES_RESET != null) { + SET_TRANSLATE_VALUES_RESET.setOnPreferenceClickListener(preference -> { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(getString(R.string.SET_DEFAULT_LOCALE_NEW), null); + editor.commit(); + return true; + }); + } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.compareToIgnoreCase(getString(R.string.SET_DEFAULT_LOCALE_NEW)) == 0) { + if (key.compareToIgnoreCase(getString(R.string.SET_DEFAULT_LOCALE_NEW)) == 0 || key.compareToIgnoreCase(getString(R.string.SET_TRANSLATE_VALUES_RESET)) == 0) { requireActivity().recreate(); Helper.recreateMainActivity(requireActivity()); } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java index a807bcc95..1ce3b38d4 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java @@ -21,8 +21,6 @@ import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -45,7 +43,6 @@ import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.helper.DividerDecoration; import app.fedilab.android.helper.Helper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.drawer.StatusAdapter; import app.fedilab.android.viewmodel.mastodon.StatusesVM; @@ -98,26 +95,19 @@ public class FragmentMastodonContext extends Fragment { } } else if (statusPosted != null && statusAdapter != null) { if (requireActivity() instanceof ContextActivity) { - new Thread(() -> { - Status convertStatus = SpannableHelper.convertStatus(context, statusPosted); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> { - int i = 0; - for (Status status : statuses) { - if (status.id.equals(convertStatus.in_reply_to_id)) { - statuses.add((i + 1), convertStatus); - statusAdapter.notifyItemInserted((i + 1)); - if (requireActivity() instanceof ContextActivity) { - //Redraw decorations - statusAdapter.notifyItemRangeChanged(0, statuses.size()); - } - break; - } - i++; + int i = 0; + for (Status status : statuses) { + if (status.id.equals(statusPosted.in_reply_to_id)) { + statuses.add((i + 1), statusPosted); + statusAdapter.notifyItemInserted((i + 1)); + if (requireActivity() instanceof ContextActivity) { + //Redraw decorations + statusAdapter.notifyItemRangeChanged(0, statuses.size()); } - }; - mainHandler.post(myRunnable); - }).start(); + break; + } + i++; + } } } } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java index 70005b28e..6d3ce11f6 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java @@ -14,17 +14,14 @@ package app.fedilab.android.ui.fragment.timeline; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ -import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.service.notification.StatusBarNotification; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -178,22 +175,6 @@ public class FragmentMastodonNotification extends Fragment implements Notificati } - @Override - public void onResume() { - super.onResume(); - NotificationManager mNotificationManager = (NotificationManager) requireActivity().getSystemService(Context.NOTIFICATION_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BaseMainActivity.currentAccount != null && BaseMainActivity.currentAccount.mastodon_account != null) { - for (StatusBarNotification statusBarNotification : mNotificationManager.getActiveNotifications()) { - if ((BaseMainActivity.currentAccount.mastodon_account.acct + "@" + BaseMainActivity.currentAccount.instance).equals(statusBarNotification.getGroupKey())) { - mNotificationManager.cancel(statusBarNotification.getId()); - } - } - } else { - mNotificationManager.cancelAll(); - } - } - - /** * Intialize the view for notifications * @@ -456,25 +437,28 @@ public class FragmentMastodonNotification extends Fragment implements Notificati } int position = 0; //We loop through messages already in the timeline - for (Notification notificationsAlreadyPresent : this.notificationList) { - //We compare the date of each status and we only add status having a date greater than the another, it is inserted at this position - //Pinned messages are ignored because their date can be older - if (notificationReceived.id.compareTo(notificationsAlreadyPresent.id) > 0) { + if (this.notificationList != null) { + notificationAdapter.notifyItemRangeChanged(0, this.notificationList.size()); + for (Notification notificationsAlreadyPresent : this.notificationList) { + //We compare the date of each status and we only add status having a date greater than the another, it is inserted at this position + //Pinned messages are ignored because their date can be older + if (notificationReceived.id.compareTo(notificationsAlreadyPresent.id) > 0) { + //We add the status to a list of id - thus we know it is already in the timeline + idOfAddedNotifications.add(notificationReceived.id); + this.notificationList.add(position, notificationReceived); + notificationAdapter.notifyItemInserted(position); + break; + } + position++; + } + //Statuses added at the bottom, we flag them by position = -2 for not dealing with them and fetch more + if (position == this.notificationList.size()) { //We add the status to a list of id - thus we know it is already in the timeline idOfAddedNotifications.add(notificationReceived.id); this.notificationList.add(position, notificationReceived); notificationAdapter.notifyItemInserted(position); - break; + return NOTIFICATION__AT_THE_BOTTOM; } - position++; - } - //Statuses added at the bottom, we flag them by position = -2 for not dealing with them and fetch more - if (position == this.notificationList.size()) { - //We add the status to a list of id - thus we know it is already in the timeline - idOfAddedNotifications.add(notificationReceived.id); - this.notificationList.add(position, notificationReceived); - notificationAdapter.notifyItemInserted(position); - return NOTIFICATION__AT_THE_BOTTOM; } return position; } @@ -491,8 +475,8 @@ public class FragmentMastodonNotification extends Fragment implements Notificati } }).start(); } - super.onDestroyView(); LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(receive_action); + super.onDestroyView(); } @Override diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java index 5d3089797..53fd1aaef 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -30,7 +30,6 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -57,7 +56,6 @@ import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.drawer.StatusAdapter; import app.fedilab.android.viewmodel.mastodon.AccountsVM; @@ -119,15 +117,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. statusAdapter.notifyItemRemoved(position); } } else if (statusPosted != null && statusAdapter != null && timelineType == Timeline.TimeLineEnum.HOME) { - new Thread(() -> { - Status convertStatus = SpannableHelper.convertStatus(context, statusPosted); - Handler mainHandler = new Handler(Looper.getMainLooper()); - Runnable myRunnable = () -> { - statuses.add(0, convertStatus); - statusAdapter.notifyItemInserted(0); - }; - mainHandler.post(myRunnable); - }).start(); + statuses.add(0, statusPosted); + statusAdapter.notifyItemInserted(0); } } } @@ -184,15 +175,12 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. public void scrollToTop() { if (binding != null) { - binding.recyclerView.scrollToPosition(0); + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + route(DIRECTION.SCROLL_TOP, true); } } - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - } - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -437,6 +425,9 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (direction == DIRECTION.BOTTOM) { flagLoading = true; } + if (direction == DIRECTION.SCROLL_TOP) { + binding.recyclerView.scrollToPosition(0); + } } /** @@ -473,7 +464,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. statusFetchMore.isFetchMore = true; statusFetchMore.id = Helper.generateString(); int insertAt; - if (direction == DIRECTION.REFRESH || direction == DIRECTION.BOTTOM) { + if (direction == DIRECTION.REFRESH || direction == DIRECTION.BOTTOM || direction == DIRECTION.SCROLL_TOP) { insertAt = lastInsertedPosition; } else { insertAt = initialInsertedPosition; @@ -499,27 +490,31 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. return STATUS_PRESENT; } int position = 0; - //We loop through messages already in the timeline - for (Status statusAlreadyPresent : this.statuses) { - //We compare the date of each status and we only add status having a date greater than the another, it is inserted at this position - //Pinned messages are ignored because their date can be older - if (statusReceived.id.compareTo(statusAlreadyPresent.id) > 0 && !statusAlreadyPresent.pinned) { + if (this.statuses != null) { + statusAdapter.notifyItemRangeChanged(0, this.statuses.size()); + //We loop through messages already in the timeline + for (Status statusAlreadyPresent : this.statuses) { + //We compare the date of each status and we only add status having a date greater than the another, it is inserted at this position + //Pinned messages are ignored because their date can be older + if (statusReceived.id.compareTo(statusAlreadyPresent.id) > 0 && !statusAlreadyPresent.pinned) { + //We add the status to a list of id - thus we know it is already in the timeline + idOfAddedStatuses.add(statusReceived.id); + this.statuses.add(position, statusReceived); + statusAdapter.notifyItemInserted(position); + break; + } + position++; + } + //Statuses added at the bottom, we flag them by position = -2 for not dealing with them and fetch more + if (position == this.statuses.size()) { //We add the status to a list of id - thus we know it is already in the timeline idOfAddedStatuses.add(statusReceived.id); this.statuses.add(position, statusReceived); statusAdapter.notifyItemInserted(position); - break; + return STATUS_AT_THE_BOTTOM; } - position++; - } - //Statuses added at the bottom, we flag them by position = -2 for not dealing with them and fetch more - if (position == this.statuses.size()) { - //We add the status to a list of id - thus we know it is already in the timeline - idOfAddedStatuses.add(statusReceived.id); - this.statuses.add(position, statusReceived); - statusAdapter.notifyItemInserted(position); - return STATUS_AT_THE_BOTTOM; } + return position; } @@ -627,11 +622,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (direction == DIRECTION.TOP) { timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -647,11 +642,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (direction == DIRECTION.TOP) { timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -669,11 +664,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); } else if (direction == DIRECTION.TOP) { flagLoading = false; - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getNitter(remoteInstance, pinnedTimeline.remoteInstance.host, null) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -692,11 +687,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); } else if (direction == DIRECTION.TOP) { flagLoading = false; - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getMisskey(remoteInstance, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -713,11 +708,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); } else if (direction == DIRECTION.TOP) { flagLoading = false; - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getPeertube(remoteInstance, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -733,11 +728,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (direction == DIRECTION.TOP) { timelinesVM.getPublic(null, remoteInstance, true, false, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getPublic(null, remoteInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -754,11 +749,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (direction == DIRECTION.TOP) { timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -778,11 +773,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (direction == DIRECTION.TOP) { timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusesRefresh, direction, true); } else { initializeStatusesCommonView(statusesRefresh); } @@ -964,11 +959,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. .observe(getViewLifecycleOwner(), statusesTop -> dealWithPagination(statusesTop, DIRECTION.TOP, true)); } - } else if (direction == DIRECTION.REFRESH) { + } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, true, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), false) .observe(getViewLifecycleOwner(), statusRefresh -> { if (statusAdapter != null) { - dealWithPagination(statusRefresh, DIRECTION.REFRESH, true); + dealWithPagination(statusRefresh, direction, true); } else { initializeStatusesCommonView(statusRefresh); } @@ -1029,6 +1024,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. public enum DIRECTION { TOP, BOTTOM, - REFRESH + REFRESH, + SCROLL_TOP } } \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java index 5cc13408d..2e138d611 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java @@ -14,15 +14,17 @@ package app.fedilab.android.ui.fragment.timeline; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ -import android.annotation.SuppressLint; +import android.app.NotificationManager; +import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; +import android.service.notification.StatusBarNotification; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; @@ -238,6 +240,20 @@ public class FragmentNotificationContainer extends Fragment { return binding.getRoot(); } + @Override + public void onResume() { + super.onResume(); + NotificationManager mNotificationManager = (NotificationManager) requireActivity().getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BaseMainActivity.currentAccount != null && BaseMainActivity.currentAccount.mastodon_account != null) { + for (StatusBarNotification statusBarNotification : mNotificationManager.getActiveNotifications()) { + if (statusBarNotification.getGroupKey().contains(BaseMainActivity.currentAccount.mastodon_account.acct + "@" + BaseMainActivity.currentAccount.instance)) { + mNotificationManager.cancel(statusBarNotification.getId()); + } + } + } else { + mNotificationManager.cancelAll(); + } + } public void scrollToTop() { if (binding != null) { @@ -251,11 +267,5 @@ public class FragmentNotificationContainer extends Fragment { } } - @SuppressLint("ApplySharedPref") - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - } } diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java index 5b8cf38ec..0c9868ba4 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java @@ -52,7 +52,6 @@ import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.api.Token; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import retrofit2.Call; @@ -306,9 +305,6 @@ public class AccountsVM extends AndroidViewModel { } } Account finalAccount = account; - if (finalAccount != null) { - SpannableHelper.convertAccount(getApplication().getApplicationContext(), finalAccount); - } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> accountMutableLiveData.setValue(finalAccount); mainHandler.post(myRunnable); @@ -342,7 +338,7 @@ public class AccountsVM extends AndroidViewModel { try { Response> accountStatusesResponse = accountStatusesCall.execute(); if (accountStatusesResponse.isSuccessful()) { - statusList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), accountStatusesResponse.body()); + statusList = accountStatusesResponse.body(); pagination = MastodonHelper.getPagination(accountStatusesResponse.headers()); } @@ -378,7 +374,7 @@ public class AccountsVM extends AndroidViewModel { try { Response> followersResponse = followersCall.execute(); if (followersResponse.isSuccessful()) { - accountList = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), followersResponse.body()); + accountList = followersResponse.body(); pagination = MastodonHelper.getPagination(followersResponse.headers()); } } catch (Exception e) { @@ -414,7 +410,7 @@ public class AccountsVM extends AndroidViewModel { try { Response> followingResponse = followingCall.execute(); if (followingResponse.isSuccessful()) { - accountList = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), followingResponse.body()); + accountList = followingResponse.body(); pagination = MastodonHelper.getPagination(followingResponse.headers()); } } catch (Exception e) { @@ -883,11 +879,6 @@ public class AccountsVM extends AndroidViewModel { } } List finalAccountList = accountList; - if (finalAccountList != null) { - for (Account account : finalAccountList) { - SpannableHelper.convertAccount(getApplication().getApplicationContext(), account); - } - } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); mainHandler.post(myRunnable); @@ -912,7 +903,7 @@ public class AccountsVM extends AndroidViewModel { Response> bookmarksResponse = bookmarksCall.execute(); if (bookmarksResponse.isSuccessful()) { statusList = bookmarksResponse.body(); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + statuses.statuses = statusList; statuses.pagination = MastodonHelper.getPagination(bookmarksResponse.headers()); } } catch (Exception e) { @@ -943,7 +934,7 @@ public class AccountsVM extends AndroidViewModel { Response> favouritesResponse = favouritesCall.execute(); if (favouritesResponse.isSuccessful()) { statusList = favouritesResponse.body(); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + statuses.statuses = statusList; statuses.pagination = MastodonHelper.getPagination(favouritesResponse.headers()); } } catch (Exception e) { @@ -975,7 +966,7 @@ public class AccountsVM extends AndroidViewModel { Response> mutesResponse = mutesCall.execute(); if (mutesResponse.isSuccessful()) { accountList = mutesResponse.body(); - accounts.accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountList); + accounts.accounts = accountList; accounts.pagination = MastodonHelper.getPagination(mutesResponse.headers()); } } catch (Exception e) { @@ -1007,7 +998,7 @@ public class AccountsVM extends AndroidViewModel { Response> blocksResponse = blocksCall.execute(); if (blocksResponse.isSuccessful()) { accountList = blocksResponse.body(); - accounts.accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountList); + accounts.accounts = accountList; accounts.pagination = MastodonHelper.getPagination(blocksResponse.headers()); } } catch (Exception e) { @@ -1303,7 +1294,7 @@ public class AccountsVM extends AndroidViewModel { Response> followRequestsResponse = followRequestsCall.execute(); if (followRequestsResponse.isSuccessful()) { accountList = followRequestsResponse.body(); - accounts.accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountList); + accounts.accounts = accountList; accounts.pagination = MastodonHelper.getPagination(followRequestsResponse.headers()); } } catch (Exception e) { diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java index afca9d266..06eccbfd4 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java @@ -23,16 +23,12 @@ import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - import java.util.List; import java.util.concurrent.TimeUnit; import app.fedilab.android.client.endpoints.MastodonAnnouncementsService; import app.fedilab.android.client.entities.api.Announcement; import app.fedilab.android.helper.Helper; -import app.fedilab.android.helper.SpannableHelper; import okhttp3.OkHttpClient; import retrofit2.Call; import retrofit2.Response; @@ -55,7 +51,6 @@ public class AnnouncementsVM extends AndroidViewModel { } private MastodonAnnouncementsService init(@NonNull String instance) { - Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://" + instance + "/api/v1/") .addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder())) @@ -83,7 +78,6 @@ public class AnnouncementsVM extends AndroidViewModel { Response> getAnnouncementsResponse = getAnnouncementsCall.execute(); if (getAnnouncementsResponse.isSuccessful()) { announcementList = getAnnouncementsResponse.body(); - SpannableHelper.convertAnnouncement(getApplication(), announcementList); } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java index eccded41a..30faf9f66 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java @@ -35,7 +35,6 @@ import app.fedilab.android.client.entities.api.Notifications; import app.fedilab.android.client.entities.api.PushSubscription; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.TimelineHelper; import okhttp3.OkHttpClient; import retrofit2.Call; @@ -103,15 +102,6 @@ public class NotificationsVM extends AndroidViewModel { if (notificationsResponse.isSuccessful()) { List notFilteredNotifications = notificationsResponse.body(); notifications.notifications = TimelineHelper.filterNotification(getApplication().getApplicationContext(), notFilteredNotifications); - if (notifications.notifications != null) { - for (Notification notification : notifications.notifications) { - if (notification != null) { - if (notification.status != null) { - notification.status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), notification.status); - } - } - } - } notifications.pagination = MastodonHelper.getPagination(notificationsResponse.headers()); } } catch (Exception e) { @@ -147,9 +137,6 @@ public class NotificationsVM extends AndroidViewModel { Response notificationResponse = notificationCall.execute(); if (notificationResponse.isSuccessful()) { notification = notificationResponse.body(); - if (notification != null) { - notification.status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), notification.status); - } } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java index b4d5f0e07..bbc928aa9 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java @@ -36,7 +36,6 @@ import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; -import app.fedilab.android.helper.SpannableHelper; import okhttp3.OkHttpClient; import retrofit2.Call; import retrofit2.Response; @@ -112,13 +111,9 @@ public class SearchVM extends AndroidViewModel { if (results != null) { if (results.statuses == null) { results.statuses = new ArrayList<>(); - } else { - results.statuses = SpannableHelper.convertStatus(getApplication(), results.statuses); } if (results.accounts == null) { results.accounts = new ArrayList<>(); - } else { - results.accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), results.accounts); } if (results.hashtags == null) { results.hashtags = new ArrayList<>(); @@ -144,7 +139,6 @@ public class SearchVM extends AndroidViewModel { try { results.statuses = new ArrayList<>(); List statuses = new StatusCache(getApplication()).searchStatus(StatusCache.CacheEnum.HOME, instance, userId, q); - statuses = SpannableHelper.convertStatus(getApplication(), statuses); results.statuses.addAll(statuses); } catch (DBException e) { e.printStackTrace(); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java index 39118e99c..6f4bf1594 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java @@ -47,7 +47,6 @@ import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.TimelineHelper; import okhttp3.Headers; import okhttp3.MultipartBody; @@ -279,7 +278,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } } catch (Exception e) { e.printStackTrace(); @@ -356,13 +355,7 @@ public class StatusesVM extends AndroidViewModel { context = contextResponse.body(); if (context != null) { TimelineHelper.filterStatus(getApplication().getApplicationContext(), context.descendants, TimelineHelper.FilterTimeLineType.CONTEXT); - for (Status status : context.descendants) { - SpannableHelper.convertStatus(getApplication().getApplicationContext(), status); - } TimelineHelper.filterStatus(getApplication().getApplicationContext(), context.ancestors, TimelineHelper.FilterTimeLineType.CONTEXT); - for (Status status : context.ancestors) { - SpannableHelper.convertStatus(getApplication().getApplicationContext(), status); - } } } @@ -406,7 +399,7 @@ public class StatusesVM extends AndroidViewModel { try { Response> accountsResponse = accountsCall.execute(); if (accountsResponse.isSuccessful()) { - accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountsResponse.body()); + accounts = accountsResponse.body(); } headers = accountsResponse.headers(); } catch (Exception e) { @@ -451,7 +444,7 @@ public class StatusesVM extends AndroidViewModel { try { Response> accountsResponse = accountsCall.execute(); if (accountsResponse.isSuccessful()) { - accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountsResponse.body()); + accounts = accountsResponse.body(); } headers = accountsResponse.headers(); } catch (Exception e) { @@ -461,7 +454,9 @@ public class StatusesVM extends AndroidViewModel { Handler mainHandler = new Handler(Looper.getMainLooper()); Accounts accountsPagination = new Accounts(); accountsPagination.accounts = accounts; - accountsPagination.pagination = MastodonHelper.getPagination(headers); + if (headers != null) { + accountsPagination.pagination = MastodonHelper.getPagination(headers); + } Runnable myRunnable = () -> accountsMutableLiveData.setValue(accountsPagination); mainHandler.post(myRunnable); }).start(); @@ -487,7 +482,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -531,7 +526,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -577,7 +572,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -621,7 +616,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -665,7 +660,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -709,7 +704,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -753,7 +748,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -797,7 +792,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -841,7 +836,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); @@ -885,7 +880,7 @@ public class StatusesVM extends AndroidViewModel { try { Response statusResponse = statusCall.execute(); if (statusResponse.isSuccessful()) { - status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + status = statusResponse.body(); } else { if (statusResponse.errorBody() != null) { errorMessage = statusResponse.errorBody().string(); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java index a04c2a0de..78c8d5b8e 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java @@ -48,7 +48,6 @@ import app.fedilab.android.client.entities.peertube.PeertubeVideo; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; -import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.TimelineHelper; import okhttp3.OkHttpClient; import retrofit2.Call; @@ -121,7 +120,7 @@ public class TimelinesVM extends AndroidViewModel { Response> publicTlResponse = publicTlCall.execute(); if (publicTlResponse.isSuccessful()) { statusList = publicTlResponse.body(); - statusList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + statusList = statusList; } } catch (Exception e) { e.printStackTrace(); @@ -190,8 +189,7 @@ public class TimelinesVM extends AndroidViewModel { Response> publicTlResponse = publicTlCall.execute(); if (publicTlResponse.isSuccessful()) { List notFilteredStatuses = publicTlResponse.body(); - List filteredStatuses = TimelineHelper.filterStatus(getApplication(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.statuses = TimelineHelper.filterStatus(getApplication(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); statuses.pagination = MastodonHelper.getPagination(publicTlResponse.headers()); } } catch (Exception e) { @@ -219,7 +217,6 @@ public class TimelinesVM extends AndroidViewModel { statusesMutableLiveData = new MutableLiveData<>(); new Thread(() -> { Call publicTlCall = mastodonTimelinesService.getNitter(accountsStr, max_position); - Statuses statuses = new Statuses(); if (publicTlCall != null) { try { @@ -233,8 +230,10 @@ public class TimelinesVM extends AndroidViewModel { statusList.add(status); } } - statuses.statuses = SpannableHelper.convertNitterStatus(getApplication().getApplicationContext(), statusList); - statuses.pagination = MastodonHelper.getPagination(publicTlResponse.headers()); + statuses.statuses = statusList; + String max_id = publicTlResponse.headers().get("min-id"); + statuses.pagination = new Pagination(); + statuses.pagination.max_id = max_id; } } catch (Exception e) { e.printStackTrace(); @@ -273,12 +272,11 @@ public class TimelinesVM extends AndroidViewModel { List statusList = new ArrayList<>(); if (misskeyNoteList != null) { for (MisskeyNote misskeyNote : misskeyNoteList) { - Status status = MisskeyNote.convert(misskeyNote); + Status status = MisskeyNote.convert(misskeyNote, instance); statusList.add(status); } } - List filteredStatuses = TimelineHelper.filterStatus(getApplication(), statusList, TimelineHelper.FilterTimeLineType.PUBLIC); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.statuses = TimelineHelper.filterStatus(getApplication(), statusList, TimelineHelper.FilterTimeLineType.PUBLIC); statuses.pagination = new Pagination(); if (statusList.size() > 0) { statuses.pagination.min_id = statusList.get(0).id; @@ -324,8 +322,7 @@ public class TimelinesVM extends AndroidViewModel { statusList.add(status); } } - List filteredStatuses = TimelineHelper.filterStatus(getApplication(), statusList, TimelineHelper.FilterTimeLineType.PUBLIC); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.statuses = TimelineHelper.filterStatus(getApplication(), statusList, TimelineHelper.FilterTimeLineType.PUBLIC); statuses.pagination = new Pagination(); if (statusList.size() > 0) { //These values are not used. @@ -400,14 +397,14 @@ public class TimelinesVM extends AndroidViewModel { MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Statuses statuses = new Statuses(); - Call> hashTagTlCall = mastodonTimelinesService.getHashTag(token, hashtag, local, onlyMedia, all, any, none, maxId, sinceId, minId, limit); + String hashtagTrim = hashtag.replaceAll("\\#", ""); + Call> hashTagTlCall = mastodonTimelinesService.getHashTag(token, hashtagTrim, local, onlyMedia, all, any, none, maxId, sinceId, minId, limit); if (hashTagTlCall != null) { try { Response> hashTagTlResponse = hashTagTlCall.execute(); if (hashTagTlResponse.isSuccessful()) { List notFilteredStatuses = hashTagTlResponse.body(); - List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.statuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); statuses.pagination = MastodonHelper.getPagination(hashTagTlResponse.headers()); } } catch (Exception e) { @@ -449,8 +446,7 @@ public class TimelinesVM extends AndroidViewModel { Response> homeTlResponse = homeTlCall.execute(); if (homeTlResponse.isSuccessful()) { List notFilteredStatuses = homeTlResponse.body(); - List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.HOME); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.statuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.HOME); statuses.pagination = MastodonHelper.getPagination(homeTlResponse.headers()); if (!fetchingMissing) { for (Status status : statuses.statuses) { @@ -502,8 +498,7 @@ public class TimelinesVM extends AndroidViewModel { statuses = statusCacheDAO.geStatuses(StatusCache.CacheEnum.HOME, instance, user_id, maxId, minId, sinceId); if (statuses != null) { - List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), statuses.statuses, TimelineHelper.FilterTimeLineType.HOME); - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.statuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), statuses.statuses, TimelineHelper.FilterTimeLineType.HOME); if (statuses.statuses != null && statuses.statuses.size() > 0) { statuses.pagination = new Pagination(); statuses.pagination.min_id = statuses.statuses.get(0).id; @@ -570,7 +565,7 @@ public class TimelinesVM extends AndroidViewModel { try { Response> listTlResponse = listTlCall.execute(); if (listTlResponse.isSuccessful()) { - statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), listTlResponse.body()); + statuses.statuses = listTlResponse.body(); statuses.pagination = MastodonHelper.getPagination(listTlResponse.headers()); } } catch (Exception e) { @@ -606,11 +601,6 @@ public class TimelinesVM extends AndroidViewModel { Response> conversationsResponse = conversationsCall.execute(); if (conversationsResponse.isSuccessful()) { conversations.conversations = conversationsResponse.body(); - if (conversations.conversations != null) { - for (Conversation conversation : conversations.conversations) { - conversation.last_status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), conversation.last_status); - } - } conversations.pagination = MastodonHelper.getPagination(conversationsResponse.headers()); } } catch (Exception e) { diff --git a/app/src/main/java/app/fedilab/android/viewmodel/pleroma/ActionsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/pleroma/ActionsVM.java new file mode 100644 index 000000000..c57b52ea7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/pleroma/ActionsVM.java @@ -0,0 +1,101 @@ +package app.fedilab.android.viewmodel.pleroma; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.MutableLiveData; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.endpoints.PleromaAPI; +import app.fedilab.android.client.entities.api.Announcement; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ActionsVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData announcementMutableLiveData; + private MutableLiveData> announcementListMutableLiveData; + + public ActionsVM(@NonNull Application application) { + super(application); + } + + private PleromaAPI init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder())) + .client(okHttpClient) + .build(); + return retrofit.create(PleromaAPI.class); + } + + /** + * React to an announcement with an emoji. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id Local ID of an announcement + * @param name Unicode emoji, or shortcode of custom emoji + */ + public void addReaction(@NonNull String instance, String token, @NonNull String id, @NonNull String name) { + PleromaAPI pleromaAPI = init(instance); + new Thread(() -> { + Call addReactionCall = pleromaAPI.addReaction(token, id, name); + if (addReactionCall != null) { + try { + addReactionCall.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Undo a react emoji to an announcement. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id Local ID of an announcement + * @param name Unicode emoji, or shortcode of custom emoji + */ + public void removeReaction(@NonNull String instance, String token, @NonNull String id, @NonNull String name) { + PleromaAPI pleromaAPI = init(instance); + new Thread(() -> { + Call removeReactionCall = pleromaAPI.removeReaction(token, id, name); + if (removeReactionCall != null) { + try { + removeReactionCall.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }).start(); + } +} diff --git a/app/src/main/res/drawable/empty_custom_emoji.xml b/app/src/main/res/drawable/empty_custom_emoji.xml new file mode 100644 index 000000000..7934ee66d --- /dev/null +++ b/app/src/main/res/drawable/empty_custom_emoji.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_insert_emoticon_24.xml b/app/src/main/res/drawable/ic_baseline_insert_emoticon_24.xml new file mode 100644 index 000000000..51f988778 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_insert_emoticon_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_language_24.xml b/app/src/main/res/drawable/ic_baseline_language_24.xml new file mode 100644 index 000000000..b0fd1af38 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_language_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index d3169a5bf..117b1cf75 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -16,12 +16,27 @@ android:orientation="vertical" android:padding="24dp"> + + - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_status.xml b/app/src/main/res/layout/drawer_status.xml index 93808fc2a..b6caece7a 100644 --- a/app/src/main/res/layout/drawer_status.xml +++ b/app/src/main/res/layout/drawer_status.xml @@ -24,7 +24,7 @@ android:layout_marginTop="@dimen/card_margin" android:clipChildren="false" android:clipToPadding="false" - app:cardElevation="2dp"> + app:cardElevation="0dp"> + - + + + tools:text="2m" /> @@ -196,9 +202,6 @@ - + + + + + + + diff --git a/app/src/main/res/layout/drawer_status_compose.xml b/app/src/main/res/layout/drawer_status_compose.xml index c2f8ea26e..550c1e21a 100644 --- a/app/src/main/res/layout/drawer_status_compose.xml +++ b/app/src/main/res/layout/drawer_status_compose.xml @@ -19,12 +19,12 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:id="@+id/cardview_container" android:layout_marginHorizontal="12dp" - android:backgroundTint="?backgroundColorLight" android:layout_marginTop="12dp" android:clipChildren="false" android:clipToPadding="false" - app:cardElevation="2dp"> + app:cardElevation="0dp"> diff --git a/app/src/main/res/layout/layout_reactions.xml b/app/src/main/res/layout/layout_reactions.xml new file mode 100644 index 000000000..46e4b1a0a --- /dev/null +++ b/app/src/main/res/layout/layout_reactions.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml index 3676b0ce0..efe0f42a5 100644 --- a/app/src/main/res/layout/nav_header_main.xml +++ b/app/src/main/res/layout/nav_header_main.xml @@ -45,8 +45,8 @@ android:layout_gravity="center_vertical" android:contentDescription="@string/profile_picture" android:paddingTop="@dimen/nav_header_vertical_spacing" - tools:src="@tools:sample/avatars" - android:scaleType="fitCenter" /> + android:scaleType="fitCenter" + tools:src="@tools:sample/avatars" /> - + android:layout_height="match_parent"> + android:orientation="vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:paddingStart="@dimen/fab_margin" + android:paddingTop="20dp" + android:paddingEnd="@dimen/fab_margin" + android:paddingBottom="20dp"> - - - - - + + android:maxLines="1" + tools:text="Display Name" /> + android:layout_marginStart="6dp" + android:alpha="0.7" + android:ellipsize="end" + android:maxLines="1" + tools:text="\@username\@instance.test" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:background="?backgroundColorLight" + android:orientation="vertical" + android:padding="@dimen/fab_margin"> - + + + + + + - - - - \ No newline at end of file + diff --git a/app/src/main/res/menu/menu_compose.xml b/app/src/main/res/menu/menu_compose.xml index 8fc21461c..88ed2086f 100644 --- a/app/src/main/res/menu/menu_compose.xml +++ b/app/src/main/res/menu/menu_compose.xml @@ -11,6 +11,11 @@ android:icon="@drawable/ic_baseline_contact_page_24" android:title="@string/contact" app:showAsAction="ifRoom" /> + Messàgios in memòria temporànea pro àteras lìnias de tempus Ses seguru de bòlere isboidare sa memòria temporànea\? Si tenes abbotzos cun cuntenutos multimediales as a pèrdere sos cuntenutos alligados. Isbòida sa memòria temporànea + Imprea sa limba predefinida de sistema \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 141ea7d3e..37cb1b2d8 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -726,4 +726,5 @@ Önbelleği temizle Diğer zaman çizelgeleri için önbellekteki mesajlar Taslaklarda saklanan mesajlar + Öntanımlı sistem dilini kullan \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 02e77fe24..b403a3caa 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -16,7 +16,7 @@ Tập tin: %1$s Mật khẩu Email - Tài khoản + Người Tút Tag Lưu @@ -63,7 +63,7 @@ Ứng dụng không thể thu thập emoji tùy chỉnh vào thời điểm này. Bạn có chắc muốn đăng xuất @%1$s@%2$s\? - Không có tút nào + Chưa có Thêm tút này vào mục yêu thích của bạn\? Xóa tút này khỏi mục yêu thích của bạn\? Đăng lại tút này\? @@ -75,7 +75,7 @@ Sao chép Chia sẻ Nhắc đến - Thời gian ẩn + Ẩn có thời hạn Xóa & viết lại Ẩn tài khoản này? @@ -143,11 +143,14 @@ Tìm máy chủ: - Không có tài khoản nào + Không có người nào Không có yêu cầu theo dõi - %1$s tút - %1$s Đang theo dõi - %1$s Người theo dõi + Tút +\n %1$s + Đang theo dõi +\n %1$s + Người theo dõi +\n %1$s Từ chối Không có tút đã lên lịch! @@ -198,10 +201,10 @@ Khi ai đó đăng lại tút của tôi Khi ai đó thích tút của tôi Khi ai đó nhắc tới tôi - Nhắc khi một cuộc bình chọn kết thúc - Nhắc khi có tút mới - Hiện xác nhận trước khi đăng lại tút - Hiện xác nhận trước khi thích tút + Khi cuộc bình chọn kết thúc + Khi có tút mới + Hiện xác nhận trước khi đăng lại + Hiện xác nhận trước khi thích Thông báo? Tắt thông báo Thời gian chờ của NSFW (giây, 0 = tắt) @@ -237,7 +240,7 @@ Bỏ ẩn Đã gửi yêu cầu Theo dõi bạn - Viết hoa chữ đầu khi trả lời + Tự động xuống dòng khi trả lời Giảm kích cỡ hình ảnh Giảm kích thước video @@ -273,7 +276,7 @@ Thêm vào danh sách Xoá danh sách Tên danh sách mới - Đã thêm tài khoản vào danh sách! + Đã thêm người này vào danh sách! Bạn chưa có danh sách nào! %1$s đã chuyển sang %2$s @@ -325,7 +328,7 @@ Tút mới Tải media Chọn âm thanh - Hiện thời gian đăng + Thời gian thông báo Bạn có chắc bỏ chặn %s\? \n \nBạn sẽ không thấy bất kỳ nội dung nào từ máy chủ này trong bảng tin và thông báo của bạn. Những người theo dõi bạn từ máy chủ này cũng sẽ bị xóa. @@ -405,9 +408,9 @@ Lựa chọn %d Bạn cần cho ít nhất 2 lựa chọn! Xong - kết thúc lúc %s + kết thúc %s Bình chọn - Cuộc bình chọn có bạn tham gia đã kết thúc + Cuộc bình chọn bạn đã tham gia kết thúc Cuộc bình chọn của bạn đã kết thúc Loại Chuyển bảng tin @@ -490,7 +493,7 @@ Ghi âm Ứng dụng sẽ gửi thông báo vào thời điểm chỉ định. Bạn có thể đảo ngược (tức là: im lặng) thời điểm này bằng con quay bên phải. Không cắt ảnh xem trước - Tự động chèn dấu ngắt dòng sau phần đề cập để viết hoa chữ cái đầu tiên + Tự động xuống dòng và viết hoa chữ đầu tiên sau khi nhắc đến ai đó. Chia sẻ tút với nguồn cấp dữ liệu RSS Soạn thảo Chọn @@ -502,8 +505,8 @@ \n \nBạn có thể thêm nội dung. Cảm ơn bạn! Mức độ hiển thị - Tắt emoji dạng GIF - Báo cáo tài khoản + Tắt emoji GIF + Báo cáo người này %d bình chọn @@ -527,10 +530,10 @@ Có lựa chọn trùng lặp! Xóa bộ nhớ đệm khi thoát Bộ nhớ đệm (media, tút, dữ liệu từ trình duyệt tích hợp) sẽ tự động bị xóa khi thoát ứng dụng. - Bạn có muốn ngưng theo dõi tài khoản này\? - Yêu cầu xác nhận trước khi hủy theo dõi ai đó + Bạn có muốn ngưng theo dõi người này\? + Yêu cầu xác nhận trước khi ngưng theo dõi ai đó. Thay thế liên kết từ Medium - Thay thế liên kết từ medium.com với một giao diện mã nguồn mở, chú trọng bảo mật. + Dùng một frontend thay thế cho Medium Mặc định: scribe.rip Sử dụng hệ thống thông báo đẩy để nhận thông báo trong thời gian thực. Thêm ghi chú @@ -615,10 +618,10 @@ Tên danh sách không hợp lệ! Không có người dùng nào trong danh sách này! Đã lên lịch - Trong khoảng thời gian + Trong khoảng thời gian này Theme gốc Chọn theme gốc là tối hay sáng - Tùy chỉnh bảng tin + Tùy chỉnh Theme của cộng đồng Chọn một theme được tạo bởi cộng đồng Hiển thị @@ -644,7 +647,7 @@ Kiểu tút mặc định: Đã thêm tút vào mục yêu thích! Đã xóa tút khỏi mục yêu thích! - Số lượng tài khoản mỗi lần tải + Số lượng người dùng mỗi lần tải Số lượng thông báo mỗi lần tải Âm nhạc Không thể để trống mục này! @@ -665,7 +668,7 @@ Địa chỉ máy chủ không hợp lệ! Đăng lại bởi Thích bởi - Hạn chế + Riêng tư Khác Vd: Nội Dung Nhạy Cảm Thêm trạng thái @@ -688,11 +691,11 @@ Đây là spam Liên kết độc hại, giả tương tác hoặc trả lời lặp đi lặp lại Vi phạm quy tắc máy chủ - Bạn đang theo dõi tài khoản này. Để không nhìn thấy tút của họ trong bảng tin nữa, hãy ngưng theo dõi họ. + Bạn đang theo dõi người này. Để không nhìn thấy tút của họ trong bảng tin nữa, hãy ngưng theo dõi họ. Ẩn %1$s Bạn sẽ không thấy tút của họ. Họ vẫn có thể theo dõi bạn và thấy tút của bạn nhưng không biết rằng họ bị ẩn. Chặn %1$s - Tài khoản này từ một máy chủ khác. Gửi luôn cho kiểm duyệt viên máy chủ đó\? + Người này từ một máy chủ khác. Gửi luôn cho kiểm duyệt viên máy chủ đó\? Chuyển tiếp %1$s Đã gửi báo cáo! Bạn sẽ không thấy tút của họ. Họ sẽ không thể xem tút của bạn hoặc theo dõi bạn. Họ sẽ biếtrằng họ bị chặn. @@ -704,7 +707,7 @@ Kết quả bình chọn Cập nhật từ mọi người Theo dõi - Đánh dấu tất cả là đã đọc + Đánh dấu đã đọc xong Hiện toàn bộ Bạn có chắc muốn xóa tất cả thông báo\? Không thể khôi phục lại. \"Mastodon không phải là một trang web duy nhất như Twitter hoặc Facebook, đó là một mạng lưới hàng ngàn cộng đồng được điều hành bởi các tổ chức và cá nhân khác nhau cung cấp trải nghiệm truyền thông xã hội liền mạch.\" @@ -730,7 +733,7 @@ Gần đây nhất Bộ lọc Tên miền - Nguồn gốc tài khoản bị báo cáo + Máy chủ người bị báo cáo Trạng thái Đã xử lý Đã duyệt @@ -745,10 +748,11 @@ Đường thẳng Chế độ Xóa Xóa cache - Tút trong cache cho Bảng Tin - Tút lưu trong nháp - Bạn có chắc muốn xóa cache\? Nếu bạn có tút nháp đính kèm media, nó sẽ bị mất. - Tút trong cache những Bảng Tin khác - Kích cỡ cache - Xóa cache + Bộ nhớ đệm tút Bảng Tin + Tút nháp + Bạn có chắc muốn xóa bộ nhớ đệm\? Nếu bạn có tút nháp đính kèm media, nó sẽ bị mất. + Bộ nhớ đệm tút những Bảng Tin khác + Dung lượng cho phép + Xóa bộ nhớ đệm + Dùng ngôn ngữ hệ thống \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebb0eee0d..294cd3a23 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -779,6 +779,7 @@ SET_FEATURED_TAG_ACTION SET_RETRIEVE_METADATA_IF_URL_FROM_EXTERAL + SET_TRANSLATE_VALUES_RESET en fr @@ -863,6 +864,7 @@ SET_TIME_FROM SET_TIME_TO SET_MED_DESC_TIMEOUT + SET_COMPOSE_LANGUAGE SET_NOTIF_FOLLOW @@ -894,7 +896,7 @@ SET_ACCOUNTS_PER_CALL SET_STATUSES_PER_CALL SET_NOTIFICATIONS_PER_CALL - + INSTANCE_INFO SET_INVIDIOUS SET_INVIDIOUS_HOST invidious.snopyta.org @@ -978,6 +980,8 @@ File cache size Clear cache Are you sure you want to delete cache? If you have drafts with media, the attached media will be lost. + Use the default system language + Language for messages diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 52027b6ca..98d258330 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -187,7 +187,7 @@ @color/cyanea_accent_dark_reference @color/cyanea_accent_dark_reference @color/cyanea_accent_dark_reference - @color/cyanea_accent_dark_reference + @color/cyanea_accent_dark_reference