From d0d8974d08ba811276b219220714ba648652739f Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 28 May 2022 14:46:51 +0200 Subject: [PATCH] Moderation - Action on accounts --- app/src/main/AndroidManifest.xml | 4 + .../activities/AdminAccountActivity.java | 415 +++++++++++++++ .../ui/drawer/AdminAccountAdapter.java | 14 + .../android/viewmodel/mastodon/AdminVM.java | 2 +- .../res/layout/activity_admin_account.xml | 495 ++++++++++++++++++ .../main/res/layout/drawer_admin_account.xml | 1 + app/src/main/res/values/strings.xml | 3 + 7 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/fedilab/android/activities/AdminAccountActivity.java create mode 100644 app/src/main/res/layout/activity_admin_account.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b4ec64f6..2194018b4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -75,6 +75,10 @@ android:name=".activities.ProfileActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:label="@string/account" /> + . */ + + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ForegroundColorSpan; +import android.text.style.UnderlineSpan; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +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.databinding.ActivityAdminAccountBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.viewmodel.mastodon.AdminVM; +import app.fedilab.android.viewmodel.mastodon.NodeInfoVM; +import es.dmoral.toasty.Toasty; + + +public class AdminAccountActivity extends BaseActivity { + + private AdminAccount adminAccount; + private Account account; + private ScheduledExecutorService scheduledExecutorService; + private ActivityAdminAccountBinding binding; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityAdminAccountBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + Bundle b = getIntent().getExtras(); + if (b != null) { + adminAccount = (AdminAccount) b.getSerializable(Helper.ARG_ACCOUNT); + if (adminAccount != null) { + account = adminAccount.account; + } + } + postponeEnterTransition(); + + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); + } + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + binding.toolbar.setPopupTheme(Helper.popupStyle()); + if (account != null) { + new Thread(() -> { + account = SpannableHelper.convertAccount(AdminAccountActivity.this, account); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> initializeView(account); + mainHandler.post(myRunnable); + + }).start(); + + } else { + Toasty.error(AdminAccountActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); + finish(); + } + } + + private void initializeView(Account account) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(AdminAccountActivity.this); + if (account == null) { + Toasty.error(AdminAccountActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); + finish(); + return; + } + binding.title.setText(String.format(Locale.getDefault(), "@%s", account.acct)); + + // MastodonHelper.loadPPMastodon(binding.profilePicture, account); + binding.appBar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + + if (Math.abs(verticalOffset) - binding.appBar.getTotalScrollRange() == 0) { + binding.profilePicture.setVisibility(View.VISIBLE); + binding.title.setVisibility(View.VISIBLE); + } else { + binding.profilePicture.setVisibility(View.GONE); + binding.title.setVisibility(View.GONE); + } + }); + + + binding.username.setText(String.format(Locale.getDefault(), "@%s", adminAccount.username)); + binding.domain.setText(adminAccount.domain); + binding.email.setText(adminAccount.email); + StringBuilder lastActive = new StringBuilder(); + if (adminAccount.ips != null) { + for (AdminAccount.IP ip : adminAccount.ips) { + lastActive.append(Helper.shortDateToString(ip.used_at)).append(" - ").append(ip.ip).append("\r\n"); + } + } + if (lastActive.toString().trim().length() == 0) { + binding.lastActiveContainer.setVisibility(View.GONE); + } + if (adminAccount.email == null || adminAccount.email.trim().length() == 0) { + binding.emailContainer.setVisibility(View.GONE); + } + binding.lastActive.setText(lastActive.toString()); + binding.disabled.setText(adminAccount.disabled ? R.string.yes : R.string.no); + binding.approved.setText(adminAccount.approved ? R.string.yes : R.string.no); + binding.silenced.setText(adminAccount.silenced ? R.string.yes : R.string.no); + binding.suspended.setText(adminAccount.suspended ? R.string.yes : R.string.no); + + binding.disableAction.setText(adminAccount.disabled ? R.string.undisable : R.string.disable); + binding.approveAction.setText(adminAccount.approved ? R.string.reject : R.string.approve); + binding.silenceAction.setText(adminAccount.silenced ? R.string.unsilence : R.string.silence); + binding.suspendAction.setText(adminAccount.suspended ? R.string.unsuspend : R.string.suspend); + + AdminVM adminVM = new ViewModelProvider(AdminAccountActivity.this).get(AdminVM.class); + + binding.disableAction.setOnClickListener(v -> { + if (adminAccount.disabled) { + adminVM.enable(MainActivity.currentInstance, MainActivity.currentToken, account.id) + .observe(AdminAccountActivity.this, adminAccountResult -> { + adminAccount.disabled = false; + binding.disableAction.setText(R.string.disable); + binding.disabled.setText(R.string.no); + }); + } else { + adminVM.performAction(MainActivity.currentInstance, MainActivity.currentToken, account.id, "disable ", null, null, null, null); + adminAccount.disabled = true; + binding.disableAction.setText(R.string.undisable); + binding.disabled.setText(R.string.yes); + } + }); + + binding.approveAction.setOnClickListener(v -> { + if (adminAccount.approved) { + adminVM.reject(MainActivity.currentInstance, MainActivity.currentToken, account.id) + .observe(AdminAccountActivity.this, adminAccountResult -> { + adminAccount.approved = false; + binding.approveAction.setText(R.string.approve); + binding.approved.setText(R.string.no); + }); + } else { + adminVM.approve(MainActivity.currentInstance, MainActivity.currentToken, account.id); + adminAccount.approved = true; + binding.approveAction.setText(R.string.reject); + binding.approved.setText(R.string.yes); + } + }); + + binding.silenceAction.setOnClickListener(v -> { + if (adminAccount.disabled) { + adminVM.unsilence(MainActivity.currentInstance, MainActivity.currentToken, account.id) + .observe(AdminAccountActivity.this, adminAccountResult -> { + adminAccount.silenced = false; + binding.silenceAction.setText(R.string.silence); + binding.disabled.setText(R.string.no); + }); + } else { + adminVM.performAction(MainActivity.currentInstance, MainActivity.currentToken, account.id, "silence", null, null, null, null); + adminAccount.silenced = true; + binding.disableAction.setText(R.string.unsilence); + binding.disabled.setText(R.string.yes); + } + }); + + binding.suspendAction.setOnClickListener(v -> { + if (adminAccount.disabled) { + adminVM.unsuspend(MainActivity.currentInstance, MainActivity.currentToken, account.id) + .observe(AdminAccountActivity.this, adminAccountResult -> { + adminAccount.suspended = false; + binding.suspendAction.setText(R.string.suspend); + binding.suspended.setText(R.string.no); + }); + } else { + adminVM.performAction(MainActivity.currentInstance, MainActivity.currentToken, account.id, "suspend", null, null, null, null); + adminAccount.suspended = true; + binding.disableAction.setText(R.string.unsuspend); + binding.suspended.setText(R.string.yes); + } + }); + + + //Retrieve relationship with the connected account + List accountListToCheck = new ArrayList<>(); + accountListToCheck.add(account.id); + //Animate emojis + if (account.emojis != null && account.emojis.size() > 0) { + boolean disableAnimatedEmoji = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); + if (!disableAnimatedEmoji) { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + scheduledExecutorService.scheduleAtFixedRate(() -> binding.accountDn.invalidate(), 0, 130, TimeUnit.MILLISECONDS); + } + } + + //Tablayout for timelines/following/followers + + boolean disableGif = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_GIF), false); + String targetedUrl = disableGif ? account.avatar_static : account.avatar; + Glide.with(AdminAccountActivity.this) + .asDrawable() + .dontTransform() + .load(targetedUrl).into( + new CustomTarget() { + @Override + public void onResourceReady(@NonNull final Drawable resource, Transition transition) { + binding.profilePicture.setImageDrawable(resource); + startPostponedEnterTransition(); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + binding.profilePicture.setImageResource(R.drawable.ic_person); + startPostponedEnterTransition(); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + } + ); + //Load header + MastodonHelper.loadProfileMediaMastodon(binding.bannerPp, account, MastodonHelper.MediaAccountType.HEADER); + //Redraws icon for locked accounts + final float scale = getResources().getDisplayMetrics().density; + if (account.locked) { + Drawable img = ContextCompat.getDrawable(AdminAccountActivity.this, R.drawable.ic_baseline_lock_24); + assert img != null; + img.setBounds(0, 0, (int) (16 * scale + 0.5f), (int) (16 * scale + 0.5f)); + binding.accountUn.setCompoundDrawables(null, null, img, null); + } else { + binding.accountUn.setCompoundDrawables(null, null, null, null); + } + //Peertube account watched by a Mastodon account + //Bot account + if (account.bot) { + binding.accountBot.setVisibility(View.VISIBLE); + } + if (account.acct != null) { + setTitle(account.acct); + } + + + final SpannableString content = new SpannableString(getString(R.string.disclaimer_full)); + content.setSpan(new UnderlineSpan(), 0, content.length(), 0); + content.setSpan(new ForegroundColorSpan(ContextCompat.getColor(AdminAccountActivity.this, R.color.cyanea_accent_reference)), 0, content.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + //This account was moved to another one + if (account.moved != null) { + binding.accountMoved.setVisibility(View.VISIBLE); + Drawable imgTravel = ContextCompat.getDrawable(AdminAccountActivity.this, R.drawable.ic_baseline_card_travel_24); + assert imgTravel != null; + imgTravel.setBounds(0, 0, (int) (20 * scale + 0.5f), (int) (20 * scale + 0.5f)); + binding.accountMoved.setCompoundDrawables(imgTravel, null, null, null); + //Retrieves content and make account names clickable + SpannableString spannableString = SpannableHelper.moveToText(AdminAccountActivity.this, account); + binding.accountMoved.setText(spannableString, TextView.BufferType.SPANNABLE); + binding.accountMoved.setMovementMethod(LinkMovementMethod.getInstance()); + } + + + binding.accountDn.setText(account.span_display_name != null ? account.span_display_name : account.display_name, TextView.BufferType.SPANNABLE); + binding.accountUn.setText(String.format("@%s", account.acct)); + binding.accountUn.setOnLongClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String account_id = account.acct; + if (account_id.split("@").length == 1) + account_id += "@" + BaseMainActivity.currentInstance; + ClipData clip = ClipData.newPlainText("mastodon_account_id", "@" + account_id); + Toasty.info(AdminAccountActivity.this, getString(R.string.account_id_clipbloard), Toast.LENGTH_SHORT).show(); + assert clipboard != null; + clipboard.setPrimaryClip(clip); + return false; + }); + + MastodonHelper.loadPPMastodon(binding.accountPp, account); + binding.accountPp.setOnClickListener(v -> { + Intent intent = new Intent(AdminAccountActivity.this, MediaActivity.class); + Bundle b = new Bundle(); + Attachment attachment = new Attachment(); + attachment.description = account.acct; + attachment.preview_url = account.avatar; + attachment.url = account.avatar; + attachment.remote_url = account.avatar; + attachment.type = "image"; + ArrayList attachments = new ArrayList<>(); + attachments.add(attachment); + b.putSerializable(Helper.ARG_MEDIA_ARRAY, attachments); + b.putInt(Helper.ARG_MEDIA_POSITION, 1); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation(AdminAccountActivity.this, binding.accountPp, attachment.url); + // start the new activity + startActivity(intent, options.toBundle()); + }); + + + binding.accountDate.setText(Helper.shortDateToString(account.created_at)); + binding.accountDate.setVisibility(View.VISIBLE); + + String[] accountInstanceArray = account.acct.split("@"); + String accountInstance = BaseMainActivity.currentInstance; + if (accountInstanceArray.length > 1) { + accountInstance = accountInstanceArray[1]; + } + + NodeInfoVM nodeInfoVM = new ViewModelProvider(AdminAccountActivity.this).get(NodeInfoVM.class); + String finalAccountInstance = accountInstance; + nodeInfoVM.getNodeInfo(accountInstance).observe(AdminAccountActivity.this, nodeInfo -> { + if (nodeInfo != null && nodeInfo.software != null) { + binding.instanceInfo.setText(nodeInfo.software.name); + binding.instanceInfo.setVisibility(View.VISIBLE); + + binding.instanceInfo.setOnClickListener(v -> { + Intent intent = new Intent(AdminAccountActivity.this, InstanceProfileActivity.class); + Bundle b = new Bundle(); + b.putString(Helper.ARG_INSTANCE, finalAccountInstance); + intent.putExtras(b); + startActivity(intent); + + }); + } + }); + + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } + return true; + } + + @Override + public void onDestroy() { + if (scheduledExecutorService != null) { + scheduledExecutorService.shutdownNow(); + scheduledExecutorService = null; + } + super.onDestroy(); + } + + public enum action { + FOLLOW, + UNFOLLOW, + BLOCK, + UNBLOCK, + NOTHING, + MUTE, + UNMUTE, + REPORT, + BLOCK_DOMAIN + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java index 55ed0b514..5d35e7111 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AdminAccountAdapter.java @@ -15,6 +15,9 @@ package app.fedilab.android.ui.drawer; * see . */ +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -24,6 +27,7 @@ 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.databinding.DrawerAdminAccountBinding; import app.fedilab.android.helper.Helper; @@ -33,6 +37,7 @@ import app.fedilab.android.helper.MastodonHelper; public class AdminAccountAdapter extends RecyclerView.Adapter { private final List adminAccountList; + private Context context; public AdminAccountAdapter(List adminAccountList) { this.adminAccountList = adminAccountList; @@ -49,6 +54,7 @@ public class AdminAccountAdapter extends RecyclerView.Adapter { + Intent intent = new Intent(context, AdminAccountActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, adminAccount); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + }); holder.binding.username.setText(adminAccount.account.display_name); holder.binding.acct.setText(String.format(Locale.getDefault(), "@%s", adminAccount.account.acct)); holder.binding.postCount.setText(String.valueOf(adminAccount.account.statuses_count)); 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 a3bb52a3d..c73e60212 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 @@ -181,7 +181,7 @@ public class AdminVM extends AndroidViewModel { String reportId, String warningPresetId, String text, - boolean sendEmailNotification) { + Boolean sendEmailNotification) { MastodonAdminService mastodonAdminService = init(instance); new Thread(() -> { Call performActionCall = mastodonAdminService.performAction(token, accountId, type, reportId, warningPresetId, text, sendEmailNotification); diff --git a/app/src/main/res/layout/activity_admin_account.xml b/app/src/main/res/layout/activity_admin_account.xml new file mode 100644 index 000000000..664fb5318 --- /dev/null +++ b/app/src/main/res/layout/activity_admin_account.xml @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_admin_account.xml b/app/src/main/res/layout/drawer_admin_account.xml index 95e2072e1..87de3994c 100644 --- a/app/src/main/res/layout/drawer_admin_account.xml +++ b/app/src/main/res/layout/drawer_admin_account.xml @@ -5,6 +5,7 @@ android:layout_marginStart="6dp" android:layout_marginTop="5dp" android:layout_marginEnd="6dp" + android:id="@+id/admin_account_container" android:padding="6dp"> Staff Most recent Filter + Domain + Approved + Approve