diff --git a/app/build.gradle b/app/build.gradle index 0b28e4e7..a677471a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { defaultConfig { minSdk 21 targetSdk 31 - versionCode 431 - versionName "3.7.4" + versionCode 433 + versionName "3.8.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } flavorDimensions "default" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a5a34ba..be259dfd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -201,7 +201,7 @@ android:configChanges="keyboardHidden|orientation|screenSize" android:label="@string/account" /> @@ -243,7 +243,16 @@ android:configChanges="keyboardHidden|orientation|screenSize" android:label="@string/action_about" android:theme="@style/AppThemeBar" /> - + + diff --git a/app/src/main/assets/release_notes/notes.json b/app/src/main/assets/release_notes/notes.json index 9d9f8e1f..ce67924f 100644 --- a/app/src/main/assets/release_notes/notes.json +++ b/app/src/main/assets/release_notes/notes.json @@ -1,4 +1,14 @@ [ + { + "version": "3.8.0", + "code": "433", + "note": "Added:\n- List of blocked domains (allow to unblock)\n- Support gemini links\n- Suggested followers\n- Mod/Adm: Manage instance blocked domains\n- Open messages with another account\n- Allow to disable notifications for admins\n- Sort lists\n\nChanged:\n- Allow search term to be edited\n\nFixed:\n- Drafts deleted with no warning\n- Remove lists from \"Manage timelines\"\n- App crashes when proxy is set\n- Filter not synced after being edited\n- Some crashes / improvements" + }, + { + "version": "3.7.5", + "code": "432", + "note": "Added:\n- List of blocked domains (allow to unblock)\n- Support gemini links\n- Suggested followers\n\nChanged:\n- Allow search term to be edited\n\nFixed:\n- Drafts deleted with no warning\n- App crashes when proxy is set\n- Filter not synced after being edited\n- Some crashes" + }, { "version": "3.7.4", "code": "431", diff --git a/app/src/main/java/app/fedilab/android/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/BaseMainActivity.java index 4682d631..f98860b4 100644 --- a/app/src/main/java/app/fedilab/android/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/BaseMainActivity.java @@ -95,7 +95,6 @@ import java.util.regex.Pattern; import app.fedilab.android.activities.AboutActivity; import app.fedilab.android.activities.ActionActivity; -import app.fedilab.android.activities.AdminActionActivity; import app.fedilab.android.activities.AnnouncementActivity; import app.fedilab.android.activities.BaseActivity; import app.fedilab.android.activities.CacheActivity; @@ -117,7 +116,9 @@ import app.fedilab.android.activities.ReorderTimelinesActivity; import app.fedilab.android.activities.ScheduledActivity; import app.fedilab.android.activities.SearchResultTabActivity; import app.fedilab.android.activities.SettingsActivity; +import app.fedilab.android.activities.SuggestionActivity; import app.fedilab.android.activities.TrendsActivity; +import app.fedilab.android.activities.admin.AdminActionActivity; import app.fedilab.android.broadcastreceiver.NetworkStateReceiver; import app.fedilab.android.client.entities.api.Emoji; import app.fedilab.android.client.entities.api.EmojiInstance; @@ -391,6 +392,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } else if (id == R.id.nav_trends) { Intent intent = new Intent(this, TrendsActivity.class); startActivity(intent); + } else if (id == R.id.nav_suggestions) { + Intent intent = new Intent(this, SuggestionActivity.class); + startActivity(intent); } else if (id == R.id.nav_cache) { Intent intent = new Intent(BaseMainActivity.this, CacheActivity.class); startActivity(intent); @@ -582,7 +586,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { - if (currentAccount == null) { + if (currentAccount == null || currentAccount.mastodon_account == null) { //It is not, the user is redirected to the login page Intent myIntent = new Intent(BaseMainActivity.this, LoginActivity.class); startActivity(myIntent); @@ -650,13 +654,13 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt regex_public = sharedpreferences.getString(getString(R.string.SET_FILTER_REGEX_PUBLIC) + currentUserID + currentInstance, null); show_art_nsfw = sharedpreferences.getBoolean(getString(R.string.SET_ART_WITH_NSFW) + currentUserID + currentInstance, false); binding.profilePicture.setOnClickListener(v -> binding.drawerLayout.openDrawer(GravityCompat.START)); - Helper.loadPP(binding.profilePicture, currentAccount); + Helper.loadPP(BaseMainActivity.this, binding.profilePicture, currentAccount); headerMainBinding.accountAcc.setText(String.format("%s@%s", currentAccount.mastodon_account.username, currentAccount.instance)); if (currentAccount.mastodon_account.display_name == null || currentAccount.mastodon_account.display_name.isEmpty()) { currentAccount.mastodon_account.display_name = currentAccount.mastodon_account.acct; } headerMainBinding.accountName.setText(currentAccount.mastodon_account.display_name); - Helper.loadPP(headerMainBinding.accountProfilePicture, currentAccount, false); + Helper.loadPP(BaseMainActivity.this, headerMainBinding.accountProfilePicture, currentAccount, false); MastodonHelper.loadProfileMediaMastodon(headerMainBinding.backgroundImage, currentAccount.mastodon_account, MastodonHelper.MediaAccountType.HEADER); /* * Some general data are loaded when the app starts such; @@ -697,16 +701,18 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class).getConnectedAccount(currentInstance, currentToken) .observe(BaseMainActivity.this, mastodonAccount -> { //Initialize static var - currentAccount.mastodon_account = mastodonAccount; - displayReleaseNotesIfNeeded(BaseMainActivity.this, false); - new Thread(() -> { - try { - //Update account in db - new Account(BaseMainActivity.this).insertOrUpdate(currentAccount); - } catch (DBException e) { - e.printStackTrace(); - } - }).start(); + if (mastodonAccount != null) { + currentAccount.mastodon_account = mastodonAccount; + displayReleaseNotesIfNeeded(BaseMainActivity.this, false); + new Thread(() -> { + try { + //Update account in db + new Account(BaseMainActivity.this).insertOrUpdate(currentAccount); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } }); }; @@ -805,10 +811,11 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt String action = intent.getAction(); String type = intent.getType(); Bundle extras = intent.getExtras(); - String userIdIntent, instanceIntent; + String userIdIntent, instanceIntent, urlOfMessage; if (extras != null && extras.containsKey(Helper.INTENT_ACTION)) { userIdIntent = extras.getString(Helper.PREF_KEY_ID); //Id of the account in the intent instanceIntent = extras.getString(Helper.PREF_INSTANCE); + urlOfMessage = extras.getString(Helper.PREF_MESSAGE_URL); if (extras.getInt(Helper.INTENT_ACTION) == Helper.NOTIFICATION_INTENT) { if (userIdIntent != null && instanceIntent != null && userIdIntent.equals(currentUserID) && instanceIntent.equals(currentInstance)) { openNotifications(intent); @@ -834,6 +841,23 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } } else if (extras.getInt(Helper.INTENT_ACTION) == Helper.OPEN_NOTIFICATION) { openNotifications(intent); + } else if (extras.getInt(Helper.INTENT_ACTION) == Helper.OPEN_WITH_ANOTHER_ACCOUNT) { + CrossActionHelper.fetchRemoteStatus(BaseMainActivity.this, MainActivity.currentAccount, urlOfMessage, new CrossActionHelper.Callback() { + @Override + public void federatedStatus(Status status) { + if (status != null) { + Intent intent = new Intent(BaseMainActivity.this, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, status); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + } + + @Override + public void federatedAccount(app.fedilab.android.client.entities.api.Account account) { + + } + }); } } else if (Intent.ACTION_SEND.equals(action) && type != null) { if ("text/plain".equals(type)) { @@ -949,7 +973,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt runOnUiThread(() -> { Bundle b = new Bundle(); b.putString(Helper.ARG_SHARE_URL, url[0]); - b.putString(Helper.ARG_SHARE_URL_MEDIA, finalImage); + if (fetchSharedMedia) { + b.putString(Helper.ARG_SHARE_URL_MEDIA, finalImage); + } b.putString(Helper.ARG_SHARE_TITLE, finalTitle); b.putString(Helper.ARG_SHARE_DESCRIPTION, finalDescription); b.putString(Helper.ARG_SHARE_SUBJECT, sharedSubject); @@ -1029,6 +1055,17 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt Matcher matcherLink = null; matcherLink = link.matcher(url); if (matcherLink.find()) { + if (currentAccount == null) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this); + if (currentToken == null || currentToken.trim().isEmpty()) { + currentToken = sharedpreferences.getString(Helper.PREF_USER_TOKEN, null); + } + try { + currentAccount = new Account(BaseMainActivity.this).getConnectedAccount(); + } catch (DBException e) { + e.printStackTrace(); + } + } if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot CrossActionHelper.fetchRemoteStatus(BaseMainActivity.this, currentAccount, url, new CrossActionHelper.Callback() { @Override diff --git a/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java b/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java index 6cbbe4fd..63257fed 100644 --- a/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java @@ -31,9 +31,9 @@ import androidx.recyclerview.widget.LinearLayoutManager; import java.util.ArrayList; import app.fedilab.android.R; -import app.fedilab.android.client.entities.api.AdminAccount; -import app.fedilab.android.client.entities.api.AdminReport; import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.api.admin.AdminAccount; +import app.fedilab.android.client.entities.api.admin.AdminReport; import app.fedilab.android.databinding.ActivityAdminReportBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; @@ -221,7 +221,7 @@ public class AccountReportActivity extends BaseActivity { binding.email.setVisibility(View.GONE); binding.emailLabel.setVisibility(View.GONE); } - if (accountAdmin.ip == null || accountAdmin.ip.ip.trim().equals("")) { + if (accountAdmin.ip == null || accountAdmin.ip.trim().equals("")) { binding.recentIp.setVisibility(View.GONE); binding.recentIpLabel.setVisibility(View.GONE); } @@ -243,7 +243,7 @@ public class AccountReportActivity extends BaseActivity { binding.emailUser.setVisibility(View.VISIBLE); binding.commentLabel.setVisibility(View.VISIBLE); binding.comment.setVisibility(View.VISIBLE); - binding.recentIp.setText(accountAdmin.ip != null ? accountAdmin.ip.ip : ""); + binding.recentIp.setText(accountAdmin.ip != null ? accountAdmin.ip : ""); binding.disable.setVisibility(View.VISIBLE); binding.suspend.setVisibility(View.VISIBLE); } else { @@ -260,18 +260,9 @@ public class AccountReportActivity extends BaseActivity { } if (accountAdmin.role != null) { - switch (accountAdmin.role) { - case "user": - binding.permissions.setText(getString(R.string.user)); - break; - case "moderator": - binding.permissions.setText(getString(R.string.moderator)); - break; - case "admin": - binding.permissions.setText(getString(R.string.administrator)); - break; - } - if (accountAdmin.role.equals("admin") || accountAdmin.role.equals("moderator")) { + binding.permissions.setText(AdminAccount.permissions.get(accountAdmin.role.permissions)); + binding.permissions.setText(getString(R.string.user)); + if (accountAdmin.role.permissions == 1 || accountAdmin.role.permissions == 400) { binding.warn.setVisibility(View.GONE); binding.suspend.setVisibility(View.GONE); binding.silence.setVisibility(View.GONE); diff --git a/app/src/main/java/app/fedilab/android/activities/ActionActivity.java b/app/src/main/java/app/fedilab/android/activities/ActionActivity.java index 7ff2e7b4..0088f7c3 100644 --- a/app/src/main/java/app/fedilab/android/activities/ActionActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ActionActivity.java @@ -29,6 +29,7 @@ import app.fedilab.android.databinding.ActivityActionsBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonDomainBlock; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; public class ActionActivity extends BaseActivity { @@ -37,6 +38,7 @@ public class ActionActivity extends BaseActivity { private boolean canGoBack; private FragmentMastodonTimeline fragmentMastodonTimeline; private FragmentMastodonAccount fragmentMastodonAccount; + private FragmentMastodonDomainBlock fragmentMastodonDomainBlock; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -54,6 +56,7 @@ public class ActionActivity extends BaseActivity { binding.bookmarks.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BOOKMARK_TIMELINE)); binding.muted.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.MUTED_TIMELINE)); binding.blocked.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_TIMELINE)); + binding.domainBlock.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_DOMAIN_TIMELINE)); } private void displayTimeline(Timeline.TimeLineEnum type) { @@ -73,6 +76,15 @@ public class ActionActivity extends BaseActivity { fragmentTransaction.commit(); }); + } else if (type == Timeline.TimeLineEnum.BLOCKED_DOMAIN_TIMELINE) { + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + fragmentMastodonDomainBlock = new FragmentMastodonDomainBlock(); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonDomainBlock); + fragmentTransaction.commit(); + }); } else { ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { @@ -102,6 +114,9 @@ public class ActionActivity extends BaseActivity { case BOOKMARK_TIMELINE: setTitle(R.string.bookmarks); break; + case BLOCKED_DOMAIN_TIMELINE: + setTitle(R.string.blocked_domains); + break; } } @@ -116,6 +131,9 @@ public class ActionActivity extends BaseActivity { if (fragmentMastodonAccount != null) { fragmentMastodonAccount.onDestroyView(); } + if (fragmentMastodonDomainBlock != null) { + fragmentMastodonDomainBlock.onDestroyView(); + } }); setTitle(R.string.interactions); } else { diff --git a/app/src/main/java/app/fedilab/android/activities/CacheActivity.java b/app/src/main/java/app/fedilab/android/activities/CacheActivity.java index 124c7800..19cc67d4 100644 --- a/app/src/main/java/app/fedilab/android/activities/CacheActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/CacheActivity.java @@ -143,7 +143,11 @@ public class CacheActivity extends BaseActivity { dialogRestart.dismiss(); Helper.restart(CacheActivity.this); }); - restartBuilder.create().show(); + AlertDialog alertDialog = restartBuilder.create(); + if (!isFinishing()) { + alertDialog.show(); + } + })); dialog.dismiss(); }); 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 8db5bb45..78e2eb78 100644 --- a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java @@ -110,6 +110,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana private StatusDraft statusDraft; private ComposeAdapter composeAdapter; private boolean promptSaveDraft; + private boolean restoredDraft; private final BroadcastReceiver imageReceiver = new BroadcastReceiver() { @@ -235,12 +236,14 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana AlertDialog alert = alt_bld.create(); alert.show(); } else { - try { - new StatusDraft(ComposeActivity.this).removeDraft(statusDraft); - finish(); - } catch (DBException e) { - e.printStackTrace(); + if (!restoredDraft) { + try { + new StatusDraft(ComposeActivity.this).removeDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } } + finish(); } } else { finish(); @@ -446,6 +449,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); promptSaveDraft = false; + restoredDraft = false; ActionBar actionBar = getSupportActionBar(); //Remove title if (actionBar != null) { @@ -576,6 +580,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } }); } else if (statusDraft != null) {//Restore a draft with all messages + restoredDraft = true; if (statusDraft.statusReplyList != null) { statusList.addAll(statusDraft.statusReplyList); binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); diff --git a/app/src/main/java/app/fedilab/android/activities/FilterActivity.java b/app/src/main/java/app/fedilab/android/activities/FilterActivity.java index 91d3688c..ed8d616a 100644 --- a/app/src/main/java/app/fedilab/android/activities/FilterActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/FilterActivity.java @@ -106,7 +106,6 @@ public class FilterActivity extends BaseActivity implements FilterAdapter.Delete }); - if (filter != null) { filterParams.filter_action = filter.filter_action; @@ -166,7 +165,9 @@ public class FilterActivity extends BaseActivity implements FilterAdapter.Delete popupAddFilterBinding.lvKeywords.setLayoutManager(new LinearLayoutManager(context)); popupAddFilterBinding.addKeyword.setOnClickListener(v -> { - filterParams.keywords.add(new Filter.KeywordsParams()); + Filter.KeywordsParams keywordsParams = new Filter.KeywordsParams(); + keywordsParams.whole_word = true; + filterParams.keywords.add(keywordsParams); keywordAdapter.notifyItemInserted(filterParams.keywords.size() - 1); }); @@ -276,7 +277,11 @@ public class FilterActivity extends BaseActivity implements FilterAdapter.Delete binding.addFilter.setOnClickListener(v -> addEditFilter(FilterActivity.this, null, filter -> { if (filter != null) { + if (MainActivity.mainFilters == null) { + MainActivity.mainFilters = new ArrayList<>(); + } filterList.add(0, filter); + MainActivity.mainFilters.add(filter); if (filterAdapter != null) { filterAdapter.notifyItemInserted(0); } else { diff --git a/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java b/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java index 9f515608..0f8e3a91 100644 --- a/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java @@ -22,12 +22,17 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.UnderlineSpan; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; @@ -39,6 +44,7 @@ import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.client.entities.api.Instance; import app.fedilab.android.databinding.ActivityInstanceBinding; +import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.viewmodel.mastodon.InstancesVM; @@ -61,6 +67,22 @@ public class InstanceActivity extends BaseActivity { if (getSupportActionBar() != null) getSupportActionBar().hide(); SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(InstanceActivity.this); + + + final SpannableString contentAbout = new SpannableString(getString(R.string.action_about_instance)); + contentAbout.setSpan(new UnderlineSpan(), 0, contentAbout.length(), 0); + contentAbout.setSpan(new ForegroundColorSpan(ContextCompat.getColor(InstanceActivity.this, R.color.cyanea_accent_reference)), 0, contentAbout.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + binding.tos.setText(contentAbout); + + final SpannableString contentPrivacy = new SpannableString(getString(R.string.action_privacy_policy)); + contentPrivacy.setSpan(new UnderlineSpan(), 0, contentPrivacy.length(), 0); + contentPrivacy.setSpan(new ForegroundColorSpan(ContextCompat.getColor(InstanceActivity.this, R.color.cyanea_accent_reference)), 0, contentPrivacy.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + binding.privacy.setText(contentPrivacy); + + binding.tos.setOnClickListener(v -> Helper.openBrowser(InstanceActivity.this, "https://" + MainActivity.currentInstance + "/about")); + binding.privacy.setOnClickListener(v -> Helper.openBrowser(InstanceActivity.this, "https://" + MainActivity.currentInstance + "/privacy-policy")); binding.close.setOnClickListener( view -> { diff --git a/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java b/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java index f4cd975b..4b9d35d7 100644 --- a/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java @@ -15,8 +15,6 @@ package app.fedilab.android.activities; * see . */ -import static app.fedilab.android.helper.PinnedTimelineHelper.sortListPositionAsc; - import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; @@ -39,6 +37,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import app.fedilab.android.BaseMainActivity; @@ -78,6 +77,7 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd private boolean flagLoading; private String max_id; private FragmentMastodonTimeline fragmentMastodonTimeline; + private boolean orderASC; @Override protected void onCreate(Bundle savedInstanceState) { @@ -92,6 +92,7 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); } flagLoading = false; + orderASC = true; max_id = null; accountsVM = new ViewModelProvider(MastodonListActivity.this).get(AccountsVM.class); timelinesVM = new ViewModelProvider(MastodonListActivity.this).get(TimelinesVM.class); @@ -114,7 +115,7 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd } } } - sortListPositionAsc(mastodonListList); + sortAsc(mastodonListList); mastodonListAdapter = new MastodonListAdapter(mastodonListList); mastodonListAdapter.actionOnList = this; binding.notContent.setVisibility(View.GONE); @@ -127,6 +128,19 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd }); } + + private void sortAsc(List mastodonLists) { + Collections.sort(mastodonLists, (obj1, obj2) -> obj1.title.compareToIgnoreCase(obj2.title)); + orderASC = true; + invalidateOptionsMenu(); + } + + private void sortDesc(List mastodonLists) { + Collections.sort(mastodonLists, (obj1, obj2) -> obj2.title.compareToIgnoreCase(obj1.title)); + orderASC = false; + invalidateOptionsMenu(); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { @@ -279,8 +293,15 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd if (mastodonListList == null) { mastodonListList = new ArrayList<>(); } - if (newMastodonList != null && mastodonListAdapter != null) { + if (newMastodonList != null) { mastodonListList.add(0, newMastodonList); + if (mastodonListAdapter == null) { + mastodonListAdapter = new MastodonListAdapter(mastodonListList); + mastodonListAdapter.actionOnList = MastodonListActivity.this; + binding.notContent.setVisibility(View.GONE); + binding.recyclerView.setAdapter(mastodonListAdapter); + binding.recyclerView.setLayoutManager(new LinearLayoutManager(MastodonListActivity.this)); + } mastodonListAdapter.notifyItemInserted(0); } else { Toasty.error(MastodonListActivity.this, getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); @@ -367,6 +388,16 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd }); dialogBuilder.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); dialogBuilder.create().show(); + } else if (item.getItemId() == R.id.action_order) { + if (mastodonListList != null && mastodonListList.size() > 0 && mastodonListAdapter != null) { + if (orderASC) { + sortDesc(mastodonListList); + } else { + sortAsc(mastodonListList); + } + invalidateOptionsMenu(); + mastodonListAdapter.notifyItemRangeChanged(0, mastodonListList.size()); + } } return super.onOptionsItemSelected(item); } @@ -396,6 +427,14 @@ public class MastodonListActivity extends BaseActivity implements MastodonListAd public boolean onCreateOptionsMenu(@NonNull Menu menu) { if (!canGoBack) { getMenuInflater().inflate(R.menu.menu_main_list, menu); + MenuItem order = menu.findItem(R.id.action_order); + if (order != null) { + if (orderASC) { + order.setIcon(R.drawable.ic_baseline_filter_asc_24); + } else { + order.setIcon(R.drawable.ic_baseline_filter_desc_24); + } + } } else { getMenuInflater().inflate(R.menu.menu_list, menu); } 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 4c5e37ad..38be0df2 100644 --- a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java @@ -364,7 +364,7 @@ public class ProfileActivity extends BaseActivity { //Fields for profile List fields = account.fields; if (fields != null && fields.size() > 0) { - FieldAdapter fieldAdapter = new FieldAdapter(fields); + FieldAdapter fieldAdapter = new FieldAdapter(fields, account); binding.fieldsContainer.setAdapter(fieldAdapter); binding.fieldsContainer.setLayoutManager(new LinearLayoutManager(ProfileActivity.this)); } @@ -695,7 +695,6 @@ public class ProfileActivity extends BaseActivity { splitAcct = account.acct.split("@"); } SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ProfileActivity.this); - AlertDialog.Builder builderInner = null; final boolean isOwner = account != null && account.id != null && BaseMainActivity.currentUserID != null && account.id.compareToIgnoreCase(BaseMainActivity.currentUserID) == 0; final String[] stringArrayConf; if (isOwner) { @@ -870,7 +869,7 @@ public class ProfileActivity extends BaseActivity { i++; } builderSingle.setMultiChoiceItems(listsArray, presentArray, (dialog, which, isChecked) -> { - if (!relationship.following) { + if (relationship == null || !relationship.following) { accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, false) .observe(ProfileActivity.this, newRelationShip -> { relationship = newRelationShip; @@ -920,21 +919,43 @@ public class ProfileActivity extends BaseActivity { startActivity(intent); return true; } else if (itemId == R.id.action_mute) { - + AlertDialog.Builder builderInner; if (relationship != null) { - if (relationship.muting) { - builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); - builderInner.setTitle(stringArrayConf[4]); - doActionAccount = action.UNMUTE; + String target; + if (item.getItemId() == R.id.action_block_instance) { + target = account.acct.split("@")[1]; } else { - builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); - builderInner.setTitle(stringArrayConf[0]); - doActionAccount = action.MUTE; + target = account.id; } - } else { - doActionAccount = action.NOTHING; - } + if (relationship.muting) { + accountsVM.unmute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + return true; + } + builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[0]); + builderInner.setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setNegativeButton(R.string.keep_notifications, (dialog, which) -> { + accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target, false, 0) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + }); + builderInner.setPositiveButton(R.string.action_mute, (dialog, which) -> { + accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target, true, 0) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + dialog.dismiss(); + }); + builderInner.show(); + } } else if (itemId == R.id.action_timed_mute) { MastodonHelper.scheduleBoost(ProfileActivity.this, MastodonHelper.ScheduleType.TIMED_MUTED, null, account, rs -> { this.relationship = rs; @@ -942,7 +963,7 @@ public class ProfileActivity extends BaseActivity { }); return true; } else if (itemId == R.id.action_report) { - builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + AlertDialog.Builder builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); builderInner.setTitle(R.string.report_account); //Text for report EditText input = new EditText(ProfileActivity.this); @@ -962,7 +983,7 @@ public class ProfileActivity extends BaseActivity { builderInner.show(); return true; } else if (itemId == R.id.action_block) { - builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + AlertDialog.Builder builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); if (relationship != null) { if (relationship.blocking) { builderInner.setTitle(stringArrayConf[5]); @@ -974,15 +995,6 @@ public class ProfileActivity extends BaseActivity { } else { doActionAccount = action.NOTHING; } - } else if (itemId == R.id.action_block_instance) { - builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); - doActionAccount = action.BLOCK_DOMAIN; - String domain = account.acct.split("@")[1]; - builderInner.setMessage(getString(R.string.block_domain_confirm_message, domain)); - } else { - return true; - } - if (doAction != action.NOTHING && builderInner != null) { builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { String target; @@ -992,20 +1004,6 @@ public class ProfileActivity extends BaseActivity { target = account.id; } switch (doActionAccount) { - case MUTE: - accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target, true, 0) - .observe(ProfileActivity.this, relationShip -> { - this.relationship = relationShip; - updateAccount(); - }); - break; - case UNMUTE: - accountsVM.unmute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target) - .observe(ProfileActivity.this, relationShip -> { - this.relationship = relationShip; - updateAccount(); - }); - break; case BLOCK: accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target) .observe(ProfileActivity.this, relationShip -> { @@ -1020,13 +1018,28 @@ public class ProfileActivity extends BaseActivity { updateAccount(); }); break; - case BLOCK_DOMAIN: - accountsVM.addDomainBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target); - break; } dialog.dismiss(); }); builderInner.show(); + } else if (itemId == R.id.action_block_instance) { + AlertDialog.Builder builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + String domain = account.acct.split("@")[1]; + builderInner.setMessage(getString(R.string.block_domain_confirm_message, domain)); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + String target; + if (item.getItemId() == R.id.action_block_instance) { + target = account.acct.split("@")[1]; + } else { + target = account.id; + } + accountsVM.addDomainBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target); + dialog.dismiss(); + }); + builderInner.show(); + } else { + return true; } return true; } diff --git a/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java b/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java index 2a9ff539..dc1eb145 100644 --- a/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.view.Menu; @@ -43,8 +42,6 @@ import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.snackbar.Snackbar; - import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -62,7 +59,6 @@ import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.helper.itemtouchhelper.OnStartDragListener; -import app.fedilab.android.helper.itemtouchhelper.OnUndoListener; import app.fedilab.android.helper.itemtouchhelper.SimpleItemTouchHelperCallback; import app.fedilab.android.ui.drawer.ReorderBottomMenuAdapter; import app.fedilab.android.ui.drawer.ReorderTabAdapter; @@ -78,7 +74,7 @@ import okhttp3.RequestBody; import okhttp3.Response; -public class ReorderTimelinesActivity extends BaseActivity implements OnStartDragListener, OnUndoListener { +public class ReorderTimelinesActivity extends BaseActivity implements OnStartDragListener { private ItemTouchHelper touchHelper; @@ -132,7 +128,7 @@ public class ReorderTimelinesActivity extends BaseActivity implements OnStartDra update = false; } sortPositionAsc(this.pinned.pinnedTimelines); - reorderTabAdapter = new ReorderTabAdapter(this.pinned, ReorderTimelinesActivity.this, ReorderTimelinesActivity.this); + reorderTabAdapter = new ReorderTabAdapter(this.pinned, ReorderTimelinesActivity.this); ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(reorderTabAdapter); touchHelper = new ItemTouchHelper(callback); @@ -393,51 +389,6 @@ public class ReorderTimelinesActivity extends BaseActivity implements OnStartDra } - @Override - public void onUndo(PinnedTimeline pinnedTimeline, int position) { - - String text = ""; - switch (pinnedTimeline.type) { - case TAG: - text = getString(R.string.reorder_tag_removed); - break; - case REMOTE: - text = getString(R.string.reorder_instance_removed); - break; - case LIST: - text = getString(R.string.reorder_list_deleted); - break; - } - - - Runnable runnable = () -> { - //change position of pinned that are after the removed item - for (int i = pinnedTimeline.position + 1; i < pinned.pinnedTimelines.size(); i++) { - pinned.pinnedTimelines.get(i).position -= 1; - } - pinned.pinnedTimelines.remove(pinnedTimeline); - reorderTabAdapter.notifyItemRemoved(position); - try { - new Pinned(ReorderTimelinesActivity.this).updatePinned(pinned); - changes = true; - } catch (DBException e) { - e.printStackTrace(); - } - }; - Handler handler = new Handler(); - handler.postDelayed(runnable, 4000); - Snackbar.make(binding.getRoot(), text, 4000) - .setAction(getString(R.string.undo), view -> { - pinned.pinnedTimelines.add(position, pinnedTimeline); - reorderTabAdapter.notifyItemInserted(position); - handler.removeCallbacks(runnable); - }) - .setTextColor(ThemeHelper.getAttColor(ReorderTimelinesActivity.this, R.attr.mTextColor)) - .setActionTextColor(ContextCompat.getColor(ReorderTimelinesActivity.this, R.color.cyanea_accent_reference)) - .setBackgroundTint(ContextCompat.getColor(ReorderTimelinesActivity.this, R.color.cyanea_primary_dark_reference)) - .show(); - } - @Override public void onStop() { super.onStop(); 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 bb8c6771..90742677 100644 --- a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java @@ -122,6 +122,9 @@ public class SearchResultTabActivity extends BaseActivity { inflater.inflate(R.menu.menu_search, menu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); + if (search != null) { + searchView.setQuery(search, false); + } searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); searchView.setIconifiedByDefault(false); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { diff --git a/app/src/main/java/app/fedilab/android/activities/SuggestionActivity.java b/app/src/main/java/app/fedilab/android/activities/SuggestionActivity.java new file mode 100644 index 00000000..b9175901 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/SuggestionActivity.java @@ -0,0 +1,61 @@ +package app.fedilab.android.activities; +/* 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.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.core.content.ContextCompat; + +import org.jetbrains.annotations.NotNull; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivitySuggestionsBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonSuggestion; + + +public class SuggestionActivity extends BaseActivity { + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + ActivitySuggestionsBinding binding = ActivitySuggestionsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); + } + + Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_suggestions, new FragmentMastodonSuggestion(), null, null, null); + } + + + @Override + public boolean onOptionsItemSelected(@NotNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java b/app/src/main/java/app/fedilab/android/activities/admin/AdminAccountActivity.java similarity index 97% rename from app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java rename to app/src/main/java/app/fedilab/android/activities/admin/AdminAccountActivity.java index db0732a5..c40cace7 100644 --- a/app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/admin/AdminAccountActivity.java @@ -1,4 +1,4 @@ -package app.fedilab.android.activities; +package app.fedilab.android.activities.admin; /* Copyright 2022 Thomas Schneider * * This file is a part of Fedilab @@ -54,8 +54,13 @@ import java.util.concurrent.TimeUnit; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; -import app.fedilab.android.client.entities.api.AdminAccount; +import app.fedilab.android.activities.BaseActivity; +import app.fedilab.android.activities.InstanceProfileActivity; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.activities.MediaActivity; import app.fedilab.android.client.entities.api.Attachment; +import app.fedilab.android.client.entities.api.admin.AdminAccount; +import app.fedilab.android.client.entities.api.admin.AdminIp; import app.fedilab.android.databinding.ActivityAdminAccountBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; @@ -204,7 +209,7 @@ public class AdminAccountActivity extends BaseActivity { StringBuilder lastActive = new StringBuilder(); if (adminAccount.ips != null) { int count = 0; - for (AdminAccount.IP ip : adminAccount.ips) { + for (AdminIp ip : adminAccount.ips) { lastActive.append(Helper.shortDateToString(ip.used_at)).append(" - ").append(ip.ip).append("\r\n"); count++; if (count > 4) { diff --git a/app/src/main/java/app/fedilab/android/activities/AdminActionActivity.java b/app/src/main/java/app/fedilab/android/activities/admin/AdminActionActivity.java similarity index 80% rename from app/src/main/java/app/fedilab/android/activities/AdminActionActivity.java rename to app/src/main/java/app/fedilab/android/activities/admin/AdminActionActivity.java index 30a3abab..a703ff41 100644 --- a/app/src/main/java/app/fedilab/android/activities/AdminActionActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/admin/AdminActionActivity.java @@ -1,4 +1,4 @@ -package app.fedilab.android.activities; +package app.fedilab.android.activities.admin; /* Copyright 2022 Thomas Schneider * * This file is a part of Fedilab @@ -14,8 +14,14 @@ package app.fedilab.android.activities; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ -import static app.fedilab.android.activities.AdminActionActivity.AdminEnum.REPORT; +import static app.fedilab.android.activities.admin.AdminActionActivity.AdminEnum.ACCOUNT; +import static app.fedilab.android.activities.admin.AdminActionActivity.AdminEnum.DOMAIN; +import static app.fedilab.android.activities.admin.AdminActionActivity.AdminEnum.REPORT; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.view.Menu; @@ -27,16 +33,20 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.google.gson.annotations.SerializedName; import app.fedilab.android.R; +import app.fedilab.android.activities.BaseActivity; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlock; import app.fedilab.android.databinding.ActivityAdminActionsBinding; import app.fedilab.android.databinding.PopupAdminFilterAccountsBinding; import app.fedilab.android.databinding.PopupAdminFilterReportsBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.fragment.admin.FragmentAdminAccount; +import app.fedilab.android.ui.fragment.admin.FragmentAdminDomain; import app.fedilab.android.ui.fragment.admin.FragmentAdminReport; public class AdminActionActivity extends BaseActivity { @@ -47,6 +57,26 @@ public class AdminActionActivity extends BaseActivity { private boolean canGoBack; private FragmentAdminReport fragmentAdminReport; private FragmentAdminAccount fragmentAdminAccount; + private FragmentAdminDomain fragmentAdminDomain; + + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Bundle b = intent.getExtras(); + if (b != null) { + AdminDomainBlock adminDomainBlock = (AdminDomainBlock) b.getSerializable(Helper.ARG_ADMIN_DOMAINBLOCK); + AdminDomainBlock adminDomainBlockDelete = (AdminDomainBlock) b.getSerializable(Helper.ARG_ADMIN_DOMAINBLOCK_DELETE); + if (adminDomainBlock != null && adminDomainBlock.domain != null && fragmentAdminDomain != null) { + fragmentAdminDomain.update(adminDomainBlock); + } + if (adminDomainBlockDelete != null && fragmentAdminDomain != null) { + fragmentAdminDomain.delete(adminDomainBlockDelete); + } + } + + } + }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -54,20 +84,21 @@ public class AdminActionActivity extends BaseActivity { ThemeHelper.applyThemeBar(this); binding = ActivityAdminActionsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - + LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(Helper.BROADCAST_DATA)); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); } canGoBack = false; binding.reports.setOnClickListener(v -> displayTimeline(REPORT)); - binding.accounts.setOnClickListener(v -> displayTimeline(AdminEnum.ACCOUNT)); + binding.accounts.setOnClickListener(v -> displayTimeline(ACCOUNT)); + binding.domains.setOnClickListener(v -> displayTimeline(DOMAIN)); + } private void displayTimeline(AdminEnum type) { canGoBack = true; if (type == REPORT) { - ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { fragmentAdminReport = new FragmentAdminReport(); Bundle bundle = new Bundle(); @@ -80,9 +111,7 @@ public class AdminActionActivity extends BaseActivity { fragmentTransaction.replace(R.id.fragment_container, fragmentAdminReport); fragmentTransaction.commit(); }); - - } else { - + } else if (type == ACCOUNT) { ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { fragmentAdminAccount = new FragmentAdminAccount(); Bundle bundle = new Bundle(); @@ -95,7 +124,19 @@ public class AdminActionActivity extends BaseActivity { fragmentTransaction.replace(R.id.fragment_container, fragmentAdminAccount); fragmentTransaction.commit(); }); - + } else if (type == DOMAIN) { + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + fragmentAdminDomain = new FragmentAdminDomain(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type); + bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + type.getValue()); + fragmentAdminDomain.setArguments(bundle); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragmentAdminDomain); + fragmentTransaction.commit(); + }); } switch (type) { case REPORT: @@ -104,13 +145,16 @@ public class AdminActionActivity extends BaseActivity { case ACCOUNT: setTitle(R.string.accounts); break; + case DOMAIN: + setTitle(R.string.domains); + break; } invalidateOptionsMenu(); } @Override public boolean onCreateOptionsMenu(@NonNull Menu menu) { - if (canGoBack) { + if (canGoBack && fragmentAdminAccount != null) { getMenuInflater().inflate(R.menu.menu_admin_account, menu); } return super.onCreateOptionsMenu(menu); @@ -272,6 +316,14 @@ public class AdminActionActivity extends BaseActivity { return super.onOptionsItemSelected(item); } + @Override + protected void onDestroy() { + super.onDestroy(); + if (mReceiver != null) { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); + } + } + @Override public void onBackPressed() { if (canGoBack) { @@ -279,9 +331,15 @@ public class AdminActionActivity extends BaseActivity { ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.buttonContainer, () -> { if (fragmentAdminReport != null) { fragmentAdminReport.onDestroyView(); + fragmentAdminReport = null; } if (fragmentAdminAccount != null) { fragmentAdminAccount.onDestroyView(); + fragmentAdminAccount = null; + } + if (fragmentAdminDomain != null) { + fragmentAdminDomain.onDestroyView(); + fragmentAdminDomain = null; } setTitle(R.string.administration); invalidateOptionsMenu(); @@ -296,8 +354,9 @@ public class AdminActionActivity extends BaseActivity { @SerializedName("REPORT") REPORT("REPORT"), @SerializedName("ACCOUNT") - ACCOUNT("ACCOUNT"); - + ACCOUNT("ACCOUNT"), + @SerializedName("DOMAIN") + DOMAIN("DOMAIN"); private final String value; AdminEnum(String value) { diff --git a/app/src/main/java/app/fedilab/android/activities/admin/AdminDomainBlockActivity.java b/app/src/main/java/app/fedilab/android/activities/admin/AdminDomainBlockActivity.java new file mode 100644 index 00000000..c14affb4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/admin/AdminDomainBlockActivity.java @@ -0,0 +1,159 @@ +package app.fedilab.android.activities.admin; +/* 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 static app.fedilab.android.helper.Helper.BROADCAST_DATA; + +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import app.fedilab.android.R; +import app.fedilab.android.activities.BaseActivity; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlock; +import app.fedilab.android.databinding.ActivityAdminDomainblockBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.viewmodel.mastodon.AdminVM; +import es.dmoral.toasty.Toasty; + +public class AdminDomainBlockActivity extends BaseActivity { + + + private final String[] severityChoices = {"silence", "suspend", "noop"}; + private AdminVM adminVM; + private AdminDomainBlock adminDomainBlock; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + ActivityAdminDomainblockBinding binding = ActivityAdminDomainblockBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); + } + Bundle b = getIntent().getExtras(); + if (b != null) { + adminDomainBlock = (AdminDomainBlock) b.getSerializable(Helper.ARG_ADMIN_DOMAINBLOCK); + } + + ArrayAdapter adapterResize = ArrayAdapter.createFromResource(this, + R.array.admin_block_severity, android.R.layout.simple_spinner_dropdown_item); + binding.severity.setAdapter(adapterResize); + + if (adminDomainBlock != null) { + binding.domain.setText(adminDomainBlock.domain); + binding.domain.setEnabled(false); + for (int i = 0; i < severityChoices.length; i++) { + if (adminDomainBlock.severity.equalsIgnoreCase(severityChoices[i])) { + binding.severity.setSelection(i, false); + break; + } + } + binding.obfuscate.setChecked(adminDomainBlock.obfuscate); + binding.rejectMedia.setChecked(adminDomainBlock.reject_media); + binding.rejectReports.setChecked(adminDomainBlock.reject_reports); + binding.privateComment.setText(adminDomainBlock.private_comment); + binding.publicComment.setText(adminDomainBlock.public_comment); + } else { + adminDomainBlock = new AdminDomainBlock(); + } + + binding.severity.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, long l) { + adminDomainBlock.severity = severityChoices[position]; + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + } + }); + binding.obfuscate.setOnCheckedChangeListener((compoundButton, checked) -> adminDomainBlock.obfuscate = checked); + binding.rejectMedia.setOnCheckedChangeListener((compoundButton, checked) -> adminDomainBlock.reject_media = checked); + binding.rejectReports.setOnCheckedChangeListener((compoundButton, checked) -> adminDomainBlock.reject_reports = checked); + adminVM = new ViewModelProvider(AdminDomainBlockActivity.this).get(AdminVM.class); + binding.saveChanges.setOnClickListener(v -> { + adminDomainBlock.domain = binding.domain.getText().toString().trim(); + adminDomainBlock.public_comment = binding.publicComment.getText().toString().trim(); + adminDomainBlock.private_comment = binding.privateComment.getText().toString().trim(); + adminVM.createOrUpdateDomainBlock(MainActivity.currentInstance, MainActivity.currentToken, adminDomainBlock) + .observe(AdminDomainBlockActivity.this, adminDomainBlockResult -> { + if (adminDomainBlockResult != null) { + Toasty.success(AdminDomainBlockActivity.this, getString(R.string.saved_changes), Toasty.LENGTH_SHORT).show(); + } else { + Toasty.error(AdminDomainBlockActivity.this, getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); + } + Intent intent = new Intent(BROADCAST_DATA).putExtra(Helper.ARG_ADMIN_DOMAINBLOCK, adminDomainBlockResult); + LocalBroadcastManager.getInstance(AdminDomainBlockActivity.this).sendBroadcast(intent); + finish(); + } + ); + }); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_admin_domain, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } else if (itemId == R.id.action_delete) { + if (adminDomainBlock.id != null) { + AlertDialog.Builder builder = new AlertDialog.Builder(AdminDomainBlockActivity.this, Helper.dialogStyle()); + builder.setMessage(getString(R.string.unblock_domain_confirm, adminDomainBlock.domain)); + builder + .setPositiveButton(R.string.unblock_domain, (dialog, which) -> { + adminVM.deleteDomain(MainActivity.currentInstance, MainActivity.currentToken, adminDomainBlock.id) + .observe(AdminDomainBlockActivity.this, adminDomainBlockResult -> { + Intent intent = new Intent(BROADCAST_DATA).putExtra(Helper.ARG_ADMIN_DOMAINBLOCK_DELETE, adminDomainBlock); + LocalBroadcastManager.getInstance(AdminDomainBlockActivity.this).sendBroadcast(intent); + finish(); + } + ); + dialog.dismiss(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + } else { + finish(); + } + + } + return true; + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java b/app/src/main/java/app/fedilab/android/activities/admin/AdminReportActivity.java similarity index 98% rename from app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java rename to app/src/main/java/app/fedilab/android/activities/admin/AdminReportActivity.java index 74afa790..c74897c0 100644 --- a/app/src/main/java/app/fedilab/android/activities/AdminReportActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/admin/AdminReportActivity.java @@ -1,4 +1,4 @@ -package app.fedilab.android.activities; +package app.fedilab.android.activities.admin; /* Copyright 2022 Thomas Schneider * * This file is a part of Fedilab @@ -55,9 +55,13 @@ import java.util.concurrent.TimeUnit; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; +import app.fedilab.android.activities.BaseActivity; +import app.fedilab.android.activities.InstanceProfileActivity; +import app.fedilab.android.activities.MediaActivity; import app.fedilab.android.client.entities.api.Account; -import app.fedilab.android.client.entities.api.AdminAccount; import app.fedilab.android.client.entities.api.Attachment; +import app.fedilab.android.client.entities.api.admin.AdminAccount; +import app.fedilab.android.client.entities.api.admin.AdminIp; import app.fedilab.android.databinding.ActivityAdminAccountBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; @@ -141,7 +145,7 @@ public class AdminReportActivity extends BaseActivity { binding.email.setText(adminAccount.email); StringBuilder lastActive = new StringBuilder(); if (adminAccount.ips != null) { - for (AdminAccount.IP ip : adminAccount.ips) { + for (AdminIp ip : adminAccount.ips) { lastActive.append(Helper.shortDateToString(ip.used_at)).append(" - ").append(ip.ip).append("\r\n"); } } diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java index b215079b..a2ec0d5f 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java @@ -25,6 +25,7 @@ import app.fedilab.android.client.entities.api.Preferences; import app.fedilab.android.client.entities.api.RelationShip; import app.fedilab.android.client.entities.api.Report; import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.api.Suggestion; import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.api.Token; import okhttp3.MultipartBody; @@ -315,7 +316,7 @@ public interface MastodonAccountsService { @DELETE("domain_blocks") Call removeDomainBlocks( @Header("Authorization") String token, - @Field("domain") String domain + @Query("domain") String domain ); @@ -391,7 +392,7 @@ public interface MastodonAccountsService { //Get user suggestions @GET("suggestions") - Call> getSuggestions( + Call> getSuggestions( @Header("Authorization") String token, @Query("limit") String limit ); diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAdminService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAdminService.java index 0cef6998..eeacfde6 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAdminService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAdminService.java @@ -17,14 +17,17 @@ package app.fedilab.android.client.endpoints; import java.util.List; -import app.fedilab.android.client.entities.api.AdminAccount; -import app.fedilab.android.client.entities.api.AdminReport; +import app.fedilab.android.client.entities.api.admin.AdminAccount; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlock; +import app.fedilab.android.client.entities.api.admin.AdminReport; import retrofit2.Call; +import retrofit2.http.DELETE; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.POST; +import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; @@ -115,6 +118,7 @@ public interface MastodonAdminService { @Query("limit") int limit ); + //***************** ADMIN REPORTS ************** @GET("admin/reports/{id}") Call getReport( @@ -149,4 +153,80 @@ public interface MastodonAdminService { @Header("Authorization") String app_token, @Path("id") String id ); + + + //*************** ADMIN DOMAINS **************** + + @GET("admin/domain_blocks") + Call> getDomainBlocks( + @Header("Authorization") String token, + @Query("max_id") String max_id, + @Query("limit") int limit + ); + + @GET("admin/domain_allows") + Call> getDomainAllows( + @Header("Authorization") String token, + @Query("max_id") String max_id, + @Query("limit") int limit + ); + + @GET("admin/domain_blocks/{id}") + Call getDomainBlock( + @Header("Authorization") String token, + @Path("id") String id + ); + + @GET("admin/domain_allows/{id}") + Call getDomainAllow( + @Header("Authorization") String token, + @Path("id") String id + ); + + + @FormUrlEncoded + @POST("admin/domain_blocks") + Call blockDomain( + @Header("Authorization") String app_token, + @Field("domain") String domain, + @Field("severity") String severity, + @Field("reject_media") Boolean reject_media, + @Field("reject_reports") Boolean reject_reports, + @Field("private_comment") String private_comment, + @Field("public_comment") String public_comment, + @Field("obfuscate") Boolean obfuscate + ); + + @FormUrlEncoded + @POST("admin/domain_allows") + Call allowDomain( + @Header("Authorization") String app_token, + @Path("domain") String domain + ); + + @FormUrlEncoded + @PUT("admin/domain_blocks/{id}") + Call updateBlockDomain( + @Header("Authorization") String app_token, + @Path("id") String id, + @Field("severity") String severity, + @Field("reject_media") Boolean reject_media, + @Field("reject_reports") Boolean reject_reports, + @Field("private_comment") String private_comment, + @Field("public_comment") String public_comment, + @Field("obfuscate") Boolean obfuscate + ); + + @DELETE("admin/domain_blocks/{id}") + Call deleteBlockDomain( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + + @DELETE("admin/domain_allows/{id}") + Call deleteAllowDomain( + @Header("Authorization") String app_token, + @Path("id") String id + ); } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Domains.java b/app/src/main/java/app/fedilab/android/client/entities/api/Domains.java new file mode 100644 index 00000000..cbcb63c3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Domains.java @@ -0,0 +1,22 @@ +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 java.util.List; + +public class Domains { + public Pagination pagination = new Pagination(); + public List domains; +} 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 f996b9a4..e3c75305 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 @@ -127,11 +127,7 @@ public class Status implements Serializable, Cloneable { public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference) { return SpannableHelper.convert(context, content, this, null, null, true, viewWeakReference); } - //Some extra spannable element - They will be filled automatically when fetching the status - 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); diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Suggestion.java b/app/src/main/java/app/fedilab/android/client/entities/api/Suggestion.java new file mode 100644 index 00000000..d60128dd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Suggestion.java @@ -0,0 +1,27 @@ +package app.fedilab.android.client.entities.api; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/* 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 . */ + +public class Suggestion implements Serializable { + @SerializedName("account") + public Account account; + @SerializedName("source") + public String source; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Suggestions.java b/app/src/main/java/app/fedilab/android/client/entities/api/Suggestions.java new file mode 100644 index 00000000..1b29d7f6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Suggestions.java @@ -0,0 +1,22 @@ +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 java.util.List; + +public class Suggestions { + public Pagination pagination = new Pagination(); + public List suggestions; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/AdminAccount.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java similarity index 51% rename from app/src/main/java/app/fedilab/android/client/entities/api/AdminAccount.java rename to app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java index 0dcaf923..26eb86a7 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/AdminAccount.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java @@ -1,4 +1,4 @@ -package app.fedilab.android.client.entities.api; +package app.fedilab.android.client.entities.api.admin; /* Copyright 2021 Thomas Schneider * * This file is a part of Fedilab @@ -18,8 +18,11 @@ import com.google.gson.annotations.SerializedName; import java.io.Serializable; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; +import app.fedilab.android.client.entities.api.Account; + public class AdminAccount implements Serializable { @SerializedName("id") @@ -32,26 +35,48 @@ public class AdminAccount implements Serializable { public Date created_at; @SerializedName("email") public String email; + public static LinkedHashMap permissions; + + static { + permissions = new LinkedHashMap<>(); + permissions.put(1, "Administrator"); + permissions.put(2, "Devops"); + permissions.put(4, "View Audit Log"); + permissions.put(8, "View Dashboard"); + permissions.put(10, "Manage Reports"); + permissions.put(20, "Manage Federation"); + permissions.put(40, "Manage Settings"); + permissions.put(80, "Manage Blocks"); + permissions.put(100, "Manage Taxonomies"); + permissions.put(200, "Manage Appeals"); + permissions.put(400, "Manage Users"); + permissions.put(800, "Manage Invites"); + permissions.put(1000, "Manage Rules"); + permissions.put(2000, "Manage Announcements"); + permissions.put(4000, "Manage Custom Emojis"); + permissions.put(8000, "Manage Webhooks"); + permissions.put(10000, "Invite Users"); + permissions.put(20000, "Manage Roles"); + permissions.put(40000, "Manage User Access"); + permissions.put(80000, "Delete User Data"); + } + @SerializedName("ip") - public IP ip; - @SerializedName("ips") - public List ips; - @SerializedName("locale") - public String locale; - @SerializedName("invite_request") - public String invite_request; + public String ip; @SerializedName("role") - public String role; + public Role role; @SerializedName("confirmed") public boolean confirmed; - @SerializedName("approved") - public boolean approved; - @SerializedName("disabled") - public boolean disabled; - @SerializedName("silenced") - public boolean silenced; @SerializedName("suspended") public boolean suspended; + @SerializedName("silenced") + public boolean silenced; + @SerializedName("disabled") + public boolean disabled; + @SerializedName("approved") + public boolean approved; + @SerializedName("ips") + public List ips; @SerializedName("account") public Account account; @SerializedName("created_by_application_id") @@ -60,12 +85,28 @@ public class AdminAccount implements Serializable { public String invited_by_account_id; - public static class IP implements Serializable { + @SerializedName("locale") + public String locale; + @SerializedName("invite_request") + public String invite_request; + + public static class Role implements Serializable { @SerializedName("ip") public String ip; - @SerializedName("used_at") - public Date used_at; - @SerializedName("user_id") - public String user_id; + @SerializedName("name") + public String name; + @SerializedName("color") + public String color; + @SerializedName("position") + public long position; + @SerializedName("permissions") + public int permissions; + @SerializedName("highlighted") + public boolean highlighted; + @SerializedName("created_at") + public Date created_at; + @SerializedName("updated_at") + public Date updated_at; } + } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/AdminAccounts.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccounts.java similarity index 88% rename from app/src/main/java/app/fedilab/android/client/entities/api/AdminAccounts.java rename to app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccounts.java index eed98dcd..02fc4b82 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/AdminAccounts.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccounts.java @@ -1,4 +1,4 @@ -package app.fedilab.android.client.entities.api; +package app.fedilab.android.client.entities.api.admin; /* Copyright 2021 Thomas Schneider * * This file is a part of Fedilab @@ -17,6 +17,8 @@ package app.fedilab.android.client.entities.api; import java.util.List; +import app.fedilab.android.client.entities.api.Pagination; + public class AdminAccounts { public Pagination pagination = new Pagination(); diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminDomainBlock.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminDomainBlock.java new file mode 100644 index 00000000..e25b9b15 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminDomainBlock.java @@ -0,0 +1,43 @@ +package app.fedilab.android.client.entities.api.admin; +/* 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.Date; + +public class AdminDomainBlock implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("domain") + public String domain; + @SerializedName("created_at") + public Date created_at; + @SerializedName("severity") + public String severity; + @SerializedName("reject_media") + public boolean reject_media; + @SerializedName("reject_reports") + public boolean reject_reports; + @SerializedName("private_comment") + public String private_comment; + @SerializedName("public_comment") + public String public_comment; + @SerializedName("obfuscate") + public boolean obfuscate; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminDomainBlocks.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminDomainBlocks.java new file mode 100644 index 00000000..451da28d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminDomainBlocks.java @@ -0,0 +1,26 @@ +package app.fedilab.android.client.entities.api.admin; +/* 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 java.util.List; + +import app.fedilab.android.client.entities.api.Pagination; + +public class AdminDomainBlocks { + + public Pagination pagination = new Pagination(); + public List adminDomainBlocks; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminEmailDomainBlock.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminEmailDomainBlock.java new file mode 100644 index 00000000..e72e649b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminEmailDomainBlock.java @@ -0,0 +1,36 @@ +package app.fedilab.android.client.entities.api.admin; +/* 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.Date; +import java.util.List; + +import app.fedilab.android.client.entities.api.History; + +public class AdminEmailDomainBlock implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("domain") + public String domain; + @SerializedName("created_at") + public Date created_at; + @SerializedName("history") + public List history; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminIp.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminIp.java new file mode 100644 index 00000000..9962b481 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminIp.java @@ -0,0 +1,29 @@ +package app.fedilab.android.client.entities.api.admin; +/* 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.Date; + +public class AdminIp implements Serializable { + + @SerializedName("ip") + public String ip; + @SerializedName("used_at") + public Date used_at; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminIpBlock.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminIpBlock.java new file mode 100644 index 00000000..24d1af15 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminIpBlock.java @@ -0,0 +1,37 @@ +package app.fedilab.android.client.entities.api.admin; +/* 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.Date; + +public class AdminIpBlock implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("ip") + public String ip; + @SerializedName("severity") + public String severity; + @SerializedName("comment") + public String comment; + @SerializedName("created_at") + public Date created_at; + @SerializedName("expires_at") + public Date expires_at; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminMeasure.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminMeasure.java new file mode 100644 index 00000000..6e33b2d8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminMeasure.java @@ -0,0 +1,45 @@ +package app.fedilab.android.client.entities.api.admin; +/* 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.Date; +import java.util.List; + +public class AdminMeasure implements Serializable { + + @SerializedName("key") + public String key; + @SerializedName("unit") + public String unit; + @SerializedName("total") + public int total; + @SerializedName("human_value") + public int human_value; + @SerializedName("previous_total") + public int previous_total; + @SerializedName("data") + public List data; + + public static class Data implements Serializable { + @SerializedName("date") + public Date date; + @SerializedName("value") + public int value; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/AdminReport.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminReport.java similarity index 84% rename from app/src/main/java/app/fedilab/android/client/entities/api/AdminReport.java rename to app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminReport.java index 92eaea06..988ace1b 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/AdminReport.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminReport.java @@ -1,4 +1,4 @@ -package app.fedilab.android.client.entities.api; +package app.fedilab.android.client.entities.api.admin; /* Copyright 2021 Thomas Schneider * * This file is a part of Fedilab @@ -20,30 +20,37 @@ import java.io.Serializable; import java.util.Date; import java.util.List; +import app.fedilab.android.client.entities.api.Instance; +import app.fedilab.android.client.entities.api.Status; + public class AdminReport implements Serializable { @SerializedName("id") public String id; - @SerializedName("account") - public AdminAccount account; @SerializedName("action_taken") public Boolean action_taken; - @SerializedName("action_taken_by_account") - public AdminAccount action_taken_by_account; - @SerializedName("assigned_account") - public AdminAccount assigned_account; + @SerializedName("action_taken_at") + public Date action_taken_at; @SerializedName("category") public String category; @SerializedName("comment") public String comment; + @SerializedName("forwarded") + public boolean forwarded; @SerializedName("created_at") public Date created_at; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("account") + public AdminAccount account; @SerializedName("target_account") public AdminAccount target_account; + @SerializedName("assigned_account") + public AdminAccount assigned_account; + @SerializedName("action_taken_by_account") + public AdminAccount action_taken_by_account; @SerializedName("statuses") public List statuses; @SerializedName("rules") public List rules; - @SerializedName("updated_at") - public Date updated_at; } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/AdminReports.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminReports.java similarity index 88% rename from app/src/main/java/app/fedilab/android/client/entities/api/AdminReports.java rename to app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminReports.java index e7674d3a..e6b61481 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/AdminReports.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminReports.java @@ -1,4 +1,4 @@ -package app.fedilab.android.client.entities.api; +package app.fedilab.android.client.entities.api.admin; /* Copyright 2021 Thomas Schneider * * This file is a part of Fedilab @@ -17,6 +17,8 @@ package app.fedilab.android.client.entities.api; import java.util.List; +import app.fedilab.android.client.entities.api.Pagination; + public class AdminReports { public Pagination pagination = new Pagination(); diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java b/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java index 655dbc1e..62672258 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java @@ -24,18 +24,13 @@ import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; import java.util.Date; import java.util.List; import app.fedilab.android.activities.MainActivity; import app.fedilab.android.client.entities.api.Conversation; -import app.fedilab.android.client.entities.api.Conversations; import app.fedilab.android.client.entities.api.Notification; -import app.fedilab.android.client.entities.api.Notifications; -import app.fedilab.android.client.entities.api.Pagination; import app.fedilab.android.client.entities.api.Status; -import app.fedilab.android.client.entities.api.Statuses; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; @@ -247,7 +242,6 @@ public class StatusCache { } Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_STATUS_CACHE + " where " + Sqlite.COL_TYPE + " != '" + Timeline.TimeLineEnum.HOME.getValue() + "'" - + " AND " + Sqlite.COL_INSTANCE + " = '" + baseAccount.instance + "'" + " AND " + Sqlite.COL_USER_ID + "= '" + baseAccount.user_id + "'", null); mCount.moveToFirst(); int count = mCount.getInt(0); @@ -266,36 +260,21 @@ public class StatusCache { if (db == null) { throw new DBException("db is null. Wrong initialization."); } - Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_STATUS_CACHE + String query = "select count(*) from " + Sqlite.TABLE_STATUS_CACHE + " where " + Sqlite.COL_STATUS_ID + " = '" + statusCache.status_id + "'" + " AND " + Sqlite.COL_INSTANCE + " = '" + statusCache.instance + "'" - + " AND " + Sqlite.COL_USER_ID + "= '" + statusCache.user_id + "'", null); + + " AND " + Sqlite.COL_USER_ID + "= '" + statusCache.user_id + "'"; + if (statusCache.type != null) { + query += " AND " + Sqlite.COL_TYPE + " = '" + statusCache.type.getValue() + "'"; + } + + Cursor mCount = db.rawQuery(query, null); mCount.moveToFirst(); int count = mCount.getInt(0); mCount.close(); return (count > 0); } - /** - * Check if a status exists in db - * - * @param status Status {@link Status} - * @return boolean - StatusCache exists - * @throws DBException Exception - */ - public boolean statusExist(Status status) throws DBException { - if (db == null) { - throw new DBException("db is null. Wrong initialization."); - } - Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_STATUS_CACHE - + " where " + Sqlite.COL_STATUS_ID + " = '" + status.id + "'" - + " AND " + Sqlite.COL_INSTANCE + " = '" + MainActivity.currentInstance + "'" - + " AND " + Sqlite.COL_USER_ID + "= '" + MainActivity.currentUserID + "'", null); - mCount.moveToFirst(); - int count = mCount.getInt(0); - mCount.close(); - return (count > 0); - } /** * Insert a status in db @@ -313,7 +292,9 @@ public class StatusCache { values.put(Sqlite.COL_INSTANCE, statusCache.instance); values.put(Sqlite.COL_SLUG, slug); values.put(Sqlite.COL_STATUS_ID, statusCache.status_id); - values.put(Sqlite.COL_TYPE, statusCache.type.getValue()); + if (statusCache.type != null) { + values.put(Sqlite.COL_TYPE, statusCache.type.getValue()); + } if (statusCache.status != null) { values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status)); } @@ -345,8 +326,6 @@ public class StatusCache { throw new DBException("db is null. Wrong initialization."); } ContentValues values = new ContentValues(); - values.put(Sqlite.COL_USER_ID, statusCache.user_id); - values.put(Sqlite.COL_STATUS_ID, statusCache.status_id); if (statusCache.status != null) { values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status)); } @@ -360,8 +339,8 @@ public class StatusCache { //Inserts token try { return db.update(Sqlite.TABLE_STATUS_CACHE, - values, Sqlite.COL_STATUS_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =?", - new String[]{statusCache.status_id, statusCache.instance}); + values, Sqlite.COL_STATUS_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =? AND " + Sqlite.COL_TYPE + " =? AND " + Sqlite.COL_USER_ID + " =?", + new String[]{statusCache.status_id, statusCache.instance, statusCache.type != null ? statusCache.type.getValue() : "", statusCache.user_id}); } catch (Exception e) { e.printStackTrace(); return -1; @@ -485,8 +464,8 @@ public class StatusCache { } try { return db.delete(Sqlite.TABLE_STATUS_CACHE, - Sqlite.COL_TYPE + " != ? AND " + Sqlite.COL_USER_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =?", - new String[]{Timeline.TimeLineEnum.HOME.getValue(), account.user_id, account.instance}); + Sqlite.COL_TYPE + " != ? AND " + Sqlite.COL_USER_ID + " = ?", + new String[]{Timeline.TimeLineEnum.HOME.getValue(), account.user_id}); } catch (Exception e) { e.printStackTrace(); return -1; @@ -543,10 +522,10 @@ public class StatusCache { * @param user_id String - us * @param max_id String - status having max id * @param min_id String - status having min id - * @return Statuses + * @return List * @throws DBException - throws a db exception */ - public Notifications getNotifications(List exclude_type, String instance, String user_id, String max_id, String min_id, String since_id) throws DBException { + public List getNotifications(List exclude_type, String instance, String user_id, String max_id, String min_id, String since_id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } @@ -572,8 +551,8 @@ public class StatusCache { selection += "AND " + Sqlite.COL_SLUG + " NOT IN (" + exclude + ") "; } try { - Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, Sqlite.COL_STATUS_ID, null, Sqlite.COL_STATUS_ID + order, limit); - return createNotificationReply(cursorToListOfNotifications(c)); + Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, Sqlite.COL_STATUS_ID, null, Sqlite.COL_STATUS_ID + " + 0 " + order, limit); + return cursorToListOfNotifications(c); } catch (Exception e) { e.printStackTrace(); return null; @@ -588,10 +567,10 @@ public class StatusCache { * @param user_id String - us * @param max_id String - status having max id * @param min_id String - status having min id - * @return Statuses + * @return List * @throws DBException - throws a db exception */ - public Conversations getConversations(String instance, String user_id, String max_id, String min_id, String since_id) throws DBException { + public List getConversations(String instance, String user_id, String max_id, String min_id, String since_id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } @@ -607,10 +586,9 @@ public class StatusCache { selection += "AND " + Sqlite.COL_STATUS_ID + " > '" + since_id + "' "; limit = null; } - try { - Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, Sqlite.COL_STATUS_ID, null, Sqlite.COL_STATUS_ID + order, limit); - return createConversationReply(cursorToListOfConversations(c)); + Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, Sqlite.COL_STATUS_ID, null, Sqlite.COL_STATUS_ID + " + 0 " + order, limit); + return cursorToListOfConversations(c); } catch (Exception e) { e.printStackTrace(); return null; @@ -625,10 +603,10 @@ public class StatusCache { * @param user_id String - us * @param max_id String - status having max id * @param min_id String - status having min id - * @return Statuses + * @return List * @throws DBException - throws a db exception */ - public Statuses geStatuses(String slug, String instance, String user_id, String max_id, String min_id, String since_id) throws DBException { + public List geStatuses(String slug, String instance, String user_id, String max_id, String min_id, String since_id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } @@ -645,8 +623,8 @@ public class StatusCache { limit = null; } try { - Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, Sqlite.COL_STATUS_ID, null, Sqlite.COL_STATUS_ID + order, limit); - return createStatusReply(cursorToListOfStatuses(c)); + Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, Sqlite.COL_STATUS_ID, null, Sqlite.COL_STATUS_ID + " + 0 " + order, limit); + return cursorToListOfStatuses(c); } catch (Exception e) { e.printStackTrace(); return null; @@ -769,75 +747,6 @@ public class StatusCache { return conversationList; } - /** - * Create a reply from db in the same way than API call - * - * @param notificationList List - * @return Notifications (with pagination) - */ - private Notifications createNotificationReply(List notificationList) { - Notifications notifications = new Notifications(); - notifications.notifications = notificationList; - Pagination pagination = new Pagination(); - if (notificationList != null && notificationList.size() > 0) { - //Status list is inverted, it happens for min_id due to ASC ordering - if (Helper.compareTo(notificationList.get(0).id, notificationList.get(notificationList.size() - 1).id) < 0) { - Collections.reverse(notificationList); - notifications.notifications = notificationList; - } - pagination.max_id = notificationList.get(0).id; - pagination.min_id = notificationList.get(notificationList.size() - 1).id; - } - notifications.pagination = pagination; - return notifications; - } - - - /** - * Create a reply from db in the same way than API call - * - * @param conversationList List - * @return Conversations (with pagination) - */ - private Conversations createConversationReply(List conversationList) { - Conversations conversations = new Conversations(); - conversations.conversations = conversationList; - Pagination pagination = new Pagination(); - if (conversationList != null && conversationList.size() > 0) { - //Status list is inverted, it happens for min_id due to ASC ordering - if (Helper.compareTo(conversationList.get(0).id, conversationList.get(conversationList.size() - 1).id) < 0) { - Collections.reverse(conversationList); - conversations.conversations = conversationList; - } - pagination.max_id = conversationList.get(0).id; - pagination.min_id = conversationList.get(conversationList.size() - 1).id; - } - conversations.pagination = pagination; - return conversations; - } - - /** - * Create a reply from db in the same way than API call - * - * @param statusList List - * @return Statuses (with pagination) - */ - private Statuses createStatusReply(List statusList) { - Statuses statuses = new Statuses(); - statuses.statuses = statusList; - Pagination pagination = new Pagination(); - if (statusList != null && statusList.size() > 0) { - //Status list is inverted, it happens for min_id due to ASC ordering - if (Helper.compareTo(statusList.get(0).id, statusList.get(statusList.size() - 1).id) < 0) { - Collections.reverse(statusList); - statuses.statuses = statusList; - } - pagination.max_id = statusList.get(0).id; - pagination.min_id = statusList.get(statusList.size() - 1).id; - } - statuses.pagination = pagination; - return statuses; - } /** * Read cursor and hydrate without closing it diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java index d5d8ebf1..0f5f6046 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java @@ -378,6 +378,8 @@ public class Timeline { TREND_TAG("TREND_TAG"), @SerializedName("TREND_MESSAGE") TREND_MESSAGE("TREND_MESSAGE"), + @SerializedName("ACCOUNT_SUGGESTION") + ACCOUNT_SUGGESTION("ACCOUNT_SUGGESTION"), @SerializedName("PUBLIC_TREND_MESSAGE") TREND_MESSAGE_PUBLIC("TREND_MESSAGE_PUBLIC"), @SerializedName("STATUS_HISTORY") @@ -388,6 +390,8 @@ public class Timeline { MUTED_TIMELINE("MUTED_TIMELINE"), @SerializedName("BOOKMARK_TIMELINE") BOOKMARK_TIMELINE("BOOKMARK_TIMELINE"), + @SerializedName("BLOCKED_DOMAIN_TIMELINE") + BLOCKED_DOMAIN_TIMELINE("BLOCKED_DOMAIN_TIMELINE"), @SerializedName("BLOCKED_TIMELINE") BLOCKED_TIMELINE("BLOCKED_TIMELINE"), @SerializedName("FAVOURITE_TIMELINE") diff --git a/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java b/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java index 51ce2fc8..10835111 100644 --- a/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java +++ b/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java @@ -52,12 +52,11 @@ public class CustomEmoji extends ReplacementSpan { 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; + imageDrawable.setBounds(0, 0, emojiSize, emojiSize); + int transY = bottom - imageDrawable.getBounds().bottom; transY -= paint.getFontMetrics().descent / 2; canvas.translate(x, (float) transY); - drawable.draw(canvas); + imageDrawable.draw(canvas); canvas.restore(); } } 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 cd295eb4..0c816eeb 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -238,6 +238,9 @@ public class Helper { public static final String ARG_STATUS_REPLY_ID = "ARG_STATUS_REPLY_ID"; public static final String ARG_ACCOUNT = "ARG_ACCOUNT"; public static final String ARG_ACCOUNT_ID = "ARG_ACCOUNT_ID"; + public static final String ARG_ADMIN_DOMAINBLOCK = "ARG_ADMIN_DOMAINBLOCK"; + public static final String ARG_ADMIN_DOMAINBLOCK_DELETE = "ARG_ADMIN_DOMAINBLOCK_DELETE"; + public static final String ARG_REPORT = "ARG_REPORT"; public static final String ARG_ACCOUNT_MENTION = "ARG_ACCOUNT_MENTION"; public static final String ARG_MINIFIED = "ARG_MINIFIED"; @@ -302,12 +305,14 @@ public class Helper { public static final String PREF_IS_MODERATOR = "PREF_IS_MODERATOR"; public static final String PREF_IS_ADMINISTRATOR = "PREF_IS_ADMINISTRATOR"; public static final String PREF_KEY_ID = "PREF_KEY_ID"; + public static final String PREF_MESSAGE_URL = "PREF_MESSAGE_URL"; public static final String PREF_INSTANCE = "PREF_INSTANCE"; public static final String SET_SECURITY_PROVIDER = "SET_SECURITY_PROVIDER"; public static final int NOTIFICATION_INTENT = 1; public static final int OPEN_NOTIFICATION = 2; + public static final int OPEN_WITH_ANOTHER_ACCOUNT = 3; public static final String INTENT_TARGETED_ACCOUNT = "INTENT_TARGETED_ACCOUNT"; public static final String INTENT_SEND_MODIFIED_IMAGE = "INTENT_SEND_MODIFIED_IMAGE"; public static final String INTENT_COMPOSE_ERROR_MESSAGE = "INTENT_COMPOSE_ERROR_MESSAGE"; @@ -334,6 +339,8 @@ public class Helper { public static final Pattern bibliogramPattern = Pattern.compile("(m\\.|www\\.)?instagram.com(/p/[\\w-/]+)"); public static final Pattern libredditPattern = Pattern.compile("(www\\.|m\\.)?(reddit\\.com|preview\\.redd\\.it|i\\.redd\\.it|redd\\.it)/(((?!([\"'<])).)*)"); public static final Pattern ouichesPattern = Pattern.compile("https?://ouich\\.es/tag/(\\w+)"); + + public static final Pattern geminiPattern = Pattern.compile("(gemini://.*)\\b"); public static final Pattern xmppPattern = Pattern.compile("xmpp:[-a-zA-Z0-9+$&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); public static final Pattern peertubePattern = Pattern.compile("(https?://([\\da-z.-]+\\.[a-z.]{2,10}))/videos/watch/(\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12})$"); public static final Pattern mediumPattern = Pattern.compile("([\\w@-]*)?\\.?medium.com/@?([/\\w-]+)"); @@ -745,39 +752,56 @@ public class Helper { */ public static String transformURL(Context context, String url) { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); - Matcher matcher = Helper.nitterPattern.matcher(url); + Matcher matcher; boolean nitter = Helper.getSharedValue(context, context.getString(R.string.SET_NITTER)); if (nitter) { + matcher = Helper.nitterPattern.matcher(url); if (matcher.find()) { final String nitter_directory = matcher.group(2); String nitterHost = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); + if (nitterHost.trim().isEmpty()) { + nitterHost = context.getString(R.string.DEFAULT_NITTER_HOST); + } return "https://" + nitterHost + nitter_directory; } } - matcher = Helper.bibliogramPattern.matcher(url); + boolean bibliogram = Helper.getSharedValue(context, context.getString(R.string.SET_BIBLIOGRAM)); + if (bibliogram) { + matcher = Helper.bibliogramPattern.matcher(url); if (matcher.find()) { final String bibliogram_directory = matcher.group(2); String bibliogramHost = sharedpreferences.getString(context.getString(R.string.SET_BIBLIOGRAM_HOST), context.getString(R.string.DEFAULT_BIBLIOGRAM_HOST)).toLowerCase(); + if (bibliogramHost.trim().isEmpty()) { + bibliogramHost = context.getString(R.string.DEFAULT_BIBLIOGRAM_HOST); + } return "https://" + bibliogramHost + bibliogram_directory; } } - matcher = Helper.libredditPattern.matcher(url); + boolean libreddit = Helper.getSharedValue(context, context.getString(R.string.SET_LIBREDDIT)); if (libreddit) { + matcher = Helper.libredditPattern.matcher(url); if (matcher.find()) { final String libreddit_directory = matcher.group(3); String libreddit_host = sharedpreferences.getString(context.getString(R.string.SET_LIBREDDIT_HOST), context.getString(R.string.DEFAULT_LIBREDDIT_HOST)).toLowerCase(); + if (libreddit_host.trim().isEmpty()) { + libreddit_host = context.getString(R.string.DEFAULT_LIBREDDIT_HOST); + } return "https://" + libreddit_host + "/" + libreddit_directory; } } - matcher = Helper.youtubePattern.matcher(url); + boolean invidious = Helper.getSharedValue(context, context.getString(R.string.SET_INVIDIOUS)); if (invidious) { + matcher = Helper.youtubePattern.matcher(url); if (matcher.find()) { final String youtubeId = matcher.group(3); String invidiousHost = sharedpreferences.getString(context.getString(R.string.SET_INVIDIOUS_HOST), context.getString(R.string.DEFAULT_INVIDIOUS_HOST)).toLowerCase(); + if (invidiousHost.trim().isEmpty()) { + invidiousHost = context.getString(R.string.DEFAULT_INVIDIOUS_HOST); + } if (matcher.group(2) != null && Objects.equals(matcher.group(2), "youtu.be")) { return "https://" + invidiousHost + "/watch?v=" + youtubeId + "&local=true"; } else { @@ -785,9 +809,10 @@ public class Helper { } } } - matcher = Helper.mediumPattern.matcher(url); + boolean medium = Helper.getSharedValue(context, context.getString(R.string.REPLACE_MEDIUM)); if (medium) { + matcher = Helper.mediumPattern.matcher(url); if (matcher.find()) { String path = matcher.group(2); String user = matcher.group(1); @@ -795,12 +820,16 @@ public class Helper { path = user + "/" + path; } String mediumReplaceHost = sharedpreferences.getString(context.getString(R.string.REPLACE_MEDIUM_HOST), context.getString(R.string.DEFAULT_REPLACE_MEDIUM_HOST)).toLowerCase(); + if (mediumReplaceHost.trim().isEmpty()) { + mediumReplaceHost = context.getString(R.string.DEFAULT_REPLACE_MEDIUM_HOST); + } return "https://" + mediumReplaceHost + "/" + path; } } - matcher = Helper.wikipediaPattern.matcher(url); + boolean wikipedia = Helper.getSharedValue(context, context.getString(R.string.REPLACE_WIKIPEDIA)); if (wikipedia) { + matcher = Helper.wikipediaPattern.matcher(url); if (matcher.find()) { String subdomain = matcher.group(1); String path = matcher.group(2); @@ -810,6 +839,9 @@ public class Helper { lang = (path.contains("?")) ? TextUtils.htmlEncode("&") : "?"; lang = lang + "lang=" + subdomain; } + if (wikipediaReplaceHost.trim().isEmpty()) { + wikipediaReplaceHost = context.getString(R.string.DEFAULT_REPLACE_WIKIPEDIA_HOST); + } return "https://" + wikipediaReplaceHost + "/" + path + lang; } } @@ -1105,8 +1137,8 @@ public class Helper { * @param view ImageView - the view where the image will be loaded * @param account - {@link Account} */ - public static void loadPP(ImageView view, BaseAccount account) { - loadPP(view, account, false); + public static void loadPP(Activity activity, ImageView view, BaseAccount account) { + loadPP(activity, view, account, false); } /** @@ -1115,15 +1147,14 @@ public class Helper { * @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); + public static void loadPP(Activity activity, ImageView view, BaseAccount account, boolean crop) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean disableGif = sharedpreferences.getBoolean(activity.getString(R.string.SET_DISABLE_GIF), false); String targetedUrl = disableGif ? account.mastodon_account.avatar_static : account.mastodon_account.avatar; - if (targetedUrl != null && Helper.isValidContextForGlide(context)) { + if (targetedUrl != null && Helper.isValidContextForGlide(activity)) { if (disableGif || (!targetedUrl.endsWith(".gif"))) { try { - RequestBuilder requestBuilder = Glide.with(context) + RequestBuilder requestBuilder = Glide.with(activity) .asDrawable() .load(targetedUrl) .thumbnail(0.1f); @@ -1134,7 +1165,7 @@ public class Helper { } catch (Exception ignored) { } } else { - RequestBuilder requestBuilder = Glide.with(context) + RequestBuilder requestBuilder = Glide.with(activity) .asGif() .load(targetedUrl) .thumbnail(0.1f); @@ -1143,8 +1174,8 @@ public class Helper { } requestBuilder.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))).into(view); } - } else if (Helper.isValidContextForGlide(context)) { - Glide.with(context) + } else if (Helper.isValidContextForGlide(activity)) { + Glide.with(activity) .asDrawable() .load(R.drawable.ic_person) .thumbnail(0.1f) @@ -1165,7 +1196,7 @@ public class Helper { return null; } Proxy proxy = new Proxy(type == 0 ? Proxy.Type.HTTP : Proxy.Type.SOCKS, - new InetSocketAddress(hostVal, portVal)); + InetSocketAddress.createUnresolved(hostVal, portVal)); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { @@ -1502,6 +1533,18 @@ public class Helper { channelId = "channel_status"; channelTitle = context.getString(R.string.channel_notif_status); break; + case UPDATE: + channelId = "channel_update"; + channelTitle = context.getString(R.string.channel_notif_update); + break; + case SIGN_UP: + channelId = "channel_signup"; + channelTitle = context.getString(R.string.channel_notif_signup); + break; + case REPORT: + channelId = "channel_report"; + channelTitle = context.getString(R.string.channel_notif_report); + break; default: channelId = "channel_boost"; channelTitle = context.getString(R.string.channel_notif_boost); @@ -1584,8 +1627,11 @@ public class Helper { .setGroup(account.mastodon_account != null ? account.mastodon_account.username + "@" + account.instance : "" + "@" + account.instance) .setGroupSummary(true) .build(); + notificationManager.notify(notificationId++, notificationBuilder.build()); - notificationManager.notify(0, summaryNotification); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + notificationManager.notify(0, summaryNotification); + } } public static void transfertIfExist(Context context) { @@ -1960,6 +2006,9 @@ public class Helper { BOOST, FAV, POLL, + UPDATE, + SIGN_UP, + REPORT, STATUS, BACKUP, STORE, 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 57675c2a..18a81cfb 100644 --- a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java @@ -158,6 +158,9 @@ public class NotificationsHelper { boolean notif_poll = prefs.getBoolean(context.getString(R.string.SET_NOTIF_POLL), true); boolean notif_fav = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FAVOURITE), true); boolean notif_status = prefs.getBoolean(context.getString(R.string.SET_NOTIF_STATUS), true); + boolean notif_update = prefs.getBoolean(context.getString(R.string.SET_NOTIF_UPDATE), true); + boolean notif_signup = prefs.getBoolean(context.getString(R.string.SET_NOTIF_ADMIN_SIGNUP), true); + boolean notif_report = prefs.getBoolean(context.getString(R.string.SET_NOTIF_ADMIN_REPORT), true); final String max_id = prefs.getString(context.getString(R.string.LAST_NOTIFICATION_ID) + key, null); @@ -232,18 +235,18 @@ public class NotificationsHelper { title = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_reblog)); else title = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_reblog)); - } - if (notification.status != null) { - if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); - else - message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); - else - message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + if (notification.status != null) { + if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + } } } break; @@ -254,18 +257,18 @@ public class NotificationsHelper { title = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_favourite)); else title = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_favourite)); - } - if (notification.status != null) { - if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); - else - message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); - else - message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + if (notification.status != null) { + if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + } } } break; @@ -298,21 +301,62 @@ public class NotificationsHelper { title = context.getString(R.string.notif_poll_self); else title = context.getString(R.string.notif_poll); - } - if (notification.status != null) { - if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); - else - message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); - else - message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + if (notification.status != null) { + if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + } } } break; + case "update": + notifType = Helper.NotifType.UPDATE; + if (notif_update) { + title = context.getString(R.string.notif_update_push); + if (notification.status != null) { + if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.spoiler_text)).toString(); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)).toString(); + else + message = new SpannableString(Html.fromHtml(notification.status.content)).toString(); + } + } + } + break; + case "admin.sign_up": + notifType = Helper.NotifType.SIGN_UP; + if (notif_signup) { + title = context.getString(R.string.notif_sign_up); + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_signed_up)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_signed_up)); + targeted_account = notification.account.id; + } + break; + case "admin.report": + notifType = Helper.NotifType.REPORT; + if (notif_report) { + title = context.getString(R.string.notif_report); + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_reported)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_reported)); + targeted_account = notification.account.id; + } + break; default: } if (message != null) { @@ -321,7 +365,7 @@ public class NotificationsHelper { intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Helper.INTENT_ACTION, Helper.NOTIFICATION_INTENT); intent.putExtra(Helper.PREF_KEY_ID, account.user_id); - if (targeted_account != null && notifType == Helper.NotifType.FOLLLOW) + if (targeted_account != null) intent.putExtra(Helper.INTENT_TARGETED_ACCOUNT, targeted_account); intent.putExtra(Helper.PREF_INSTANCE, account.instance); notificationUrl = notification.account.avatar; 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 df8c628a..3da822cd 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -147,6 +147,7 @@ public class SpannableHelper { linkify(context, content, urlDetails); linkifyURL(context, content, urlDetails); emails(context, content); + gemini(context, content); replaceQuoteSpans(context, content); } else { content = new SpannableStringBuilder(text); @@ -249,11 +250,17 @@ public class SpannableHelper { dialogBuilder.setView(popupLinksBinding.getRoot()); AlertDialog alertDialog = dialogBuilder.create(); alertDialog.show(); - String finalURl = url; - String uniqueUrl = url.endsWith("…") ? url : url + "…"; + String finalURl = newURL; + String uniqueUrl = newURL.endsWith("…") ? newURL : newURL + "…"; if (urlDetails.containsValue(uniqueUrl)) { finalURl = Helper.getKeyByValue(urlDetails, uniqueUrl); } + if (finalURl == null) { + return; + } + if (finalURl.startsWith("http://")) { + finalURl = finalURl.replace("http://", "https://"); + } String finalURl1 = finalURl; popupLinksBinding.displayFullLink.setOnClickListener(v -> { AlertDialog.Builder builder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); @@ -668,6 +675,38 @@ public class SpannableHelper { } } + private static void gemini(Context context, Spannable content) { + // --- For all patterns defined in Helper class --- + Pattern pattern = Helper.geminiPattern; + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + int matchStart = matcher.start(); + int matchEnd = matcher.end(); + String geminiLink = content.toString().substring(matchStart, matchEnd); + if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { + ClickableSpan[] clickableSpans = content.getSpans(matchStart, matchEnd, ClickableSpan.class); + if (clickableSpans != null) { + for (ClickableSpan clickableSpan : clickableSpans) { + content.removeSpan(clickableSpan); + } + } + content.removeSpan(clickableSpans); + content.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View textView) { + Helper.openBrowser(context, geminiLink); + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(linkColor); + } + }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + } private static void emails(Context context, Spannable content) { // --- For all patterns defined in Helper class --- @@ -872,24 +911,6 @@ public class SpannableHelper { } } - /** - * Convert HTML content to text. Also, it handles click on link - * This needs to be run asynchronously - * - * @param text String - text to convert, it can be content, spoiler, poll items, etc. - * @return Spannable string - */ - public static Spannable convertNitter(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; - } /** 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 3dd0704e..02ce2b2f 100644 --- a/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java @@ -177,7 +177,6 @@ public class TimelineHelper { public static List filterNotification(Context context, List notifications) { //A security to make sure filters have been fetched before displaying messages List notificationToRemove = new ArrayList<>(); - if (!BaseMainActivity.filterFetched) { try { FiltersVM filtersVM = new ViewModelProvider((ViewModelStoreOwner) context).get(FiltersVM.class); diff --git a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java index f929d9d0..3c42ab28 100644 --- a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java +++ b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java @@ -64,7 +64,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { return makeMovementFlags(dragFlags, swipeFlags); } else { final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; - final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + final int swipeFlags = 0; return makeMovementFlags(dragFlags, swipeFlags); } } 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 d2b2d639..dff83d9b 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 @@ -143,6 +143,7 @@ public class ComposeAdapter extends RecyclerView.Adapter emojisList = new ArrayList<>(); public promptDraftListener promptDraftListener; + private boolean unlisted_changed = false; public ComposeAdapter(List statusList, int statusCount, BaseAccount account, app.fedilab.android.client.entities.api.Account mentionedAccount, String visibility, String editMessageId) { this.statusList = statusList; @@ -619,7 +620,7 @@ public class ComposeAdapter extends RecyclerView.Adapter { + accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, 5, false, false).observe((LifecycleOwner) context, accounts -> { if (accounts == null) { return; } @@ -1244,10 +1245,10 @@ public class ComposeAdapter extends RecyclerView.Adapter 1) { + if (!unlisted_changed && position == 0 && unlistedReplies && statusDraft.visibility.equalsIgnoreCase("public") && statusList.size() > 1) { statusDraft.visibility = "unlisted"; } - } else if (position == statusCount && unlistedReplies && statusDraft.visibility.equalsIgnoreCase("public") && statusList.size() > 1) { + } else if (!unlisted_changed && position == statusCount && unlistedReplies && statusDraft.visibility.equalsIgnoreCase("public") && statusList.size() > 1) { statusDraft.visibility = "unlisted"; } @@ -1277,6 +1278,7 @@ public class ComposeAdapter extends RecyclerView.Adapter { holder.binding.visibilityPanel.setVisibility(View.GONE); @@ -1292,6 +1294,7 @@ public class ComposeAdapter extends RecyclerView.Adapter. */ + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.databinding.DrawerDomainBlockBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class DomainBlockAdapter extends RecyclerView.Adapter { + private final List domainList; + private Context context; + + public DomainBlockAdapter(List domainList) { + this.domainList = domainList; + } + + public int getCount() { + return domainList.size(); + } + + public String getItem(int position) { + return domainList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerDomainBlockBinding itemBinding = DrawerDomainBlockBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new DomainBlockViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + String domain = domainList.get(position); + DomainBlockViewHolder holder = (DomainBlockViewHolder) viewHolder; + holder.binding.domainName.setText(domain); + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + holder.binding.unblockDomain.setOnClickListener(v -> { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + alt_bld.setMessage(context.getString(R.string.unblock_domain_confirm, domain)); + alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { + accountsVM.removeDomainBlocks(MainActivity.currentInstance, MainActivity.currentToken, domain); + domainList.remove(position); + notifyItemRemoved(position); + dialog.dismiss(); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + }); + } + + @Override + public int getItemCount() { + return domainList.size(); + } + + + public static class DomainBlockViewHolder extends RecyclerView.ViewHolder { + DrawerDomainBlockBinding binding; + + DomainBlockViewHolder(DrawerDomainBlockBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} 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 b4b39fc1..3513525e 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 @@ -38,10 +38,11 @@ public class FieldAdapter extends RecyclerView.Adapter fields; private Context context; - private Account account; + private final Account account; - public FieldAdapter(List fields) { + public FieldAdapter(List fields, Account account) { this.fields = fields; + this.account = account; } @Override @@ -68,12 +69,18 @@ public class FieldAdapter extends RecyclerView.Adapter(holder.binding.value)), TextView.BufferType.SPANNABLE); holder.binding.value.setMovementMethod(LinkMovementMethod.getInstance()); - holder.binding.label.setText(field.name); + + holder.binding.label.setText( + field.getValueSpan(context, account, + new WeakReference<>(holder.binding.label)), + TextView.BufferType.SPANNABLE); + holder.binding.label.setMovementMethod(LinkMovementMethod.getInstance()); } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java index e7f8bf28..954369d4 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java @@ -78,11 +78,14 @@ public class FilterAdapter extends RecyclerView.Adapter FilterActivity.addEditFilter(context, filter, filter1 -> { - if (filter1 != null) { + if (filter1 != null && BaseMainActivity.mainFilters.size() > position) { BaseMainActivity.mainFilters.get(position).context = filter1.context; BaseMainActivity.mainFilters.get(position).expires_at = filter1.expires_at; + BaseMainActivity.mainFilters.get(position).filter_action = filter1.filter_action; + BaseMainActivity.mainFilters.get(position).keywords = filter1.keywords; + BaseMainActivity.mainFilters.get(position).title = filter1.title; + filterAdapter.notifyItemChanged(position); } - filterAdapter.notifyItemChanged(position); })); holder.binding.deleteFilter.setOnClickListener(v -> { AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ReorderBottomMenuAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ReorderBottomMenuAdapter.java index 5d0e4dd0..052a5c67 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ReorderBottomMenuAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ReorderBottomMenuAdapter.java @@ -20,7 +20,6 @@ import android.content.Context; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ViewGroup; -import android.widget.Toast; import androidx.recyclerview.widget.RecyclerView; @@ -36,7 +35,6 @@ import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.itemtouchhelper.ItemTouchHelperAdapter; import app.fedilab.android.helper.itemtouchhelper.ItemTouchHelperViewHolder; import app.fedilab.android.helper.itemtouchhelper.OnStartDragListener; -import es.dmoral.toasty.Toasty; /** @@ -130,8 +128,6 @@ public class ReorderBottomMenuAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { private final OnStartDragListener mDragStartListener; - private final OnUndoListener mUndoListener; private final Pinned pinned; private Context context; - public ReorderTabAdapter(Pinned pinned, OnStartDragListener dragStartListener, OnUndoListener undoListener) { + public ReorderTabAdapter(Pinned pinned, OnStartDragListener dragStartListener) { this.mDragStartListener = dragStartListener; - this.mUndoListener = undoListener; this.pinned = pinned; } @@ -169,24 +170,61 @@ public class ReorderTabAdapter extends RecyclerView.Adapter { if (item.type == Timeline.TimeLineEnum.TAG || item.type == Timeline.TimeLineEnum.REMOTE || item.type == Timeline.TimeLineEnum.LIST) { - mUndoListener.onUndo(item, position); - pinned.pinnedTimelines.remove(position); - notifyItemRemoved(position); + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + String title = ""; + String message = ""; + alt_bld.setTitle(R.string.action_lists_delete); + alt_bld.setMessage(R.string.action_lists_confirm_delete); + switch (item.type) { + case TAG: + case REMOTE: + title = context.getString(R.string.action_pinned_delete); + message = context.getString(R.string.unpin_timeline_description); + break; + case LIST: + title = context.getString(R.string.action_lists_delete); + message = context.getString(R.string.action_lists_confirm_delete); + break; + } + alt_bld.setTitle(title); + alt_bld.setMessage(message); + + alt_bld.setPositiveButton(R.string.delete, (dialog, id) -> { + //change position of pinned that are after the removed item + if (position < pinned.pinnedTimelines.size()) { + for (int i = item.position + 1; i < pinned.pinnedTimelines.size(); i++) { + pinned.pinnedTimelines.get(i).position -= 1; + } + pinned.pinnedTimelines.remove(position); + notifyItemRemoved(position); + notifyItemChanged(position, pinned.pinnedTimelines.size() - position); + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + } + + if (item.type == Timeline.TimeLineEnum.LIST) { + TimelinesVM timelinesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(TimelinesVM.class); + timelinesVM.deleteList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, item.mastodonList.id); + } + + + ((ReorderTimelinesActivity) context).setChanges(true); + dialog.dismiss(); + + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + } }); } @Override public void onItemDismiss(int position) { - PinnedTimeline item = pinned.pinnedTimelines.get(position); - if (item.type == Timeline.TimeLineEnum.TAG || item.type == Timeline.TimeLineEnum.REMOTE || item.type == Timeline.TimeLineEnum.LIST) { - mUndoListener.onUndo(item, position); - pinned.pinnedTimelines.remove(position); - notifyItemRemoved(position); - } else { - notifyItemChanged(position); - Toasty.info(context, context.getString(R.string.warning_main_timeline), Toast.LENGTH_SHORT).show(); - } } @Override 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 bc36b5ef..af62860f 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 @@ -24,6 +24,7 @@ import static app.fedilab.android.BaseMainActivity.regex_public; import static app.fedilab.android.BaseMainActivity.show_boosts; import static app.fedilab.android.BaseMainActivity.show_replies; import static app.fedilab.android.activities.ContextActivity.expand; +import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN; import android.annotation.SuppressLint; import android.app.Activity; @@ -38,6 +39,8 @@ 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.SpannableString; import android.text.Spanned; @@ -100,20 +103,22 @@ import java.util.regex.Pattern; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; -import app.fedilab.android.activities.AdminAccountActivity; 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; import app.fedilab.android.activities.StatusHistoryActivity; import app.fedilab.android.activities.StatusInfoActivity; +import app.fedilab.android.activities.admin.AdminAccountActivity; import app.fedilab.android.client.entities.api.Attachment; 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.BaseAccount; import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.client.entities.app.StatusDraft; import app.fedilab.android.client.entities.app.Timeline; @@ -1240,8 +1245,10 @@ public class StatusAdapter extends RecyclerView.Adapter layoutMediaBinding.media.setOnClickListener(v -> { if (statusToDeal.isMediaObfuscated && mediaObfuscated(statusToDeal) && !expand_media) { statusToDeal.isMediaObfuscated = false; - adapter.notifyItemChanged(holder.getBindingAdapterPosition()); + int position = holder.getBindingAdapterPosition(); + adapter.notifyItemChanged(position); final int timeout = sharedpreferences.getInt(context.getString(R.string.SET_NSFW_TIMEOUT), 5); + if (timeout > 0) { new CountDownTimer((timeout * 1000L), 1000) { public void onTick(long millisUntilFinished) { @@ -1249,7 +1256,7 @@ public class StatusAdapter extends RecyclerView.Adapter public void onFinish() { status.isMediaObfuscated = true; - adapter.notifyItemChanged(holder.getBindingAdapterPosition()); + adapter.notifyItemChanged(position); } }.start(); } @@ -1609,7 +1616,11 @@ public class StatusAdapter extends RecyclerView.Adapter .observe((LifecycleOwner) context, poll -> { int i = 0; for (Poll.PollItem item : statusToDeal.poll.options) { - poll.options.get(i).span_title = item.span_title; + if (item.span_title != null) { + poll.options.get(i).span_title = item.span_title; + } else { + poll.options.get(i).span_title = new SpannableString(item.title); + } i++; } statusToDeal.poll = poll; @@ -1625,7 +1636,11 @@ public class StatusAdapter extends RecyclerView.Adapter if (poll != null) { int i = 0; for (Poll.PollItem item : statusToDeal.poll.options) { - poll.options.get(i).span_title = item.span_title; + if (item.span_title != null) { + poll.options.get(i).span_title = item.span_title; + } else { + poll.options.get(i).span_title = new SpannableString(item.title); + } i++; } statusToDeal.poll = poll; @@ -1640,7 +1655,11 @@ public class StatusAdapter extends RecyclerView.Adapter //Store span elements int i = 0; for (Poll.PollItem item : statusToDeal.poll.options) { - poll.options.get(i).span_title = item.span_title; + if (item.span_title != null) { + poll.options.get(i).span_title = item.span_title; + } else { + poll.options.get(i).span_title = new SpannableString(item.title); + } i++; } statusToDeal.poll = poll; @@ -1849,14 +1868,18 @@ public class StatusAdapter extends RecyclerView.Adapter AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); builderInner.setTitle(stringArrayConf[0]); builderInner.setMessage(statusToDeal.account.acct); - builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); - builderInner.setPositiveButton(R.string.yes, (dialog, which) -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.account.id, null, null) + builderInner.setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setNegativeButton(R.string.keep_notifications, (dialog, which) -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.account.id, false, null) + .observe((LifecycleOwner) context, relationShip -> { + sendAction(context, Helper.ARG_STATUS_ACCOUNT_ID_DELETED, null, statusToDeal.account.id); + Toasty.info(context, context.getString(R.string.toast_mute), Toasty.LENGTH_LONG).show(); + })); + builderInner.setPositiveButton(R.string.action_mute, (dialog, which) -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.account.id, null, null) .observe((LifecycleOwner) context, relationShip -> { sendAction(context, Helper.ARG_STATUS_ACCOUNT_ID_DELETED, null, statusToDeal.account.id); Toasty.info(context, context.getString(R.string.toast_mute), Toasty.LENGTH_LONG).show(); })); builderInner.show(); - } else if (itemId == R.id.action_mute_conversation) { if (statusToDeal.muted) { statusesVM.unMute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id).observe((LifecycleOwner) context, status1 -> Toasty.info(context, context.getString(R.string.toast_unmute_conversation)).show()); @@ -1993,6 +2016,54 @@ public class StatusAdapter extends RecyclerView.Adapter b.putSerializable(Helper.ARG_STATUS_MENTION, statusToDeal); intent.putExtras(b); context.startActivity(intent); + } else if (itemId == R.id.action_open_with) { + new Thread(() -> { + try { + List accounts = new Account(context).getCrossAccounts(); + if (accounts.size() > 1) { + List accountList = new ArrayList<>(); + for (BaseAccount account : accounts) { + accountList.add(account.mastodon_account); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + AlertDialog.Builder builderSingle = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderSingle.setTitle(context.getString(R.string.choose_accounts)); + final AccountsSearchAdapter accountsSearchAdapter = new AccountsSearchAdapter(context, accountList); + final BaseAccount[] accountArray = new BaseAccount[accounts.size()]; + int i = 0; + for (BaseAccount account : accounts) { + accountArray[i] = account; + i++; + } + builderSingle.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderSingle.setAdapter(accountsSearchAdapter, (dialog, which) -> { + BaseAccount account = accountArray[which]; + + Toasty.info(context, context.getString(R.string.toast_account_changed, "@" + account.mastodon_account.acct + "@" + account.instance), Toasty.LENGTH_LONG).show(); + BaseMainActivity.currentToken = account.token; + BaseMainActivity.currentUserID = account.user_id; + BaseMainActivity.currentInstance = account.instance; + MainActivity.currentAccount = account; + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(PREF_USER_TOKEN, account.token); + editor.commit(); + Intent mainActivity = new Intent(context, MainActivity.class); + mainActivity.putExtra(Helper.INTENT_ACTION, Helper.OPEN_WITH_ANOTHER_ACCOUNT); + mainActivity.putExtra(Helper.PREF_MESSAGE_URL, statusToDeal.url); + context.startActivity(mainActivity); + ((Activity) context).finish(); + dialog.dismiss(); + }); + builderSingle.show(); + }; + mainHandler.post(myRunnable); + } + + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); } return true; }); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/SuggestionAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/SuggestionAdapter.java new file mode 100644 index 00000000..ced0db25 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/SuggestionAdapter.java @@ -0,0 +1,143 @@ +package app.fedilab.android.ui.drawer; +/* 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.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.ColorStateList; +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; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.lang.ref.WeakReference; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.client.entities.api.Account; +import app.fedilab.android.client.entities.api.Suggestion; +import app.fedilab.android.databinding.DrawerSuggestionBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class SuggestionAdapter extends RecyclerView.Adapter { + + private final List suggestionList; + private Context context; + + public SuggestionAdapter(List suggestionList) { + this.suggestionList = suggestionList; + } + + + public int getCount() { + return suggestionList.size(); + } + + public Suggestion getItem(int position) { + return suggestionList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + this.context = parent.getContext(); + DrawerSuggestionBinding itemBinding = DrawerSuggestionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new SuggestionViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Account account = suggestionList.get(position).account; + SuggestionViewHolder holder = (SuggestionViewHolder) viewHolder; + MastodonHelper.loadPPMastodon(holder.binding.avatar, account); + + + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + + holder.binding.avatar.setOnClickListener(v -> { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.binding.avatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + }); + holder.binding.followAction.setIconResource(R.drawable.ic_baseline_person_add_24); + holder.binding.followAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference))); + if (account == null) { + return; + } + holder.binding.displayName.setText( + account.getSpanDisplayName(context, + new WeakReference<>(holder.binding.displayName)), + TextView.BufferType.SPANNABLE); + holder.binding.username.setText(String.format("@%s", account.acct)); + holder.binding.bio.setText( + account.getSpanNote(context, + new WeakReference<>(holder.binding.bio)), + TextView.BufferType.SPANNABLE); + + holder.binding.followAction.setEnabled(false); + holder.binding.followAction.setOnClickListener(v -> { + suggestionList.remove(position); + notifyItemRemoved(position); + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, false); + }); + holder.binding.notInterested.setOnClickListener(view -> { + suggestionList.remove(position); + notifyItemRemoved(position); + accountsVM.removeSuggestion(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id); + }); + //TODO, remove when supported + holder.binding.notInterested.setVisibility(View.GONE); + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return suggestionList.size(); + } + + + public static class SuggestionViewHolder extends RecyclerView.ViewHolder { + DrawerSuggestionBinding binding; + + SuggestionViewHolder(DrawerSuggestionBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/admin/AdminAccountAdapter.java similarity index 92% rename from app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java rename to app/src/main/java/app/fedilab/android/ui/drawer/admin/AdminAccountAdapter.java index 5d35e711..16968e41 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/admin/AdminAccountAdapter.java @@ -1,4 +1,4 @@ -package app.fedilab.android.ui.drawer; +package app.fedilab.android.ui.drawer.admin; /* Copyright 2022 Thomas Schneider * * This file is a part of Fedilab @@ -27,8 +27,8 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.List; import java.util.Locale; -import app.fedilab.android.activities.AdminAccountActivity; -import app.fedilab.android.client.entities.api.AdminAccount; +import app.fedilab.android.activities.admin.AdminAccountActivity; +import app.fedilab.android.client.entities.api.admin.AdminAccount; import app.fedilab.android.databinding.DrawerAdminAccountBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; @@ -78,8 +78,7 @@ public class AdminAccountAdapter extends RecyclerView.Adapter 0) { holder.binding.lastActive.setText(Helper.shortDateToString(adminAccount.ips.get(0).used_at)); holder.binding.ip.setText(adminAccount.ips.get(0).ip); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/admin/AdminDomainAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/admin/AdminDomainAdapter.java new file mode 100644 index 00000000..bee4ccee --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/admin/AdminDomainAdapter.java @@ -0,0 +1,101 @@ +package app.fedilab.android.ui.drawer.admin; +/* 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.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.admin.AdminDomainBlockActivity; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlock; +import app.fedilab.android.databinding.DrawerAdminDomainBinding; +import app.fedilab.android.helper.Helper; + + +public class AdminDomainAdapter extends RecyclerView.Adapter { + + private final List adminDomainBlockList; + private Context context; + + + public AdminDomainAdapter(List adminDomainBlocks) { + this.adminDomainBlockList = adminDomainBlocks; + } + + @NotNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerAdminDomainBinding itemBinding = DrawerAdminDomainBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new DomainViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + DomainViewHolder holder = (DomainViewHolder) viewHolder; + AdminDomainBlock adminDomainBlock = adminDomainBlockList.get(position); + + holder.binding.date.setText(Helper.shortDateToString(adminDomainBlock.created_at)); + holder.binding.title.setText(adminDomainBlock.domain); + String text = adminDomainBlock.severity; + if (adminDomainBlock.reject_media) { + text += " - " + context.getString(R.string.reject_media); + } + if (adminDomainBlock.reject_reports) { + text += " - " + context.getString(R.string.reject_reports); + } + holder.binding.severity.setText(text); + holder.binding.mainContainer.setOnClickListener(view -> { + Intent intent = new Intent(context, AdminDomainBlockActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ADMIN_DOMAINBLOCK, adminDomainBlock); + intent.putExtras(b); + context.startActivity(intent); + }); + + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return adminDomainBlockList.size(); + } + + + public static class DomainViewHolder extends RecyclerView.ViewHolder { + DrawerAdminDomainBinding binding; + + DomainViewHolder(DrawerAdminDomainBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ReportAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/admin/ReportAdapter.java similarity index 97% rename from app/src/main/java/app/fedilab/android/ui/drawer/ReportAdapter.java rename to app/src/main/java/app/fedilab/android/ui/drawer/admin/ReportAdapter.java index 394a9bc1..34dedb56 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ReportAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/admin/ReportAdapter.java @@ -1,4 +1,4 @@ -package app.fedilab.android.ui.drawer; +package app.fedilab.android.ui.drawer.admin; /* Copyright 2022 Thomas Schneider * * This file is a part of Fedilab @@ -31,7 +31,7 @@ import java.util.List; import app.fedilab.android.activities.AccountReportActivity; import app.fedilab.android.client.entities.api.Account; -import app.fedilab.android.client.entities.api.AdminReport; +import app.fedilab.android.client.entities.api.admin.AdminReport; import app.fedilab.android.databinding.DrawerReportBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminAccount.java b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminAccount.java index ba3e9b3b..5400a711 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminAccount.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminAccount.java @@ -33,14 +33,14 @@ import java.util.List; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; -import app.fedilab.android.activities.AdminActionActivity; -import app.fedilab.android.client.entities.api.AdminAccount; -import app.fedilab.android.client.entities.api.AdminAccounts; +import app.fedilab.android.activities.admin.AdminActionActivity; +import app.fedilab.android.client.entities.api.admin.AdminAccount; +import app.fedilab.android.client.entities.api.admin.AdminAccounts; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.ThemeHelper; -import app.fedilab.android.ui.drawer.AdminAccountAdapter; +import app.fedilab.android.ui.drawer.admin.AdminAccountAdapter; import app.fedilab.android.viewmodel.mastodon.AdminVM; diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminDomain.java b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminDomain.java new file mode 100644 index 00000000..6123be6a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminDomain.java @@ -0,0 +1,256 @@ +package app.fedilab.android.ui.fragment.admin; +/* 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.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +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.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.admin.AdminDomainBlockActivity; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlock; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlocks; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.admin.AdminDomainAdapter; +import app.fedilab.android.viewmodel.mastodon.AdminVM; + + +public class FragmentAdminDomain extends Fragment { + + + private FragmentPaginationBinding binding; + private AdminVM adminVM; + private boolean flagLoading; + private List adminDomainBlocks; + private String max_id, min_id; + private AdminDomainAdapter adminDomainAdapter; + private LinearLayoutManager mLayoutManager; + private String viewModelKey; + + public void scrollToTop() { + if (binding != null) { + binding.recyclerView.scrollToPosition(0); + } + } + + public void delete(AdminDomainBlock adminDomainBlock) { + int position = 0; + for (AdminDomainBlock adminDomainBlockPresent : adminDomainBlocks) { + if (adminDomainBlockPresent.id.equals(adminDomainBlock.id)) { + adminDomainBlocks.remove(position); + adminDomainAdapter.notifyItemRemoved(position); + break; + } + position++; + } + } + + public void update(AdminDomainBlock adminDomainBlock) { + if (adminDomainBlocks == null) { + AdminDomainBlocks adminDomainBlocks = new AdminDomainBlocks(); + adminDomainBlocks.adminDomainBlocks = new ArrayList<>(); + adminDomainBlocks.adminDomainBlocks.add(adminDomainBlock); + initializeStatusesCommonView(adminDomainBlocks); + } + int position = 0; + boolean find = false; + for (AdminDomainBlock adminDomainBlockPresent : adminDomainBlocks) { + if (adminDomainBlockPresent.id.equals(adminDomainBlock.id)) { + adminDomainBlocks.get(position).private_comment = adminDomainBlock.private_comment; + adminDomainBlocks.get(position).public_comment = adminDomainBlock.public_comment; + adminDomainBlocks.get(position).severity = adminDomainBlock.severity; + adminDomainBlocks.get(position).reject_reports = adminDomainBlock.reject_reports; + adminDomainBlocks.get(position).reject_media = adminDomainBlock.reject_media; + adminDomainBlocks.get(position).obfuscate = adminDomainBlock.obfuscate; + adminDomainAdapter.notifyItemChanged(position); + find = true; + break; + } + position++; + } + if (!find) { + adminDomainBlocks.add(0, adminDomainBlock); + adminDomainAdapter.notifyItemInserted(0); + } + } + + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + if (getArguments() != null) { + viewModelKey = getArguments().getString(Helper.ARG_VIEW_MODEL_KEY, ""); + } + + binding = FragmentPaginationBinding.inflate(inflater, container, false); + binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity())); + + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + + adminVM = new ViewModelProvider(FragmentAdminDomain.this).get(viewModelKey, AdminVM.class); + + binding.noActionText.setText(R.string.no_blocked_domains); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + flagLoading = false; + adminVM.getDomainBlocks( + BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + binding.addAction.setVisibility(View.VISIBLE); + binding.addAction.setOnClickListener(v -> { + Intent intent = new Intent(requireActivity(), AdminDomainBlockActivity.class); + Bundle b = new Bundle(); + intent.putExtras(b); + startActivity(intent); + }); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + /** + * Intialize the common view for domain block on different timelines + * + * @param adminDomainBlocks {@link AdminDomainBlocks} + */ + private void initializeStatusesCommonView(final AdminDomainBlocks adminDomainBlocks) { + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + max_id = null; + flagLoading = false; + adminVM.getDomainBlocks( + BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + }); + + if (adminDomainBlocks == null || adminDomainBlocks.adminDomainBlocks == null || adminDomainBlocks.adminDomainBlocks.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + return; + } + flagLoading = adminDomainBlocks.pagination.max_id == null; + binding.recyclerView.setVisibility(View.VISIBLE); + if (adminDomainAdapter != null && this.adminDomainBlocks != null) { + int size = this.adminDomainBlocks.size(); + this.adminDomainBlocks.clear(); + this.adminDomainBlocks = new ArrayList<>(); + adminDomainAdapter.notifyItemRangeRemoved(0, size); + } + if (this.adminDomainBlocks == null) { + this.adminDomainBlocks = new ArrayList<>(); + } + this.adminDomainBlocks.addAll(adminDomainBlocks.adminDomainBlocks); + + if (max_id == null || (adminDomainBlocks.pagination.max_id != null && Helper.compareTo(adminDomainBlocks.pagination.max_id, max_id) < 0)) { + max_id = adminDomainBlocks.pagination.max_id; + } + if (min_id == null || (adminDomainBlocks.pagination.max_id != null && Helper.compareTo(adminDomainBlocks.pagination.min_id, min_id) > 0)) { + min_id = adminDomainBlocks.pagination.min_id; + } + + adminDomainAdapter = new AdminDomainAdapter(adminDomainBlocks.adminDomainBlocks); + + mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(adminDomainAdapter); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(binding.recyclerView.getContext(), + mLayoutManager.getOrientation()); + binding.recyclerView.addItemDecoration(dividerItemDecoration); + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (requireActivity() instanceof BaseMainActivity) { + if (dy < 0 && !((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(true); + if (dy > 0 && ((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(false); + } + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + adminVM.getDomainAllows( + BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id) + .observe(getViewLifecycleOwner(), adminDomainBlocks1 -> dealWithPagination(adminDomainBlocks1)); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + } + }); + } + + /** + * Update view and pagination when scrolling down + * + * @param admDomainBlocks AdminDomainBlocks + */ + private void dealWithPagination(AdminDomainBlocks admDomainBlocks) { + + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loadingNextElements.setVisibility(View.GONE); + if (this.adminDomainBlocks != null && admDomainBlocks != null && admDomainBlocks.adminDomainBlocks != null && admDomainBlocks.adminDomainBlocks.size() > 0) { + flagLoading = admDomainBlocks.pagination.max_id == null; + //There are some adminDomainBlocks present in the timeline + int startId = this.adminDomainBlocks.size(); + this.adminDomainBlocks.addAll(admDomainBlocks.adminDomainBlocks); + adminDomainAdapter.notifyItemRangeInserted(startId, admDomainBlocks.adminDomainBlocks.size()); + if (max_id == null || (admDomainBlocks.pagination.max_id != null && Helper.compareTo(admDomainBlocks.pagination.max_id, max_id) < 0)) { + max_id = admDomainBlocks.pagination.max_id; + } + if (min_id == null || (admDomainBlocks.pagination.min_id != null && Helper.compareTo(admDomainBlocks.pagination.min_id, min_id) > 0)) { + min_id = admDomainBlocks.pagination.min_id; + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java index 0f5a5ec1..5f8536c4 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java @@ -33,13 +33,13 @@ import java.util.List; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; -import app.fedilab.android.activities.AdminActionActivity; -import app.fedilab.android.client.entities.api.AdminReport; -import app.fedilab.android.client.entities.api.AdminReports; +import app.fedilab.android.activities.admin.AdminActionActivity; +import app.fedilab.android.client.entities.api.admin.AdminReport; +import app.fedilab.android.client.entities.api.admin.AdminReports; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; -import app.fedilab.android.ui.drawer.ReportAdapter; +import app.fedilab.android.ui.drawer.admin.ReportAdapter; import app.fedilab.android.viewmodel.mastodon.AdminVM; diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java index c193b1aa..6ec7e0da 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java @@ -75,7 +75,7 @@ public class FragmentLoginMain extends Fragment { binding = FragmentLoginMainBinding.inflate(inflater, container, false); View root = binding.getRoot(); - + InstanceSocialVM instanceSocialVM = new ViewModelProvider(FragmentLoginMain.this).get(InstanceSocialVM.class); binding.menuIcon.setOnClickListener(this::showMenu); binding.loginInstance.setOnItemClickListener((parent, view, position, id) -> oldSearch = parent.getItemAtPosition(position).toString().trim()); binding.loginInstance.addTextChangedListener(new TextWatcher() { @@ -101,7 +101,7 @@ public class FragmentLoginMain extends Fragment { } if (oldSearch == null || !oldSearch.equals(s.toString().trim())) { searchInstanceRunning = true; - InstanceSocialVM instanceSocialVM = new ViewModelProvider(FragmentLoginMain.this).get(InstanceSocialVM.class); + instanceSocialVM.getInstances(query).observe(requireActivity(), instanceSocialList -> { binding.loginInstance.setAdapter(null); if (instanceSocialList.instances.isEmpty()) { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java new file mode 100644 index 00000000..65209c2f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java @@ -0,0 +1,214 @@ +package app.fedilab.android.ui.fragment.timeline; +/* 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.os.Bundle; +import android.view.LayoutInflater; +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.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.client.entities.api.Domains; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.DomainBlockAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class FragmentMastodonDomainBlock extends Fragment { + + + private FragmentPaginationBinding binding; + private DomainBlockAdapter domainBlockAdapter; + private AccountsVM accountsVM; + private List domainList; + private boolean flagLoading; + private String max_id; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + binding = FragmentPaginationBinding.inflate(inflater, container, false); + binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity())); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + accountsVM = new ViewModelProvider(FragmentMastodonDomainBlock.this).get(AccountsVM.class); + flagLoading = false; + max_id = null; + domainList = new ArrayList<>(); + + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + max_id = null; + int size = domainList.size(); + domainList.clear(); + domainList = new ArrayList<>(); + domainBlockAdapter.notifyItemRangeRemoved(0, size); + router(); + }); + domainBlockAdapter = new DomainBlockAdapter(domainList); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(domainBlockAdapter); + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + router(); + } + + /** + * Router for timelines + */ + private void router() { + + if (max_id == null) { + accountsVM.getDomainBlocks(MainActivity.currentInstance, MainActivity.currentToken, null, null, null) + .observe(getViewLifecycleOwner(), this::initializeTagCommonView); + } else { + accountsVM.getDomainBlocks(MainActivity.currentInstance, MainActivity.currentToken, null, max_id, null) + .observe(getViewLifecycleOwner(), this::dealWithPagination); + } + } + + public void scrollToTop() { + binding.recyclerView.setAdapter(domainBlockAdapter); + } + + /** + * Intialize the view for domains + * + * @param domains List of {@link String} + */ + private void initializeTagCommonView(final Domains domains) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + max_id = null; + router(); + }); + if (domains == null || domains.domains == null || domains.domains.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + binding.noActionText.setText(R.string.no_accounts); + return; + } + binding.recyclerView.setVisibility(View.VISIBLE); + if (domainBlockAdapter != null && this.domainList != null) { + int size = this.domainList.size(); + this.domainList.clear(); + this.domainList = new ArrayList<>(); + domainBlockAdapter.notifyItemRangeRemoved(0, size); + } + + this.domainList = domains.domains; + domainBlockAdapter = new DomainBlockAdapter(this.domainList); + flagLoading = domains.pagination.max_id == null; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(domainBlockAdapter); + + max_id = domains.pagination.max_id; + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + } + + + private void dealWithPagination(Domains fetched_domains) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loadingNextElements.setVisibility(View.GONE); + if (domainList != null && fetched_domains != null && fetched_domains.domains != null) { + flagLoading = fetched_domains.pagination.max_id == null; + int startId = 0; + //There are some domains present in the timeline + if (domainList.size() > 0) { + startId = domainList.size(); + } + int position = domainList.size(); + domainList.addAll(fetched_domains.domains); + max_id = fetched_domains.pagination.max_id; + domainBlockAdapter.notifyItemRangeInserted(startId, fetched_domains.domains.size()); + } else { + flagLoading = true; + } + } +} \ No newline at end of file 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 f87a09cf..65bb4163 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 @@ -622,8 +622,7 @@ public class FragmentMastodonNotification extends Fragment implements Notificati for (Notification notificationsAlreadyPresent : 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 (Helper.compareTo(notificationReceived.id, notificationsAlreadyPresent.id) > 0) { - if (notificationReceived.created_at.after(notificationsAlreadyPresent.created_at)) { + if (Helper.compareTo(notificationReceived.id, notificationsAlreadyPresent.id) > 0) { if (!notificationList.contains(notificationReceived)) { notificationList.add(position, notificationReceived); notificationAdapter.notifyItemInserted(position); diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonSuggestion.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonSuggestion.java new file mode 100644 index 00000000..417876cd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonSuggestion.java @@ -0,0 +1,161 @@ +package app.fedilab.android.ui.fragment.timeline; +/* 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.os.Bundle; +import android.view.LayoutInflater; +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.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.api.Suggestion; +import app.fedilab.android.client.entities.api.Suggestions; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.SuggestionAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + +public class FragmentMastodonSuggestion extends Fragment { + + + private FragmentPaginationBinding binding; + private AccountsVM accountsVM; + private boolean flagLoading; + private List suggestions; + private String max_id; + private SuggestionAdapter suggestionAdapter; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + flagLoading = false; + binding = FragmentPaginationBinding.inflate(inflater, container, false); + binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity())); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + accountsVM = new ViewModelProvider(FragmentMastodonSuggestion.this).get(AccountsVM.class); + max_id = null; + accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + } + + + public void scrollToTop() { + binding.recyclerView.setAdapter(suggestionAdapter); + } + + /** + * Intialize the view for Suggestions + * + * @param suggestions {@link Suggestions} + */ + private void initializeAccountCommonView(final Suggestions suggestions) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + max_id = null; + accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + }); + if (suggestions == null || suggestions.suggestions == null || suggestions.suggestions.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + binding.noActionText.setText(R.string.no_accounts); + return; + } + binding.recyclerView.setVisibility(View.VISIBLE); + + this.suggestions = suggestions.suggestions; + suggestionAdapter = new SuggestionAdapter(this.suggestions); + flagLoading = suggestions.pagination.max_id == null; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(suggestionAdapter); + max_id = suggestions.pagination.max_id; + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id) + .observe(getViewLifecycleOwner(), suggestionsPaginated -> { + dealWithPagination(suggestionsPaginated); + }); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + } + + + /** + * Update view and pagination when scrolling down + * + * @param suggestions_fetched Suggestions + */ + private void dealWithPagination(Suggestions suggestions_fetched) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loadingNextElements.setVisibility(View.GONE); + if (this.suggestions != null && suggestions_fetched != null && suggestions_fetched.suggestions != null) { + flagLoading = suggestions_fetched.pagination.max_id == null; + int startId = this.suggestions.size(); + this.suggestions.addAll(suggestions_fetched.suggestions); + max_id = suggestions_fetched.pagination.max_id; + suggestionAdapter.notifyItemRangeInserted(startId, suggestions_fetched.suggestions.size()); + } else { + flagLoading = true; + } + } +} \ 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 b5cc921c..97e0b50f 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 @@ -25,6 +25,7 @@ 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; @@ -56,10 +57,15 @@ public class FragmentNotificationContainer extends Fragment { public static UpdateCounters update; private FragmentNotificationContainerBinding binding; + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentNotificationContainerBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); boolean display_all_notification = sharedpreferences.getBoolean(getString(R.string.SET_DISPLAY_ALL_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, false); if (!display_all_notification) { @@ -77,6 +83,8 @@ public class FragmentNotificationContainer extends Fragment { binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_home_24)); binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_person_add_alt_1_24)); binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_edit_24)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_person_add_alt_1_24)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_report_24)); binding.viewpagerNotificationContainer.setAdapter(new FedilabNotificationPageAdapter(getChildFragmentManager(), true)); } AtomicBoolean changes = new AtomicBoolean(false); @@ -92,6 +100,8 @@ public class FragmentNotificationContainer extends Fragment { ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayUpdatesFromPeople); ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayFollows); ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayUpdates); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displaySignups); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayReports); DrawableCompat.setTintList(DrawableCompat.wrap(dialogView.displayAllCategories.getThumbDrawable()), ThemeHelper.getSwitchCompatThumbDrawable(requireActivity())); DrawableCompat.setTintList(DrawableCompat.wrap(dialogView.displayAllCategories.getTrackDrawable()), ThemeHelper.getSwitchCompatTrackDrawable(requireActivity())); @@ -128,6 +138,8 @@ public class FragmentNotificationContainer extends Fragment { dialogView.displayUpdatesFromPeople.setChecked(true); dialogView.displayFollows.setChecked(true); dialogView.displayUpdates.setChecked(true); + dialogView.displaySignups.setChecked(true); + dialogView.displayReports.setChecked(true); String excludedCategories = sharedpreferences.getString(getString(R.string.SET_EXCLUDED_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, null); List excludedCategoriesList = new ArrayList<>(); if (excludedCategories != null) { @@ -162,6 +174,14 @@ public class FragmentNotificationContainer extends Fragment { excludedCategoriesList.add("update"); dialogView.displayUpdates.setChecked(false); break; + case "admin.sign_up": + excludedCategoriesList.add("admin.sign_up"); + dialogView.displaySignups.setChecked(false); + break; + case "admin.report": + excludedCategoriesList.add("admin.report"); + dialogView.displayReports.setChecked(false); + break; } } } @@ -182,6 +202,10 @@ public class FragmentNotificationContainer extends Fragment { notificationType = "follow"; } else if (checkedId == R.id.display_updates) { notificationType = "update"; + } else if (checkedId == R.id.display_signups) { + notificationType = "admin.sign_up"; + } else if (checkedId == R.id.display_reports) { + notificationType = "admin.report"; } if (isChecked) { excludedCategoriesList.remove(notificationType); @@ -234,9 +258,9 @@ public class FragmentNotificationContainer extends Fragment { } } }); - return binding.getRoot(); } + private void doAction(boolean changed, List excludedCategoriesList) { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); if (changed) { diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java index edd50f9c..b9ec991f 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java @@ -86,6 +86,12 @@ public class FedilabNotificationPageAdapter extends FragmentStatePagerAdapter { case 7: bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.UPDATES); break; + case 8: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.ADMIN_SIGNUP); + break; + case 9: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.ADMIN_REPORT); + break; } } fragmentMastodonNotification.setArguments(bundle); @@ -94,6 +100,6 @@ public class FedilabNotificationPageAdapter extends FragmentStatePagerAdapter { @Override public int getCount() { - return extended ? 8 : 2; + return extended ? 10 : 2; } } \ No newline at end of file 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 037da464..f3ba0615 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 @@ -26,9 +26,6 @@ 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.LinkedHashMap; import java.util.List; import java.util.concurrent.TimeUnit; @@ -38,6 +35,7 @@ import app.fedilab.android.activities.MainActivity; import app.fedilab.android.client.endpoints.MastodonAccountsService; import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.api.Accounts; +import app.fedilab.android.client.entities.api.Domains; import app.fedilab.android.client.entities.api.FeaturedTag; import app.fedilab.android.client.entities.api.Field; import app.fedilab.android.client.entities.api.Filter; @@ -50,6 +48,8 @@ import app.fedilab.android.client.entities.api.Report; import app.fedilab.android.client.entities.api.Source; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.client.entities.api.Statuses; +import app.fedilab.android.client.entities.api.Suggestion; +import app.fedilab.android.client.entities.api.Suggestions; import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.api.Token; import app.fedilab.android.client.entities.app.StatusCache; @@ -75,6 +75,7 @@ public class AccountsVM extends AndroidViewModel { private MutableLiveData accountMutableLiveData; private MutableLiveData> accountListMutableLiveData; + private MutableLiveData suggestionsMutableLiveData; private MutableLiveData statusesMutableLiveData; private MutableLiveData accountsMutableLiveData; private MutableLiveData> statusListMutableLiveData; @@ -89,7 +90,7 @@ public class AccountsVM extends AndroidViewModel { private MutableLiveData> tagListMutableLiveData; private MutableLiveData preferencesMutableLiveData; private MutableLiveData tokenMutableLiveData; - private MutableLiveData> stringListMutableLiveData; + private MutableLiveData domainsMutableLiveData; private MutableLiveData reportMutableLiveData; public AccountsVM(@NonNull Application application) { @@ -97,7 +98,6 @@ public class AccountsVM extends AndroidViewModel { } private MastodonAccountsService init(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())) @@ -106,6 +106,15 @@ public class AccountsVM extends AndroidViewModel { return retrofit.create(MastodonAccountsService.class); } + private MastodonAccountsService initv2(String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v2/") + .addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder())) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAccountsService.class); + } + /** * Get connected account * @@ -1021,11 +1030,12 @@ public class AccountsVM extends AndroidViewModel { * View domains the user has blocked. * * @param limit Maximum number of results. Defaults to 40. - * @return {@link LiveData} containing a {@link List} of {@link String}s + * @return {@link LiveData} containing {@link Domains} */ - public LiveData> getDomainBlocks(@NonNull String instance, String token, String limit, String maxId, String sinceId) { - stringListMutableLiveData = new MutableLiveData<>(); + public LiveData getDomainBlocks(@NonNull String instance, String token, String limit, String maxId, String sinceId) { + domainsMutableLiveData = new MutableLiveData<>(); MastodonAccountsService mastodonAccountsService = init(instance); + Domains domains = new Domains(); new Thread(() -> { List stringList = null; Call> getDomainBlocksCall = mastodonAccountsService.getDomainBlocks(token, limit, maxId, sinceId); @@ -1033,18 +1043,18 @@ public class AccountsVM extends AndroidViewModel { try { Response> getDomainBlocksResponse = getDomainBlocksCall.execute(); if (getDomainBlocksResponse.isSuccessful()) { - stringList = getDomainBlocksResponse.body(); + domains.domains = getDomainBlocksResponse.body(); + domains.pagination = MastodonHelper.getPagination(getDomainBlocksResponse.headers()); } } catch (Exception e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); - List finalStringList = stringList; - Runnable myRunnable = () -> stringListMutableLiveData.setValue(finalStringList); + Runnable myRunnable = () -> domainsMutableLiveData.setValue(domains); mainHandler.post(myRunnable); }).start(); - return stringListMutableLiveData; + return domainsMutableLiveData; } /** @@ -1393,28 +1403,28 @@ public class AccountsVM extends AndroidViewModel { * @param limit Maximum number of results to return. Defaults to 40. * @return {@link LiveData} containing a {@link List} of {@link Account}s */ - public LiveData> getSuggestions(@NonNull String instance, String token, String limit) { - accountListMutableLiveData = new MutableLiveData<>(); - MastodonAccountsService mastodonAccountsService = init(instance); + public LiveData getSuggestions(@NonNull String instance, String token, String limit) { + suggestionsMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = initv2(instance); new Thread(() -> { - List accountList = null; - Call> suggestionsCall = mastodonAccountsService.getSuggestions(token, limit); + Call> suggestionsCall = mastodonAccountsService.getSuggestions(token, limit); + Suggestions suggestions = new Suggestions(); if (suggestionsCall != null) { try { - Response> suggestionsResponse = suggestionsCall.execute(); + Response> suggestionsResponse = suggestionsCall.execute(); if (suggestionsResponse.isSuccessful()) { - accountList = suggestionsResponse.body(); + suggestions.pagination = MastodonHelper.getOffSetPagination(suggestionsResponse.headers()); + suggestions.suggestions = suggestionsResponse.body(); } } catch (Exception e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); - List finalAccountList = accountList; - Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); + Runnable myRunnable = () -> suggestionsMutableLiveData.setValue(suggestions); mainHandler.post(myRunnable); }).start(); - return accountListMutableLiveData; + return suggestionsMutableLiveData; } /** diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java index cb764d01..3d58ae0c 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java @@ -23,17 +23,16 @@ 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.MastodonAdminService; -import app.fedilab.android.client.entities.api.AdminAccount; -import app.fedilab.android.client.entities.api.AdminAccounts; -import app.fedilab.android.client.entities.api.AdminReport; -import app.fedilab.android.client.entities.api.AdminReports; +import app.fedilab.android.client.entities.api.admin.AdminAccount; +import app.fedilab.android.client.entities.api.admin.AdminAccounts; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlock; +import app.fedilab.android.client.entities.api.admin.AdminDomainBlocks; +import app.fedilab.android.client.entities.api.admin.AdminReport; +import app.fedilab.android.client.entities.api.admin.AdminReports; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import okhttp3.OkHttpClient; @@ -54,13 +53,16 @@ public class AdminVM extends AndroidViewModel { private MutableLiveData adminAccountsListMutableLiveData; private MutableLiveData adminReportMutableLiveData; private MutableLiveData adminReporstListMutableLiveData; + private MutableLiveData adminDomainBlockMutableLiveData; + private MutableLiveData adminDomainBlockListMutableLiveData; + private MutableLiveData booleanMutableLiveData; + public AdminVM(@NonNull Application application) { super(application); } private MastodonAdminService 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())) @@ -69,6 +71,15 @@ public class AdminVM extends AndroidViewModel { return retrofit.create(MastodonAdminService.class); } + private MastodonAdminService initv2(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v2/") + .addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder())) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAdminService.class); + } + /** * View accounts matching certain criteria for filtering, up to 100 at a time. * @@ -107,7 +118,7 @@ public class AdminVM extends AndroidViewModel { String maxId, String sinceId, Integer limit) { - MastodonAdminService mastodonAdminService = init(instance); + MastodonAdminService mastodonAdminService = initv2(instance); adminAccountsListMutableLiveData = new MutableLiveData<>(); new Thread(() -> { Call> getAccountsCall = mastodonAdminService.getAccounts( @@ -551,4 +562,203 @@ public class AdminVM extends AndroidViewModel { }).start(); return adminReportMutableLiveData; } + + + /** + * View all domains blocked. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link List} of {@link AdminDomainBlocks}s + */ + public LiveData getDomainBlocks(@NonNull String instance, + String token, + String max_id) { + MastodonAdminService mastodonAdminService = init(instance); + adminDomainBlockListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List adminDomainBlockList; + Call> getDomainBlocks = mastodonAdminService.getDomainBlocks(token, max_id, MastodonHelper.statusesPerCall(getApplication())); + AdminDomainBlocks adminDomainBlocks = new AdminDomainBlocks(); + if (getDomainBlocks != null) { + try { + Response> getDomainBlocksResponse = getDomainBlocks.execute(); + if (getDomainBlocksResponse.isSuccessful()) { + adminDomainBlocks.adminDomainBlocks = getDomainBlocksResponse.body(); + adminDomainBlocks.pagination = MastodonHelper.getPagination(getDomainBlocksResponse.headers()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> adminDomainBlockListMutableLiveData.setValue(adminDomainBlocks); + mainHandler.post(myRunnable); + }).start(); + return adminDomainBlockListMutableLiveData; + } + + + /** + * View a single blocked domain + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link List} of {@link AdminDomainBlocks}s + */ + public LiveData getDomainBlock(@NonNull String instance, + String token, + String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminDomainBlockMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminDomainBlock adminDomainBlock = null; + Call getDomainBlock = mastodonAdminService.getDomainBlock(token, id); + if (getDomainBlock != null) { + try { + Response getDomainBlocksResponse = getDomainBlock.execute(); + if (getDomainBlocksResponse.isSuccessful()) { + adminDomainBlock = getDomainBlocksResponse.body(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminDomainBlock finalAdminDomainBlock = adminDomainBlock; + Runnable myRunnable = () -> adminDomainBlockMutableLiveData.setValue(finalAdminDomainBlock); + mainHandler.post(myRunnable); + }).start(); + return adminDomainBlockMutableLiveData; + } + + + /** + * View a single blocked domain + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link List} of {@link AdminDomainBlocks}s + */ + public LiveData createOrUpdateDomainBlock(@NonNull String instance, + String token, + AdminDomainBlock adminDomainBlock) { + MastodonAdminService mastodonAdminService = init(instance); + adminDomainBlockMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminDomainBlock admDomainBlock = null; + Call getDomainBlock; + if (adminDomainBlock.id == null) { + getDomainBlock = mastodonAdminService.blockDomain(token, adminDomainBlock.domain, adminDomainBlock.severity, adminDomainBlock.reject_media, adminDomainBlock.reject_reports, adminDomainBlock.private_comment, adminDomainBlock.public_comment, adminDomainBlock.obfuscate); + } else { + getDomainBlock = mastodonAdminService.updateBlockDomain(token, adminDomainBlock.id, adminDomainBlock.severity, adminDomainBlock.reject_media, adminDomainBlock.reject_reports, adminDomainBlock.private_comment, adminDomainBlock.public_comment, adminDomainBlock.obfuscate); + } + if (getDomainBlock != null) { + try { + Response getDomainBlocksResponse = getDomainBlock.execute(); + if (getDomainBlocksResponse.isSuccessful()) { + admDomainBlock = getDomainBlocksResponse.body(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminDomainBlock finalAdminDomainBlock = admDomainBlock; + Runnable myRunnable = () -> adminDomainBlockMutableLiveData.setValue(finalAdminDomainBlock); + mainHandler.post(myRunnable); + }).start(); + return adminDomainBlockMutableLiveData; + } + + + /** + * View all allowed domains. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link List} of {@link AdminDomainBlocks}s + */ + public LiveData getDomainAllows(@NonNull String instance, + String token, + String max_id) { + MastodonAdminService mastodonAdminService = init(instance); + adminDomainBlockListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List adminDomainBlockList; + Call> getDomainBlocks = mastodonAdminService.getDomainAllows(token, max_id, MastodonHelper.statusesPerCall(getApplication())); + AdminDomainBlocks adminDomainBlocks = new AdminDomainBlocks(); + if (getDomainBlocks != null) { + try { + Response> getDomainBlocksResponse = getDomainBlocks.execute(); + if (getDomainBlocksResponse.isSuccessful()) { + adminDomainBlocks.adminDomainBlocks = getDomainBlocksResponse.body(); + adminDomainBlocks.pagination = MastodonHelper.getPagination(getDomainBlocksResponse.headers()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> adminDomainBlockListMutableLiveData.setValue(adminDomainBlocks); + mainHandler.post(myRunnable); + }).start(); + return adminDomainBlockListMutableLiveData; + } + + public LiveData deleteDomain(@NonNull String instance, + String token, + String id) { + MastodonAdminService mastodonAdminService = init(instance); + booleanMutableLiveData = new MutableLiveData<>(); + final boolean[] successReply = {false}; + new Thread(() -> { + Call getDomainBlock = mastodonAdminService.deleteBlockDomain(token, id); + if (getDomainBlock != null) { + try { + Response response = getDomainBlock.execute(); + successReply[0] = response.isSuccessful(); + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> booleanMutableLiveData.setValue(successReply[0]); + mainHandler.post(myRunnable); + }).start(); + return booleanMutableLiveData; + } + + /** + * View a single allowed domain + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link List} of {@link AdminDomainBlocks}s + */ + public LiveData getDomainAllow(@NonNull String instance, + String token, + String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminDomainBlockMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminDomainBlock adminDomainBlock = null; + Call getDomainBlock = mastodonAdminService.getDomainAllow(token, id); + if (getDomainBlock != null) { + try { + Response getDomainBlocksResponse = getDomainBlock.execute(); + if (getDomainBlocksResponse.isSuccessful()) { + adminDomainBlock = getDomainBlocksResponse.body(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminDomainBlock finalAdminDomainBlock = adminDomainBlock; + Runnable myRunnable = () -> adminDomainBlockMutableLiveData.setValue(finalAdminDomainBlock); + mainHandler.post(myRunnable); + }).start(); + return adminDomainBlockMutableLiveData; + } } diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java index 26445ae7..7a77d610 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java @@ -57,7 +57,7 @@ public class AppsVM extends AndroidViewModel { super(application); } - private MastodonAppsService init(String instance) { + private MastodonAppsService init(String instance) throws IllegalArgumentException { Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://" + instance + "/api/v1/") @@ -81,7 +81,13 @@ public class AppsVM extends AndroidViewModel { String scopes, String website) { appMutableLiveData = new MutableLiveData<>(); - MastodonAppsService mastodonAppsService = init(instance); + MastodonAppsService mastodonAppsService; + try { + mastodonAppsService = init(instance); + } catch (IllegalArgumentException e) { + appMutableLiveData.setValue(null); + return appMutableLiveData; + } new Thread(() -> { App app = null; Call appCall = mastodonAppsService.createApp(client_name, redirect_uris, scopes, website); 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 e58e4bd3..16271d4e 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 @@ -112,24 +112,24 @@ public class NotificationsVM extends AndroidViewModel { if (notificationsResponse.isSuccessful()) { List notFiltered = notificationsResponse.body(); notifications.notifications = TimelineHelper.filterNotification(getApplication().getApplicationContext(), notFiltered); - addFetchMoreNotifications(notifications.notifications, notificationList, timelineParams); notifications.pagination = MastodonHelper.getPagination(notificationsResponse.headers()); - if (notifications.notifications != null && notifications.notifications.size() > 0) { addFetchMoreNotifications(notifications.notifications, notificationList, timelineParams); - for (Notification notification : notifications.notifications) { - StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); - StatusCache statusCache = new StatusCache(); - statusCache.instance = timelineParams.instance; - statusCache.user_id = timelineParams.userId; - statusCache.notification = notification; - statusCache.slug = notification.type; - statusCache.type = Timeline.TimeLineEnum.NOTIFICATION; - statusCache.status_id = notification.id; - try { - statusCacheDAO.insertOrUpdate(statusCache, notification.type); - } catch (DBException e) { - e.printStackTrace(); + if (notFiltered != null && notFiltered.size() > 0) { + for (Notification notification : notFiltered) { + StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); + StatusCache statusCache = new StatusCache(); + statusCache.instance = timelineParams.instance; + statusCache.user_id = timelineParams.userId; + statusCache.notification = notification; + statusCache.slug = notification.type; + statusCache.type = Timeline.TimeLineEnum.NOTIFICATION; + statusCache.status_id = notification.id; + try { + statusCacheDAO.insertOrUpdate(statusCache, notification.type); + } catch (DBException e) { + e.printStackTrace(); + } } } } @@ -146,41 +146,39 @@ public class NotificationsVM extends AndroidViewModel { return notificationsMutableLiveData; } - public LiveData getNotificationCache(List notificationList, TimelinesVM.TimelineParams timelineParams) { + public LiveData getNotificationCache(List timelineNotification, TimelinesVM.TimelineParams timelineParams) { notificationsMutableLiveData = new MutableLiveData<>(); new Thread(() -> { StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); - Notifications notifications = null; + Notifications notifications = new Notifications(); + List notificationsDb; try { - notifications = statusCacheDAO.getNotifications(timelineParams.excludeType, timelineParams.instance, timelineParams.userId, timelineParams.maxId, timelineParams.minId, timelineParams.sinceId); - if (notifications != null) { - if (notifications.notifications != null && notifications.notifications.size() > 0) { - if (notificationList != null) { - List notPresentNotifications = new ArrayList<>(); - for (Notification notification : notifications.notifications) { - if (!notificationList.contains(notification)) { - notification.cached = true; - notPresentNotifications.add(notification); - } + notificationsDb = statusCacheDAO.getNotifications(timelineParams.excludeType, timelineParams.instance, timelineParams.userId, timelineParams.maxId, timelineParams.minId, timelineParams.sinceId); + if (notificationsDb != null && notificationsDb.size() > 0) { + if (timelineNotification != null) { + List notPresentNotifications = new ArrayList<>(); + for (Notification notification : notificationsDb) { + if (!timelineNotification.contains(notification)) { + notification.cached = true; + notPresentNotifications.add(notification); } - //Only not already present statuses are added - notifications.notifications = notPresentNotifications; - } - TimelineHelper.filterNotification(getApplication().getApplicationContext(), notifications.notifications); - if (notifications.notifications.size() > 0) { - addFetchMoreNotifications(notifications.notifications, notificationList, timelineParams); - notifications.pagination = new Pagination(); - notifications.pagination.min_id = notifications.notifications.get(0).id; - notifications.pagination.max_id = notifications.notifications.get(notifications.notifications.size() - 1).id; } + //Only not already present statuses are added + notificationsDb = notPresentNotifications; + } + notifications.notifications = TimelineHelper.filterNotification(getApplication().getApplicationContext(), notificationsDb); + if (notifications.notifications.size() > 0) { + addFetchMoreNotifications(notifications.notifications, timelineNotification, timelineParams); + notifications.pagination = new Pagination(); + notifications.pagination.min_id = notifications.notifications.get(0).id; + notifications.pagination.max_id = notifications.notifications.get(notifications.notifications.size() - 1).id; } } } catch (DBException e) { e.printStackTrace(); } Handler mainHandler = new Handler(Looper.getMainLooper()); - Notifications finalNotifications = notifications; - Runnable myRunnable = () -> notificationsMutableLiveData.setValue(finalNotifications); + Runnable myRunnable = () -> notificationsMutableLiveData.setValue(notifications); mainHandler.post(myRunnable); }).start(); return notificationsMutableLiveData; 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 b3fff957..d94d1ad0 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 @@ -463,21 +463,21 @@ public class TimelinesVM extends AndroidViewModel { StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); Statuses statuses = new Statuses(); try { - Statuses statusesDb = statusCacheDAO.geStatuses(timelineParams.slug, timelineParams.instance, timelineParams.userId, timelineParams.maxId, timelineParams.minId, timelineParams.sinceId); - if (statusesDb != null && statusesDb.statuses != null && statusesDb.statuses.size() > 0) { + List statusesDb = statusCacheDAO.geStatuses(timelineParams.slug, timelineParams.instance, timelineParams.userId, timelineParams.maxId, timelineParams.minId, timelineParams.sinceId); + if (statusesDb != null && statusesDb.size() > 0) { if (timelineStatuses != null) { List notPresentStatuses = new ArrayList<>(); - for (Status status : statusesDb.statuses) { + for (Status status : statusesDb) { if (!timelineStatuses.contains(status)) { status.cached = true; notPresentStatuses.add(status); } } //Only not already present statuses are added - statusesDb.statuses = notPresentStatuses; + statusesDb = notPresentStatuses; } - statuses.statuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), statusesDb.statuses, timelineParams.type); - if (statuses.statuses != null && statuses.statuses.size() > 0) { + statuses.statuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), statusesDb, timelineParams.type); + if (statuses.statuses.size() > 0) { addFetchMore(statuses.statuses, timelineStatuses, timelineParams); statuses.pagination = new Pagination(); statuses.pagination.min_id = statuses.statuses.get(0).id; @@ -528,21 +528,16 @@ public class TimelinesVM extends AndroidViewModel { conversationListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(timelineParams.instance); new Thread(() -> { - Conversations conversations = null; + Conversations conversations = new Conversations(); Call> conversationsCall = mastodonTimelinesService.getConversations(timelineParams.token, timelineParams.maxId, timelineParams.sinceId, timelineParams.minId, timelineParams.limit); if (conversationsCall != null) { - conversations = new Conversations(); try { Response> conversationsResponse = conversationsCall.execute(); if (conversationsResponse.isSuccessful()) { - List conversationList = conversationsResponse.body(); - conversations.conversations = conversationList; + conversations.conversations = conversationsResponse.body(); conversations.pagination = MastodonHelper.getPagination(conversationsResponse.headers()); - - if (conversationList != null && conversationList.size() > 0) { - - addFetchMoreConversation(conversationList, conversationsTimeline, timelineParams); - + if (conversations.conversations != null && conversations.conversations.size() > 0) { + addFetchMoreConversation(conversations.conversations, conversationsTimeline, timelineParams); for (Conversation conversation : conversations.conversations) { StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); StatusCache statusCache = new StatusCache(); @@ -564,8 +559,7 @@ public class TimelinesVM extends AndroidViewModel { } } Handler mainHandler = new Handler(Looper.getMainLooper()); - Conversations finalConversations = conversations; - Runnable myRunnable = () -> conversationListMutableLiveData.setValue(finalConversations); + Runnable myRunnable = () -> conversationListMutableLiveData.setValue(conversations); mainHandler.post(myRunnable); }).start(); @@ -577,36 +571,34 @@ public class TimelinesVM extends AndroidViewModel { conversationListMutableLiveData = new MutableLiveData<>(); new Thread(() -> { StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); - Conversations conversations = null; + Conversations conversations = new Conversations(); try { - conversations = statusCacheDAO.getConversations(timelineParams.instance, timelineParams.userId, timelineParams.maxId, timelineParams.minId, timelineParams.sinceId); - if (conversations != null) { - if (conversations.conversations != null && conversations.conversations.size() > 0) { - if (timelineConversations != null) { - List notPresentConversations = new ArrayList<>(); - for (Conversation conversation : conversations.conversations) { - if (!timelineConversations.contains(conversation)) { - conversation.cached = true; - timelineConversations.add(conversation); - } + List conversationsDb = statusCacheDAO.getConversations(timelineParams.instance, timelineParams.userId, timelineParams.maxId, timelineParams.minId, timelineParams.sinceId); + if (conversationsDb != null && conversationsDb.size() > 0) { + if (timelineConversations != null) { + List notPresentConversations = new ArrayList<>(); + for (Conversation conversation : conversationsDb) { + if (!timelineConversations.contains(conversation)) { + conversation.cached = true; + timelineConversations.add(conversation); } - //Only not already present statuses are added - conversations.conversations = notPresentConversations; - } - if (conversations.conversations.size() > 0) { - addFetchMoreConversation(conversations.conversations, timelineConversations, timelineParams); - conversations.pagination = new Pagination(); - conversations.pagination.min_id = conversations.conversations.get(0).id; - conversations.pagination.max_id = conversations.conversations.get(conversations.conversations.size() - 1).id; } + //Only not already present statuses are added + conversationsDb = notPresentConversations; + } + conversations.conversations = conversationsDb; + if (conversations.conversations.size() > 0) { + addFetchMoreConversation(conversations.conversations, timelineConversations, timelineParams); + conversations.pagination = new Pagination(); + conversations.pagination.min_id = conversations.conversations.get(0).id; + conversations.pagination.max_id = conversations.conversations.get(conversations.conversations.size() - 1).id; } } } catch (DBException e) { e.printStackTrace(); } Handler mainHandler = new Handler(Looper.getMainLooper()); - Conversations finalConversations = conversations; - Runnable myRunnable = () -> conversationListMutableLiveData.setValue(finalConversations); + Runnable myRunnable = () -> conversationListMutableLiveData.setValue(conversations); mainHandler.post(myRunnable); }).start(); return conversationListMutableLiveData; @@ -669,22 +661,24 @@ public class TimelinesVM extends AndroidViewModel { public LiveData> getLists(@NonNull String instance, String token) { mastodonListListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); + List mastodonListList = new ArrayList<>(); new Thread(() -> { - List mastodonListList = null; Call> getListsCall = mastodonTimelinesService.getLists(token); if (getListsCall != null) { try { Response> getListsResponse = getListsCall.execute(); if (getListsResponse.isSuccessful()) { - mastodonListList = getListsResponse.body(); + List mastodonLists = getListsResponse.body(); + if (mastodonLists != null) { + mastodonListList.addAll(mastodonLists); + } } } catch (Exception e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); - List finalMastodonListList = mastodonListList; - Runnable myRunnable = () -> mastodonListListMutableLiveData.setValue(finalMastodonListList); + Runnable myRunnable = () -> mastodonListListMutableLiveData.setValue(mastodonListList); mainHandler.post(myRunnable); }).start(); return mastodonListListMutableLiveData; diff --git a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml b/app/src/main/res/drawable/ic_baseline_account_circle_24.xml index f9110996..0b3fa82b 100644 --- a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml +++ b/app/src/main/res/drawable/ic_baseline_account_circle_24.xml @@ -1,7 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_filter_desc_24.xml b/app/src/main/res/drawable/ic_baseline_filter_desc_24.xml new file mode 100644 index 00000000..b311f427 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_filter_desc_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml b/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml index 11162198..33fa9bd3 100644 --- a/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml +++ b/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml @@ -1,7 +1,7 @@ + + diff --git a/app/src/main/res/layout/activity_actions.xml b/app/src/main/res/layout/activity_actions.xml index 88ce2245..052ae1c6 100644 --- a/app/src/main/res/layout/activity_actions.xml +++ b/app/src/main/res/layout/activity_actions.xml @@ -71,6 +71,22 @@ app:iconTint="@color/cyanea_accent_dark_reference" app:strokeColor="@color/cyanea_accent_dark_reference" /> + + + + diff --git a/app/src/main/res/layout/activity_admin_domainblock.xml b/app/src/main/res/layout/activity_admin_domainblock.xml new file mode 100644 index 00000000..6e1e1caf --- /dev/null +++ b/app/src/main/res/layout/activity_admin_domainblock.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_drafts.xml b/app/src/main/res/layout/activity_drafts.xml index 85563ae9..64a0f574 100644 --- a/app/src/main/res/layout/activity_drafts.xml +++ b/app/src/main/res/layout/activity_drafts.xml @@ -87,8 +87,7 @@ android:gravity="center" android:padding="10dp" android:text="@string/no_draft" - android:textSize="20sp" - android:typeface="serif" /> + android:textSize="20sp" /> diff --git a/app/src/main/res/layout/activity_filters.xml b/app/src/main/res/layout/activity_filters.xml index 0b151685..9640dae3 100644 --- a/app/src/main/res/layout/activity_filters.xml +++ b/app/src/main/res/layout/activity_filters.xml @@ -26,8 +26,7 @@ android:gravity="center" android:padding="10dp" android:text="@string/action_filters_empty_content" - android:textSize="20sp" - android:typeface="serif" /> + android:textSize="20sp" /> @@ -36,7 +36,7 @@ @@ -118,7 +118,7 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_list.xml b/app/src/main/res/layout/activity_list.xml index 262881a5..e55a04b5 100644 --- a/app/src/main/res/layout/activity_list.xml +++ b/app/src/main/res/layout/activity_list.xml @@ -22,7 +22,6 @@ android:text="@string/action_lists_empty" android:textSize="20sp" android:textStyle="bold" - android:typeface="serif" android:visibility="gone" /> + android:textSize="20sp" /> diff --git a/app/src/main/res/layout/activity_suggestions.xml b/app/src/main/res/layout/activity_suggestions.xml new file mode 100644 index 00000000..5d58d4fe --- /dev/null +++ b/app/src/main/res/layout/activity_suggestions.xml @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_admin_domain.xml b/app/src/main/res/layout/drawer_admin_domain.xml new file mode 100644 index 00000000..63d5dbec --- /dev/null +++ b/app/src/main/res/layout/drawer_admin_domain.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_domain_block.xml b/app/src/main/res/layout/drawer_domain_block.xml new file mode 100644 index 00000000..0207a1a3 --- /dev/null +++ b/app/src/main/res/layout/drawer_domain_block.xml @@ -0,0 +1,53 @@ + + + + + + + + + + \ 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 1708ffd8..d6fce74a 100644 --- a/app/src/main/res/layout/drawer_status.xml +++ b/app/src/main/res/layout/drawer_status.xml @@ -148,7 +148,7 @@ android:layout_marginStart="6dp" android:layout_weight="1" android:ellipsize="end" - android:maxLines="1" + android:singleLine="true" tools:text="@tools:sample/full_names" /> + android:padding="3dp"> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_pagination.xml b/app/src/main/res/layout/fragment_pagination.xml index ad96c567..f26f23ce 100644 --- a/app/src/main/res/layout/fragment_pagination.xml +++ b/app/src/main/res/layout/fragment_pagination.xml @@ -17,6 +17,7 @@ @@ -55,8 +56,7 @@ android:gravity="center" android:padding="10dp" android:text="@string/no_status" - android:textSize="20sp" - android:typeface="serif" /> + android:textSize="20sp" /> @@ -93,4 +93,17 @@ + diff --git a/app/src/main/res/layout/fragment_scheduled.xml b/app/src/main/res/layout/fragment_scheduled.xml index 4a6a804b..a107407b 100644 --- a/app/src/main/res/layout/fragment_scheduled.xml +++ b/app/src/main/res/layout/fragment_scheduled.xml @@ -46,8 +46,7 @@ android:gravity="center" android:padding="10dp" android:text="@string/no_scheduled_toots" - android:textSize="20sp" - android:typeface="serif" /> + android:textSize="20sp" /> diff --git a/app/src/main/res/layout/popup_notification_settings.xml b/app/src/main/res/layout/popup_notification_settings.xml index 330dcdfa..636f6fc0 100644 --- a/app/src/main/res/layout/popup_notification_settings.xml +++ b/app/src/main/res/layout/popup_notification_settings.xml @@ -109,6 +109,22 @@ android:layout_height="wrap_content" android:text="@string/notif_display_updates" app:icon="@drawable/ic_baseline_edit_24" /> + + + + diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index c82203cc..63e6efef 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -58,7 +58,11 @@ android:icon="@drawable/ic_baseline_trending_up_24" android:title="@string/trending" android:visible="true" /> - + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main_list.xml b/app/src/main/res/menu/menu_main_list.xml index 01b82f78..a3e9b56a 100644 --- a/app/src/main/res/menu/menu_main_list.xml +++ b/app/src/main/res/menu/menu_main_list.xml @@ -1,6 +1,12 @@ + + - + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 140f11a3..ad88ce9a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -17,19 +17,19 @@ Heslo Email Účty - Tooty + Zprávy Štítky Uložit Instance Instance: mastodon.social Nyní používáte účet %1$s Přidat účet - Obsah tootu byl zkopírován do schránky - Adresa tootu byla zkopírována do schránky + Obsah zprávy byl zkopírován do schránky + Adresa zprávy byla zkopírována do schránky Fotoaparát Smazat všechno Naplánovat - Velikost textu a ikony + Velikost textu Další Předchozí Otevřít čím @@ -57,17 +57,17 @@ Žádost o sledování Nastavení Poslat e-mail - Naplánované tooty + Naplánované zprávy Níže uvedené informace mohou popisovat uživatelský profil neúplně. Vložit smajlík Aplikace prozatím nenačetla uživatelské smajlíky. Are you sure you want to logout @%1$s@%2$s? - Žádné tooty k zobrazení - Přidat tento toot k oblíbeným? - Odstranit tento toot z oblíbených? - Boostnout tento toot? - Zrušit boost? + Žádné zprávy k zobrazení + Přidat tuto zprávu k oblíbeným\? + Odstranit tuto zprávu z oblíbených\? + Boostnout tuto zprávu\? + Zrušit boost\? Ztlumit Blokovat Nahlásit @@ -130,8 +130,8 @@ Nastala chyba při výběru média! Smazat médium? - Váš toot je prázdný! - Toot byl odeslán! + Vaše zpráva je prázdná! + Zpráva byla odeslána! Citlivý obsah? Žádné koncepty! Vyberte účet @@ -151,14 +151,15 @@ Žádný účet k zobrazení Není požadavek ke sledování - Tooty \n %1$s + Zprávy +\n %1$s Sleduji \n %1$s Sledující \n %1$s Odmítnout - Žádné naplánované tooty k zobrazení! - Odstranit naplánovaný toot? - Toot byl naplánován! + Žádné naplánované zprávy k zobrazení! + Odstranit naplánovanou zprávu\? + Zpráva byla naplánována! Plánované datum musí být vyšší než aktuální hodina! Časový interval pro ztlumení musí být vyšší než jedna minuta. @@ -184,10 +185,10 @@ Účet není nadále ztlumen! Účet je sledován! Účet není nadále sledován! - Toot byl boostnut! - Toot již není boostnut! - Toot byl přidán k oblíbeným! - Toot byl odstraněn z oblíbených! + Zpráva byla boostnuta! + Zpráva již není boostnuta! + Zpráva byla přidána k oblíbeným! + Zpráva byla odstraněna z oblíbených! Oops! Došlo k chybě! Došlo k chybě! Instance nevrátila autorizační kód! Tato doména není platná! @@ -196,7 +197,7 @@ Nelze vykonat akci Při překladu došlo k chybě! - Počet tootů pro jedno nahrání + Počet zpráv pro jedno nahrání Zakázat GIF avatary Oznámení v případě sledování Oznámení v případě boostnutí vašeho tootu @@ -275,7 +276,7 @@ Port Přihlašovací jméno Heslo - Přidat podrobnosti tootu při sdílení + Přidat podrobnosti zprávy při sdílení Podpořit aplikaci na Liberapay Chyba v regulárním výrazu! Časová osa nenalezena na této instanci! @@ -296,7 +297,7 @@ Konverzace Velikost písmen ani varování o obsahu nebudou brána v potaz Zahodit místo skrytí - Filtrované tooty zmizí nezvratně i v případě, že je filtr později odstraněn + Filtrované zprávy zmizí nezvratně i v případě, že je filtr později odstraněn V případě, že klíčové slovo nebo fráze je pouze alfanumerické, filtr se uplatní pouze pokud odpovídá celému slovu Celé slovo Kontext filtru @@ -311,7 +312,7 @@ Nové oblíbení Nová zmínka Anketa skončila - Záloha tootů + Záloha zpráv New posts Stahování médií Vybrat tón @@ -323,11 +324,11 @@ Peertube instance Použít Emoji One Informace - Zobrazit náhled ve všech tootech + Zobrazit náhled ve všech zprávách Identifikátor účtu byl zkopírován do schránky! Změna jazyka - Ořezat dlouhé tooty - Ořezat tooty delší než \'x\' řádků. 0 znamená vypnuto. + Ořezat dlouhé zprávy + Ořezat zprávy delší než \'x\' řádků. 0 znamená vypnuto. Zobrazit více Zobrazit méně Štítek již existuje! @@ -373,8 +374,8 @@ Kategorie Popis Sdílet - Tooty (Server) - Tooty (zařízení) + Zprávy (Server) + Zprávy (Zařízení) Časové osi Rozhraní Kontakty @@ -400,7 +401,7 @@ Kategorie Přesunout časovou osu Skrýt časovou osu - Změnit pořadí časových os + Spravovat časové osy Seznam trvale smazán Sledovaná instance odstraněna Připnuté značky odstraněny @@ -725,4 +726,14 @@ Oprávnění nebylo uděleno! Opravdu chcete vymazat cache\? Pokud máte koncepty s médii, budou přiložená média ztracena. Časové osy v seznamu + Odesílání zprávy… + Odesílání zprávy %d/%d + Běží! + Neběží! + verze: %s +\n %s uživatelů - %s statusů + Odebrat status + Vybrat nejlepší shodu + Zkontrolováno: %s + Přidat status \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7e39a813..7f0a8576 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -393,7 +393,7 @@ Kategorien Zeitleiste verschieben Zeitleiste ausblenden - Zeitleiste neu anordnen + Verwaltung von Zeitleisten Liste endgültig gelöscht Gefolgte Instanz entfernt Angeheftetes Schlagwort entfernt @@ -718,7 +718,7 @@ Sind Sie sich sicher, den Cache zu leeren\? Wenn Sie Entwürfe mit Bildern, etc haben, werden die angehängten Dateien gelöscht. Verlassen, ohne das Bild zu speichern\? Form - Domain + Domäne Personal Nachrichtensprache Meine Instanz @@ -770,4 +770,104 @@ Push-Dienst Die App konnte kein Token abrufen Nachricht bearbeiten + Angepinnte Zeitleisten löschen\? + Bericht eingereicht + Datenschutz-Bestimmungen + Die angepinnte Zeitleiste entfernen\? + Bist Du sicher, dass diese Zeitleiste nicht mehr angepinnt sein soll\? + Geblockte Domänen + %1$s bearbeitet %2$s + Domäne freigeben + Du hast keine geblockten Domänen + Sicher, dass Du %1$s wieder freigeben willst\? + Vorschläge + Nicht interessiert + Zuweisung aufheben + Als ungelöst markieren + Konto verwarnt + Konto-Sperrung aufgehoben + Konto gesperrt + Als gelöst markieren + Konto abgelehnt + Konto genehmigt + Meldung + Medien in Benachrichtigungen für Reblogs und Favoriten werden angezeigt + Übersetzung in eine spezifische Sprache erzwingen. Wähle den ersten Wert um die Einstellungen zurückzusetzen + Nachrichten-Verlauf + Bearbeitet am %1$s + Erstellt am %1$s + Nicht angezeigte Antworten + Benachrichtigungen wurden aus dem Zwischenspeicher entfernt. + eMail-Status + Beigetreten + Aktuelle IP + Erlauben + Warnen + Benutzer per eMail benachrichtigen + Benutzerdefinierte Warnung + Status von Meldungen + Stummgeschaltet + Benutzer + Moderator + Administrator + Bestätigt + Unbestätigt + Mir zuweisen + Konto deaktiviert + Konto nicht stummgeschaltet + Konto stummgeschaltet + App neu starten\? + Neustart + Du solltest die App neu starten um die Änderungen anzuwenden. + Du folgst bisher keinen Tags! + Tag nicht mehr folgen + Bist Du sicher dass Du diesem Tag nicht mehr folgen willst\? + Nicht mehr folgen + Tag folgen + Schreibe den Tag dem Du folgen willst + Gefolgte Tags + Tag folgen + Falls aktiv wird die App alle zusammenhängenden Benachrichtigungen einklappen + Liste bearbeiten + Profile + Deine Instanz scheint diese Funktion nicht zu unterstützen! + Erkunde Trends dieser Instanz + Nachricht bearbeiten + Aktualisierungen + Mit Warnung verstecken + Vollständig verstecken + Den gefilterten Inhalt komplett verstecken und so verhalten als ob dieser nie existiert hätte + Start und Listen + Titel + Schlagwort oder Satz + Schlagwort löschen + Schlagwort hinzufügen + Gefiltert: %1$s + Trotzdem anzeigen + Zeitleiste löschen + Angemeldet + Neue Registrierung + Eine Benutzer hat sich registriert + Remote-Profil anzeigen + Die App kann keine Remote-Daten finden! + Über Updates benachrichtigen + Domänen + Neues Update + Eine Nachricht die Du geteilt hast wurde bearbeitet + Ein Benutzer hat eine Meldung gesendet + Registrierungen + Neue Meldung + Verstecke den gefilterten Inhalt hinter einer Warnung die den Titel des Filters enthält + Aktion filtern + Wähle die Aktion aus die durchgeführt werden soll wenn ein Beitrag Deinem Filter entspricht + Hinzufügen des Kontos zur Liste fehlgeschlagen! + Benachrichtigungen behalten + Neue Meldung (Moderatoren) + Neue Registrierung (Moderatoren) + Benachrichtigungen zusammenfassen + Betrifft nur \"öffentliche\" Antworten. Falls aktiv, werden Deine Antworten automatisch \"nicht gelistet\" statt \"öffentlich\" + Anmeldestatus + Sprachen in der Auswahl + Erlaube die Liste der Sprachen in der Auswahl beim Verfassen einer Nachricht zu reduzieren. + Bezeichnung des Tags ist nicht zulässig! \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 61d843bf..d13e95e7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -399,7 +399,7 @@ Catégories Déplacer le fil Cacher le fil - Réorganiser les fils + Gérer les fils Liste supprimée définitivement Instance suivie supprimée Balise épinglée supprimée @@ -420,9 +420,9 @@ Image enregistrée avec succès ! Échec de l\'enregistrement de l\'image Ajouter un élément de sondage - Mettre la conversation en sourdine + Masquer la conversation Enlever la sourdine de la discussion - La conversation n\'est plus mise en sourdine ! + La conversation n’est plus masquée ! La conversation est mise en sourdine Général Régional @@ -857,4 +857,11 @@ Statut de la connexion Afficher le profil distant L\'application ne trouve pas de données distantes ! + Un⋅e utilisateur⋅ice a envoyé un signalement + Inscriptions + Nouvelle mise à jour + Nouvel abonnement + Nouveau signalement + Un message que vous avez partagé a été édité + Un⋅e utilisateur⋅ice s\'est abonné⋅e \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index ad756220..8ec4cea8 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -4,7 +4,7 @@ Acerca da instancia Intimidade Caché - Desconectar + Pechar sesión Pechar Si @@ -17,19 +17,19 @@ Contrasinal Correo-e Contas - Toots + Mensaxes Etiquetas Gardar Instancia Instancia: mastodon.social A funcionar coa conta %1$s Engadir unha conta - Copiouse ao portapapeis o contido do toot - Copiouse o URL do toot ao portapapéis + Copiose ao portapapeis o contido da mensaxe + Copiouse o URL da mensaxe ao portapapéis Cámara Elimnar todo Programar - Tamaño do texto e iconas + Tamaño do texto Seguinte Anterior Abrir con @@ -50,24 +50,24 @@ Traducir Inicio - Liña temporal local + Cronoloxía local Usuarias acaladas Usuarias bloqueadas Notificacións Solicitudes de seguimento Axustes Enviar un correo-e - Toots programados + Mensaxes programadas A información inferior sobre a usuaria podería estar incompleta. Incrustar emoji Polo momento a app non permite emojis personalizados. - Tes a certeza de querer desconectar @%1$s@%2$s? + Tes a certeza de querer pechar a sesión @%1$s@%2$s\? - Sen toot que mostrar - Engadir este toot aos seus favoritos? - Eliminar este toot dos seus favoritos? - Promover este toot? - Deixar de promover o toot? + Non hai mensaxes a mostrar + Engadir esta publicación a favoritos\? + Eliminar esta mensaxe dos favoritos\? + Promover esta mensaxe\? + Retirar promoción da mensaxe\? Acalar Bloquear Informar @@ -122,8 +122,8 @@ Algo fallou ao engadir os medios! Eliminar este elemento? - O seu toot está baldeiro! - O toot foi enviado! + A mensaxe está baleira! + Enviaches a mensaxe! Contido sensible? Sen borradores! Escolla unha conta @@ -143,14 +143,15 @@ Sen conta que mostrar Sen petición de seguimento - Toots \n %1$s + Mensaxes +\n %1$s Seguindo \n %1$s Seguidoras \n %1$s Rexeitar - Sen toots programados! - Borrar o toot programado? - O toot foi programado! + Non hai mensaxes programadas! + Eliminar a mensaxe programada\? + A mensaxe foi programada! A data programada debe ser posterior a hora actual! O tempo de silencio debe ser maior de un minuto. @@ -176,10 +177,10 @@ A conta xa non está acalada! A conta foi seguida! A conta xa non está seguida! - O toot foi promovido! - O toot xa non está promovido! - Este toot foi engadido aos seus favoritos! - Este toot eliminouse dos favoritos! + A mensaxe foi promovida! + Retirácheslle a promoción á mensaxe! + A mensaxe foi engadida ás favoritas! + A mensaxe foi eliminada das favoritas! Vaia! Algo fallou! Algo fallou! A instancia non devolveu o código de autorización! O dominio da instancia non semella ser válido! @@ -188,7 +189,7 @@ Non se poden realizar accións Algo fallou ao traducir! - Número de toots por carga + Número de mensaxes por carga Desactivar avatares GIF Notificar cando alguén a segue Notificar cando alguén promove un dos seus toots @@ -265,12 +266,12 @@ Activar proxy? Host Porto - Conectar + Acceder Contrasinal - Engadir detalles do toot ao compartir + Engadir detalles da mensaxe ao compartir Axude a app en Liberapay Hai un fallo na expresión regular! - Non se atoparon liñas temporais en esta instancia! + Non se atoparon cronoloxías nesta instancia! Seguir instancia Xa segue a esta instancia! Asociados @@ -282,13 +283,13 @@ Filtros Sen filtros que mostrar. Pode crear un pulsando no botón \"+\". Palabra chave ou frase - Liña temporal de inicio + Cronoloxía de inicio Liñas de tempo públicas Notificacións Conversas - Farase coincidir sen importar se é texto agochado ou con aviso de contido na mensaxe + Farase coincidir sen importar se é texto ten maiúsculas ou con aviso de contido na mensaxe Desbotar en lugar de ocultar - Os toots filtrados desaparecerán irreversiblemente, incluso si despois elimina o filtro + As mensaxes filtradas desaparecerán irreversiblemente, incluso se despois eliminas o filtro Cando a palabra chave é só alfanumérica, só se aplicará si coincide a palabra completa Palabra completa Filtrar contextos @@ -303,23 +304,25 @@ Novo favorito Nova mención Rematou a sondaxe - Respaldo de Toots + Copia de Apoio de mensaxes Novas publicacións Descarga de medios Escoller Tono Activar tramo horario - Desexa bloquear a %s\? + Queres bloquear a %s\? +\n +\nNon verás ningún contido dese dominio en ningunha cronoloxía pública nin nas notificacións. As túas seguidoras nese dominio serán eliminadas. Bloquear dominio O dominio foi bloqueado Obtendo estado remoto Instancia Peertube Utilizar Emoji One Información - Mostrar vista previa en todos os toots + Mostrar vista previa en tódalas mensaxes Copiouse o id de conta ao portapapeis! Cambiar o idioma - Cortar en varios os toots longos - Repartir os toots superiores a \'x\' liñas. Cero significa desactivado. + Recortar mensaxes longas + Cortar as mensaxes superiores a \'x\' liñas. Cero significa desactivado. Mostrar máis Mostrar menos A etiqueta xa existe! @@ -365,19 +368,19 @@ Categoría Descrición Compartir - Toots (Servidor) - Toots (Dispositivo) - Liñas temporais + Mensaxes (Servidor) + Mensaxes (Dispositivo) + Cronoloxías Interface Contactos Algo fallou ao seleccionar o ficheiro de respaldo! - Desconectar conta + Pechar sesión da conta Todo Copiar ligazón peticións http bloqueadas pola aplicación Lista de peticións bloquedas Enviar - Filtrar liña temporal con etiquetas + Filtrar cronoloxía con etiquetas Sen etiquetas Obter metadatos se o URL os comparte desde outras apps @@ -388,16 +391,16 @@ finaliza en %s Votar Rematou a sondaxe na que participou - Rematou unha sondaxe na que tooteou + Rematou unha enquisa que publicaches Categorías - Mover liña temporal - Agochar liña temporal - Ordear liñas temporais + Mover cronoloxía + Agochar cronoloxía + Xestionar cronoloxías Lista eliminada de xeito permanente Eliminouse o seguimento da instancia Eliminouse a etiqueta fixada Desfacer - As liñas temporais principais só poden ocultarse! + As cronoloxías principais só poden ocultarse! Marcar os medios sempre como sensibles Instancia GNU Incluír etiquetas nas respostas @@ -442,11 +445,13 @@ O contrasinal debe conter ao menos 8 caracteres O nome de usuaria debería ter só letras, números e guión baixo Conta creada! - Xa ten unha conta!\n\n - Lembre validar o correo-e nas seguintes48 horas.\n\n - Xa pode conectar coa súa conta escribindo %1$s no primeiro campo e pulsando Conectar.\n\n - Importante: se a súa instancia require validación, recibirá un correo unha vez sexa validada! - + Xa tes unha conta! +\n +\n Lembra validar o email nas seguintes 48 horas. +\n +\n Xa podes acceder coa túa conta escribindo %1$s no primeiro campo e premendo Acceder. +\n +\n Importante: se a túa instancia require validación, recibirás un correo unha vez sexa validada! ¿Gardar mensaxe en borradores? Administración Informes @@ -467,7 +472,7 @@ A aplicación precisa acceso a gravación de audio Mensaxe de voz Durante o tempo marcado, a app enviará notificacións. Pode revertir (silenciar) esta marxe coa roda da dereita. - Non se recortarán as vistas previas en liñas temporais + Non se recortarán as vistas previas nas cronoloxías Inserta automáticamente un salto de liña tras a mención para por en maiúscula a primeira letra Permitir as creadoras de contido compartir estados desde as súas fontes RSS Redactar @@ -503,7 +508,7 @@ Queres deixar de seguir esta conta? Mostrar cadro de confirmación antes de deixar de seguir Substituír ligazóns a Medium - Substituír as ligazóns a medium.com cunha interface de código aberto alternativa centrada na privacidade. + Usar unha interface alternativa para Medium Por defecto: scribe.rip Usar un sistema de notificacións push para ter notificacións en tempo real. Engadir notas @@ -518,12 +523,12 @@ Cambiar a cor do nome de usuaria enriba das mensaxes Cambiar a cor da cabeceira das mensaxes repetidas Publicacións - Cor de fondo das publicacións en liñas temporais + Cor de fondo das publicacións nas cronoloxías Restablecer cores Premendo aquí restableces os valores por omisión Restablecer Iconas - Cor das iconas inferiores nas liñas temporais + Cor das iconas inferiores nas cronoloxías Logo da instancia Editar perfil Tomar decisión @@ -570,4 +575,292 @@ Non se atopan distribuidores! Precisas un distribuidor para recibir notificacións push.\nAtoparás máis detalles en %1$s.\n\nTamén podes desactivar as notificacións push nos axustes para ignorar esa mensaxe. Elixe un distribuidor + Twitter + Usar interface alternativa para Twitter + Dominio da interface para Twitter + Instagram + Usar unha interface alternativa para Instagram + Dominio da interface para Instagram + Reddit + Funciona! + O problema non axeita a ningunha das categorías + Outra cousa + Acalar a %1$s + Bloquear a %1$s + Engadiuse a mensaxe aos marcadores! + Confirma a retirada do seguimento + Elixe se a base do decorado debe ser clara ou escura + Menú barra superior + Outro + Engadir estado + Conta tipo bot + Base do decorado + Permitir crear o teu propio decorado + Decorados da comunidade + Tipo de enquisa: + Elixe un decorado que foi creado por persoas da comunidade + Duración da enquisa: + Tamaño das iconas + Visibilidade por defecto para as mensaxes: + Retirouse a mensaxe dos marcadores! + Enviando mensaxe %d/%d + Cres que non segue determinada regra + Mencións + Favoritas + Tes a certeza de querer eliminar tódalas notificacións\? Non ten volta atrás. + Interaccións + Bloqueado + Gardar cambios + Agochar contido < + Deter gravación + Denuncia de %1$s + Cóntanos cal é o problema coa publicación + Elixe a mellor coincidencia + Ligazóns maliciosas, engano, ou respostas repetitivas + Non é algo que queira ver + É spam + Hai algunha publicación que apoie esta denuncia\? + Que regra se incumpriu\? + Promocións + Resultados da enquisa + Actualizacións das persoas + Segue + Engadir filtro + Engadir Campo + Desbloqueado + Programado + Conta non descubrible + Eliminar campo + Elixe un decorado + Máis accións + Número de contas por carga + Número de notificacións por carga + Música + O campo non pode estar baleiro! + YouTube + Usar interface alternativa para YouTube + Domino da interface para YouTube + Usar interface alternativa para Reddit + Dominio da interface para Reddit + Tipo de notificacións + Elixe o tipo de notificacións + Desactivar notificacións + Menú inferior + Última actividade + Personalizar cronoloxías + Enviouse a mensaxe! + Tipos de notificación a mostrar + Mostar sempre o botón do marcador + Mostrar + Importáronse correctamente os axustes + Exportáronse os axustes + Exportáronse correctamente os axustes + Non semella ser unha instancia válida! + Promovida por + Favorecida por + Só para seguidoras + Ex.: Contido sensible + Publicando mensaxe… + Non funciona! + Continuar + Personalizar + Uptime: %,.2f %% + Mostrar contido > + Non me gusta + Viola as regras do servidor + Non queres ver isto\? + Aquí tes opcións para controlar o que ves en Mastodon: + Deixa de seguir a %1$s + Estás seguindo esta conta. Para non ver as súas publicacións na cronoloxía de inicio, deixa de seguila. + Non verás as súas publicacións. Poderá seguirte a ti e ver as túas publicacións e non saberá que a tes acalada. + Non verás as súas publicacións. Non poderá ver as túas publicacións nin seguirte. Poderá comprobar que a tes bloqueada. + Elixe todo o que aplique + version: %s +\n %s usuarias - %s estados + Comprobado o: %s + A conta está noutro servidor. Enviamos alí unha copia anónimizada da denuncia\? + Enviar a %1$s + Hai algo máis que debamos saber\? + Comentarios adicionais + Únete ao fediverso + Enviouse a denuncia! + Non tes unha conta\? + Ola! Convidámoste a unirte ao Fediverso. + \"Mastodon non é un sitio web único como Twitter ou Facebook, é unha rede de miles de comunidades xestionadas por diferentes organizacións e persoas que proporciona unha experiencia do internet social conxunta.\" + Limpar tódalas notificacións + Marcar tódalas notificacións como lidas + Mostrar tódalas categorías + O nome da lista non é válido! + Non hai contas nesta lista! + Tes a certeza de querer eliminar este campo\? + Actualizouse o perfil! + Son moderadora + Localización + "Tamén a favoreceron: " + Tamén a promocionaron: + Durante este marco temporal + Son das notificacións + Eliminar estado + Cronoloxías nunha lista + Ao activalas, tódalas cronoloxías fixadas monstraranse nun menú despregable + Ao activala, a app só terá unha única barra para as cronoloxías + Mensaxes na caché para Inicio + Obter máis mensaxes… + Anuncio - %1$s - %2$s + Limpar caché + Abrir mensaxe orixinal + As conoloxías irán á caché para que a aplicación sexa máis rápida. + Responder + Opcións de presentación + A app non puido obter un token + Dominio + Establece o número máx. de caracteres + Notas da publicación + Non se puido subir o multimedia! + Establecer a demora entre obtencións + Mostra contadores nas mensaxes + Cargar miniaturas multimedia + Mostrar multimedia + Mensaxe na caché + Obter notificacións + Barra única + Usar caché + Mostrar contadores + Abrir borrador + Tempo entre notificacións + Estado + Resolta + Oval + A miña instancia + Baleirar caché + Mensaxes na caché para outras cronoloxías + Mensaxes gardadas en borradores + A miña app + Tes a certeza de saír sen gardar a imaxe\? + Toca aquí para actualizar a enquisa + Aprobado + Tamaño dos ficheiros da caché + Aprobar + Orixe da conta denunciada + Máis recente + Forma + Modo Borrar + Tes a certeza de querer baleirar a caché\? Se tes borradores que inclúen multimedia, estes perderanse. + A miña conta + Montrará icona con contador para as novas mensaxes nas cronoloxías da lapela + Cargar axustes exportados + Xestor de notificacións + Importar axustes + Mostrar cronoloxías + Permiso non outorgado! + Agrupar notificacións + Ao activarlo, a app xuntará tódalas notificacións relacionadas + Lembrar posición nas cronoloxías + Equipo + Filtro + Rectángulo + Usar o idioma por defecto no sistema + Obter notificacións cada: + Exportar axustes + Liña + Idioma das mensaxes + Debes reiniciar a app para aplicar os cambios. + Sen confirmar + Marcar como resolta + A non pode atopar os datos remotos! + Enviou unha denuncia + Política de privacidade + Dominios bloqueados + Editada o %1$s + Desbloquear dominio + Non tes dominios bloqueados + Tes a certeza de desbloquear %1$s\? + Elixe un logo + Cambiar logo + Cambia o logo da app no teu dispositivo + Fixar mensaxe + Traducir mensaxes + Forzar a tradución a un idioma concreto. Elixe o primeiro valor para restablecer aos axustes do sistema + Creada o %1$s + Indentación máx. nos fíos + Respostas non listadas + Suxestións + Non interesada + Só aplica a respostas \"públicas\". Ao activalo, as respostas terán automáticamente visibilidade \"non listada\" no lugar de \"pública\" + A mensaxe xa non está fixada! + Fixouse a mensaxe + Eliminadas da caché as notificacións. + Estado do email + Estado da sesión + Asignarma + Retirar asignación + Conta con avisos + Retirada a suspensión da conta + Conta suspendida + Conta acalada + Denunciar + Desafixar mensaxe + Uniuse + IP recente + Permitir + Avisar + Notificar á usuaria por email + Aviso personalizado + Estados denunciados + Acalada + Usuaria + Moderadora + Administradora + Confirmada + Macar como non resolta + Conta rexeitada + Conta aprobada + Conta reactivada + Conta desactivada + Conta restablecida + Estado + Reiniciar a app\? + Reiniciar + Idiomas no selector + Permite reducir a lista de idiomas no selector ao escribir unha mensaxe. + Non segues ningún cancelo! + Non seguir cancelo + Queres deixar de seguir este cancelo\? + Non seguir + Seguir un cancelo + Escribe o cancelo a seguir + O nome do cancelo non é válido! + Cancelos seguidos + Seguir cancelo + Mostrar multimedia nas notificacións + Mostrarase o multimedia nas notificacións de RT e FAV + Editar lista + Perfís + A túa instancia non ten soporte para esta función! + Mira os temas en voga na instancia + Editou unha mensaxe + Actualizacións + Editar mensaxe + %1$s editou %2$s + Historial da mensaxe + Acción do filtro + Elixe que acción debe realizar o filtro ao atopar unha concordancia + Agochar cun aviso + Agochar completamente + Agochar o contido filtrado detrás dun aviso que menciona o título do filtro + Agochar completamente o contido filtrado, como se non tivese existido + Inicio e listas + Título + Palabra ou frase a filtrar + Eliminar palabra + Engadir palabra + Filtrado %1$s + Mostrar igualmente + Mostrar perfil remoto + A app non puido engadir a conta á lista! + Eliminar cronoloxía + Rexistrada + Eliminar as cronoloxías fixadas\? + Eliminar a cronoloxía fixada\? + Tes a certeza de desafixar a cronoloxía\? \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 65487523..77b64af8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -389,7 +389,7 @@ カテゴリ タイムラインの移動 タイムラインを非表示にする - タイムラインの並び替え + タイムラインの管理 削除されたリスト フォローしたサーバーを削除 ピン留めされたタグの削除 @@ -601,7 +601,7 @@ 全ての通知を既読にする 全てのカテゴリーを表示する 全ての通知を削除しますか?この操作は取り消せません。 - 相互 + 交流 変更を保存 ボットアカウント アカウントを検索可能にする @@ -698,7 +698,7 @@ アナウンス・%1$s - %2$s 他のタイムラインの投稿のキャッシュ 下書きのメッセージ - ファイルキャッシュのサイズ + キャッシュファイルのサイズ キャッシュを消去 直近 フィルター @@ -846,4 +846,17 @@ 更新 強制的に表示する リモートのプロフィールを表示 + プライバシーポリシー + ピン留めしたタイムラインを削除しますか? + ピン留めされたタイムラインを削除しますか? + このタイムラインのピン留めを解除しますか? + ブロックしたドメイン + ドメインのブロックを解除 + ブロックしているドメインはありません + %1$sのブロックを解除しますか? + サジェスト + 興味なし + タイムラインの削除 + レポートを送信しました + 登録しました \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b710463b..661f0daa 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -17,19 +17,19 @@ Senha E-mail Contas - Toots + Mensagens Tags Salvar Instância Instância: mastodon.social Usando a conta %1$s agora Adicionar conta - O conteúdo do toot foi copiado para a área de transferência - O link do toot foi copiado para a área de transferência + O conteúdo da mensagem foi copiado para a área de transferência + O link da mensagem foi copiado para a área de transferência Câmera Excluir tudo Agendar - Tamanho dos textos e ícones + Tamanho dos textos Próximo Anterior Abrir com @@ -57,17 +57,17 @@ Seguidores pendentes Configurações Mandar um e-mail - Toots agendados + Mensagens agendadas As informações abaixo podem refletir incompletamente o perfil do usuário. Inserir emoji O aplicativo não achou emojis personalizados no momento. Tem a certeza que quer sair @%1$s@%2$s? - Sem toots - Favoritar toot? - Desfavoritar toot? - Dar boost? - Desfazer boost? + Sem mensagens para mostrar + Favoritar essa mensagem\? + Desfavoritar essa mensagem\? + Dar boost\? + Desfazer boost\? Silenciar Bloquear Denunciar @@ -105,25 +105,29 @@ %d d %d segundo + %d segundos %d segundos %d minuto + %d minutos %d minutos %d hora + %d horas %d horas %d dia + %d dias %d dias Ocorreu um erro ao selecionar a mídia! Remover mídia? - Toot vazio! - Toot enviado! + Sua mensagem está vazia! + Mensagem enviada! Conteúdo sensível? Sem rascunhos! Escolha uma conta @@ -143,14 +147,15 @@ Sem conta Sem seguidores pendentes - Toots \n %1$s + Mensagens +\n %1$s Seguindo \n %1$s Seguidores \n %1$s Recusar - Sem toots agendados! - Excluir toot agendado? - Toot agendado! + Sem mensagens agendadas! + Excluir mensagem agendada\? + Mensagem agendada! A data de agendamento deve ser após o horário atual! O tempo de silêncio deve ser maior do que um minuto. @@ -176,10 +181,10 @@ Silêncio desativado! Você seguiu a conta! Você deixou de seguir a conta! - Toot compartilhado! + Mensagem compartilhada! Boost desfeito! - Toot favoritado! - Toot desfavoritado! + Mensagem adicionada aos favoritos! + A mensagem foi removida dos seus favoritos! Oops! Ocorreu um erro! Ocorreu um erro! A instância não retornou um código de autorização! Parece que o domínio da instância não é válido! @@ -188,7 +193,7 @@ A ação não pode ser feita ou não é suportada Ocorreu um erro na tradução! - Número de toots por vez + Número de mensagens por vez Desativar GIF Notificar quando alguém te seguir Notificar quando alguém der boost nos seus toots @@ -200,7 +205,7 @@ Mostrar diálogo antes de favoritar Notificar? Silenciar notificações - Segundos para expirar a prévia de mídia sensível, 0 para desativar. + Segundos para expirar a prévia de mídia sensível, 0 para desativar Tempo limite da descrição de ficheiros multimédia (segundos, 0 significa desligado) Compartilhamento externo personalizado Seu link de compartilhamento externo… @@ -267,7 +272,7 @@ Porta Login Senha - Adicionar detalhes do toot ao compartilhar + Adicionar detalhes da mensagem ao compartilhar Apoie o aplicativo no Liberapay Há um erro na expressão regular! Nenhuma timeline foi encontrada nesta instância! @@ -286,9 +291,9 @@ Timelines públicas Notificações Conversas - Serão correspondidas independente de maiúsculas ou minúsculas no texto ou no aviso de conteúdo de um toot + Serão correspondidas independente de maiúsculas ou minúsculas no texto ou no aviso de conteúdo de uma mensagem Apagar em vez de ocultar - Os toots filtrados desaparecerão irreversivelmente, mesmo se o filtro for removido mais tarde + As mensagens filtradas desaparecerão irreversivelmente, mesmo se o filtro for removido mais tarde Quando a palavra-chave ou frase só tem alfanuméricos, ela será aplicada somente se combinar com a palavra inteira Palavra inteira Contextos do filtro @@ -303,7 +308,7 @@ Novo favorito Nova menção Sondagem terminada - Backup de Toots + Backup de Mensagens Novas publicações Baixar mídia Selecionar toque @@ -315,11 +320,11 @@ Instância Peertube Usar Emoji One Informação - Mostrar pré-visualizações em todos os toots + Mostrar pré-visualizações em todas as mensagens O nome de utilizador foi copiado para a área de transferência! Mudar o idioma - Cortar toots longos - Limitar toots por \'x\' linhas. Zero significa desativar. + Cortar mensagens longas + Limitar mensagens por \'x\' linhas. Zero significa desativar. Mostrar mais Mostrar menos A tag já existe! @@ -365,8 +370,8 @@ Categoria Descrição Compartilhar - Toots (Servidor) - Toots (Disp) + Mensagens (Servidor) + Mensagens (Dispositivo) Timelines Interface Contatos @@ -570,4 +575,64 @@ No distributors found! Necessita de um distribuidor para receber notificações instantâneas. \nVai encontrar mais detalhes em %1$s.\n\nPode desactivar as mensagens instantâneas nas configurações para ignorar esta mensagem. Select a distributor + Sons de notificação + Usar um frontend alternativo para Twitter + Twitter + Domínio do frontend para Twitter + Instagram + Usar um frontend alternativo para Instagram + Domínio do frontend para Instagram + Personalizado + A instância não parece ser válida! + Recebeu boost de + Remover status + Publicando mensagem… + Escolha o tipo de notificação + Tipo de notificação + Escolha um tema + Domínio do frontend para YouTube + Usar um frontend alternativo para o Reddit + Continuar + Outro + Ex.: Conteúdo Sensível + Está inoperante! + versão: %s +\n%s users - %s status + Em atividade há: %,.2f %% + Tamanho dos ícones + A mensagem foi adicionada aos seus favoritos! + A mensagem foi removida dos seus favoritos! + Número de notificações por vez + YouTube + Usar um frontend alternativo para o YouTube + Parar de gravar + Relatório %1$s + Esconder conteúdo < + Visibilidade padrão das mensagens: + Número de contas por vez + Esse campo não pode ser vazio! + Domínio do frontend para Reddit + Está ativo! + Base do tema + Durante esse momento + Desabilitar notificações + Permite criar seu tema personalizado + Mais Ações + Escolha se a base do tema será dark ou light + Escolha um tema que foi criado por colaboradores + Temas de colaboradores + Personalizar timelines + Tipos de notificação para mostrar + Reddit + As configurações foram importadas com sucesso + Verificado em: %s + Mostrar conteúdo > + Música + As configurações foram exportadas + Configurações foram exportadas com sucesso + Favoritada por + Apenas seguidores + Diga-nos o que está havendo com este post + Adicionar status + Enviando mensagem %d/%d \ No newline at end of file diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 25d6872f..768e9969 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -849,4 +849,16 @@ Mustra su profilu remotu S\'aplicatzione non resesset a agatare datos remotos! Iscantzella sa lìnia de tempus + Polìtica de riservadesa + Bogare sa lìnia de tempus apicada\? + Ses seguru de bòlere isblocare cussa lìnia de tempus\? + Iscantzellare is lìnias de tempus apicadas\? + Domìnios blocados + Isbloca su domìniu + Non tenes domìnios blocados + Ses seguru de bòlere isblocare %1$s\? + Cussìgios + No interessadu + At imbiadu un\'informe + S\'est registradu \ 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 4fbc3c96..1661cd33 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -857,4 +857,29 @@ Zaman çizelgesini sil Rapor gönderildi Kaydoldu + Öneriler + İlgilenmiyor + Engellenen etki alanları + Etki alanlarını engellemediniz + Etki alanının engelini kaldır + %1$s engellemesini kaldırmak istediğinizden emin misiniz\? + Gizlilik politikası + Sabitlenen zaman çizelgeleri silinsin mi\? + Bu zaman çizelgesinin sabitlemesini kaldırmak istediğinizden emin misiniz\? + Sabitlenen zaman çizelgesi kaldırılsın mı\? + Yeni rapor + Etki alanları + Güncellemeler için bildir + Yeni kayıt (moderatörler) + Yeni güncelleme + bir rapor gönderdi + Yeni kayıt + Paylaştığınız bir mesaj düzenlendi + Bir kullanıcı kaydoldu + Bir kullanıcı bir rapor gönderdi + Bildirimleri sakla + Yeni rapor (moderatörler) + Kayıtlar + Başka bir hesapla aç + Listeleri sırala \ 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 502ec56f..8741ba6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -320,6 +320,9 @@ Poll Ended Messages Backup New posts + New update + New sign-up + New report Media Download Select Tone Enable time slot @@ -351,6 +354,13 @@ Custom emoji picker Favicon Add description for media (for the visually impaired) + + + Silence + Suspend + None + + Never 30 minutes @@ -404,6 +414,9 @@ Vote A poll you have voted in has ended A poll you published has ended + A message you shared has been edited + A user signed-up + A user sent a report Categories Move timeline Hide timeline @@ -449,7 +462,8 @@ I agree to %1$s and %2$s server rules terms of service - Sign up + Sign-up + Sign-ups This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. This field cannot be empty! Passwords don\'t match! @@ -1254,6 +1268,9 @@ SET_NOTIF_FAVOURITE SET_NOTIF_POLL SET_NOTIF_STATUS + SET_NOTIF_UPDATE + SET_NOTIF_ADMIN_SIGNUP + SET_NOTIF_ADMIN_REPORT SET_FONT_SCALE SET_MAX_INDENTATION SET_FONT_SCALE_INT @@ -1904,4 +1921,39 @@ Delete timeline Submitted a report Signed up + sent a report + Suggestions + Not interested + Blocked domains + Unblock domain + You have not blocked domains + Are you sure to unblock %1$s? + Privacy policy + Remove pinned timeline? + Are you sure to unpin that timeline? + Delete the pinned timelines? + Domains + Keep notifications + Notify for updates + New sign-up (moderators) + New report (moderators) + Open with another account + Order lists + Reject media + Reject reports + The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. + Severity + Silence will make the account\'s posts invisible to anyone who isn\'t following them. Suspend will remove all of the account\'s content, media, and profile data. Use None if you just want to reject media files. + Reject media files + Ignore all reports coming from this domain. Irrelevant for suspensions + Reject reports + Ignore all reports coming from this domain. Irrelevant for suspensions + Obfuscate domain name + Partially obfuscate the domain name in the list if advertising the list of domain limitations is enabled + Private comment + Comment about this domain limitation for internal use by the moderators. + Public comment + Comment about this domain limitation for the general public, if advertising the list of domain limitations is enabled. + Changes have been saved! + Create domain block \ No newline at end of file diff --git a/app/src/main/res/xml/pref_notifications.xml b/app/src/main/res/xml/pref_notifications.xml index 9c4e7cb6..a6fe7ac3 100644 --- a/app/src/main/res/xml/pref_notifications.xml +++ b/app/src/main/res/xml/pref_notifications.xml @@ -77,6 +77,24 @@ app:key="@string/SET_NOTIF_STATUS" app:singleLineTitle="false" app:title="@string/set_notif_status" /> + + +