Moderation - Display accounts and filter them

This commit is contained in:
Thomas 2022-05-28 09:31:19 +02:00
parent 35c248bcb2
commit 26e6fab5d9
26 changed files with 1385 additions and 136 deletions

View File

@ -100,6 +100,11 @@
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/interactions"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.AdminActionActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/administration"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.MastodonListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"

View File

@ -78,6 +78,7 @@ import java.util.List;
import java.util.regex.Pattern;
import app.fedilab.android.activities.ActionActivity;
import app.fedilab.android.activities.AdminActionActivity;
import app.fedilab.android.activities.BaseActivity;
import app.fedilab.android.activities.ComposeActivity;
import app.fedilab.android.activities.ContextActivity;
@ -337,6 +338,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
} else if (id == R.id.nav_follow_requests) {
Intent intent = new Intent(this, FollowRequestActivity.class);
startActivity(intent);
} else if (id == R.id.nav_administration) {
Intent intent = new Intent(this, AdminActionActivity.class);
startActivity(intent);
}
binding.drawerLayout.close();
return false;
@ -571,6 +575,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
if (account.mastodon_account.locked) {
binding.navView.getMenu().findItem(R.id.nav_follow_requests).setVisible(true);
}
if (account.admin) {
binding.navView.getMenu().findItem(R.id.nav_administration).setVisible(true);
}
if (bottomMenu != null) {
//ManageClick on bottom menu items
if (binding.bottomNavView.findViewById(R.id.nav_home) != null) {

View File

@ -0,0 +1,256 @@
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 <http://www.gnu.org/licenses>. */
import static app.fedilab.android.activities.AdminActionActivity.AdminEnum.REPORT;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.gson.annotations.SerializedName;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityAdminActionsBinding;
import app.fedilab.android.databinding.PopupAdminFilterAccountsBinding;
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.FragmentAdminReport;
public class AdminActionActivity extends BaseActivity {
public static Boolean local = true, remote = true, active = true, pending = true, disabled = true, silenced = true, suspended = true, staff = null, orderByMostRecent = true;
private ActivityAdminActionsBinding binding;
private boolean canGoBack;
private FragmentAdminReport fragmentAdminReport;
private FragmentAdminAccount fragmentAdminAccount;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityAdminActionsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
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));
}
private void displayTimeline(AdminEnum type) {
canGoBack = true;
if (type == REPORT) {
ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> {
fragmentAdminReport = new FragmentAdminReport();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type);
bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + type.getValue());
fragmentAdminReport.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragmentAdminReport);
fragmentTransaction.commit();
});
} else {
ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> {
fragmentAdminAccount = new FragmentAdminAccount();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type);
bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + type.getValue());
fragmentAdminAccount.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragmentAdminAccount);
fragmentTransaction.commit();
});
}
switch (type) {
case REPORT:
setTitle(R.string.reports);
break;
case ACCOUNT:
setTitle(R.string.accounts);
break;
}
invalidateOptionsMenu();
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
if (canGoBack && getTitle().toString().equalsIgnoreCase(getString(R.string.accounts))) {
getMenuInflater().inflate(R.menu.menu_admin_account, menu);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
} else if (item.getItemId() == R.id.action_filter) {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(AdminActionActivity.this, Helper.dialogStyle());
PopupAdminFilterAccountsBinding binding = PopupAdminFilterAccountsBinding.inflate(getLayoutInflater());
alertDialogBuilder.setView(binding.getRoot());
if (local != null && remote == null) {
binding.locationLocal.setChecked(true);
} else if (remote != null && local == null) {
binding.locationRemote.setChecked(true);
} else {
binding.locationAll.setChecked(true);
}
binding.location.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.location_all) {
local = true;
remote = true;
} else if (checkedId == R.id.location_local) {
local = true;
remote = null;
} else if (checkedId == R.id.location_remote) {
local = null;
remote = true;
}
});
if (pending != null && suspended == null && active == null) {
binding.moderationPending.setChecked(true);
} else if (suspended != null && pending == null && active == null) {
binding.moderationSuspended.setChecked(true);
} else if (active != null && pending == null && suspended == null) {
binding.moderationActive.setChecked(true);
} else {
binding.moderationAll.setChecked(true);
}
binding.moderation.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.moderation_all) {
active = true;
suspended = true;
pending = true;
} else if (checkedId == R.id.moderation_active) {
active = true;
suspended = null;
pending = null;
} else if (checkedId == R.id.moderation_suspended) {
active = null;
suspended = true;
pending = null;
} else if (checkedId == R.id.moderation_pending) {
active = null;
suspended = null;
pending = true;
}
});
if (staff != null) {
binding.permissionsStaff.setChecked(true);
} else {
binding.permissionsAll.setChecked(true);
}
binding.permissions.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.permissions_all) {
staff = null;
} else if (checkedId == R.id.permissions_staff) {
staff = true;
}
});
if (orderByMostRecent != null) {
binding.orderByMostRecent.setChecked(true);
} else {
binding.orderByLastActive.setChecked(true);
}
binding.orderBy.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.order_by_most_recent) {
orderByMostRecent = true;
} else if (checkedId == R.id.order_by_last_active) {
orderByMostRecent = null;
}
});
alertDialogBuilder.setPositiveButton(R.string.filter, (dialog, id) -> {
final FragmentTransaction ft1 = getSupportFragmentManager().beginTransaction();
ft1.detach(fragmentAdminAccount);
ft1.commit();
final FragmentTransaction ft2 = getSupportFragmentManager().beginTransaction();
ft2.attach(fragmentAdminAccount);
ft2.commit();
dialog.dismiss();
});
alertDialogBuilder.setNegativeButton(R.string.reset, (dialog, id) -> {
binding.locationAll.callOnClick();
binding.permissionsAll.callOnClick();
binding.moderationAll.callOnClick();
binding.orderByMostRecent.callOnClick();
});
AlertDialog alert = alertDialogBuilder.create();
alert.show();
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
if (canGoBack) {
canGoBack = false;
ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.buttonContainer, () -> {
if (fragmentAdminReport != null) {
fragmentAdminReport.onDestroyView();
}
if (fragmentAdminAccount != null) {
fragmentAdminAccount.onDestroyView();
}
setTitle(R.string.administration);
invalidateOptionsMenu();
});
} else {
super.onBackPressed();
}
}
public enum AdminEnum {
@SerializedName("REPORT")
REPORT("REPORT"),
@SerializedName("ACCOUNT")
ACCOUNT("ACCOUNT");
private final String value;
AdminEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}

View File

@ -32,7 +32,6 @@ import java.util.Locale;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivitySettingsBinding;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.settings.FragmentAdministrationSettings;
import app.fedilab.android.ui.fragment.settings.FragmentComposeSettings;
import app.fedilab.android.ui.fragment.settings.FragmentInterfaceSettings;
import app.fedilab.android.ui.fragment.settings.FragmentLanguageSettings;
@ -120,12 +119,6 @@ public class SettingsActivity extends BaseActivity {
currentFragment = fragmentThemingSettings;
category = getString(R.string.theming);
break;
case ADMINISTRATION:
FragmentAdministrationSettings fragmentAdministrationSettings = new FragmentAdministrationSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentAdministrationSettings);
currentFragment = fragmentAdministrationSettings;
category = getString(R.string.administration);
break;
case LANGUAGE:
FragmentLanguageSettings fragmentLanguageSettings = new FragmentLanguageSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentLanguageSettings);

View File

@ -109,7 +109,9 @@ public interface MastodonAdminService {
@Header("Authorization") String token,
@Field("resolved") Boolean resolved,
@Field("account_id") String account_id,
@Field("target_account_id") String target_account_id
@Field("target_account_id") String target_account_id,
@Field("max_id") String max_id,
@Field("limit") int limit
);
@FormUrlEncoded

View File

@ -16,10 +16,11 @@ package app.fedilab.android.client.entities.api;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
public class AdminAccount {
public class AdminAccount implements Serializable {
@SerializedName("id")
public String id;
@ -59,7 +60,7 @@ public class AdminAccount {
public String invited_by_account_id;
public final class IP {
public static class IP implements Serializable {
@SerializedName("ip")
public String ip;
@SerializedName("used_at")

View File

@ -0,0 +1,24 @@
package app.fedilab.android.client.entities.api;
/* Copyright 2021 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 <http://www.gnu.org/licenses>. */
import java.util.List;
public class AdminAccounts {
public Pagination pagination = new Pagination();
public List<AdminAccount> adminAccounts;
}

View File

@ -16,29 +16,34 @@ package app.fedilab.android.client.entities.api;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
public class AdminReport {
public class AdminReport implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("account")
public Account account;
@SerializedName("action_taken")
public String action_taken;
@SerializedName("action_taken_by_account")
public String action_taken_by_account;
@SerializedName("assigned_account")
public Account assigned_account;
@SerializedName("category")
public String category;
@SerializedName("comment")
public String comment;
@SerializedName("created_at")
public Date created_at;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("account")
public Account account;
@SerializedName("target_account")
public Account target_account;
@SerializedName("assigned_account")
public Account assigned_account;
@SerializedName("action_taken_by_account")
public String action_taken_by_account;
@SerializedName("statuses")
public List<Status> statuses;
@SerializedName("rules")
public List<Instance.Rule> rules;
@SerializedName("updated_at")
public Date updated_at;
}

View File

@ -0,0 +1,24 @@
package app.fedilab.android.client.entities.api;
/* Copyright 2021 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 <http://www.gnu.org/licenses>. */
import java.util.List;
public class AdminReports {
public Pagination pagination = new Pagination();
public List<AdminReport> adminReports;
}

View File

@ -43,7 +43,7 @@ class RecyclerViewThreadLines(context: Context, private val lineInfoList: List<L
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
if (position < 0) return
if (position < 0 || position >= lineInfoList.size) return
val level = lineInfoList[position].level
val startMargin = margin * level + margin * fontScale
if (parent.layoutDirection == View.LAYOUT_DIRECTION_LTR) outRect.left = startMargin else outRect.right = startMargin
@ -54,7 +54,7 @@ class RecyclerViewThreadLines(context: Context, private val lineInfoList: List<L
for (i in 0 until childCount) {
val view = parent.getChildAt(i)
val position = parent.getChildAdapterPosition(view)
if (position < 0) return
if (position < 0 || position >= lineInfoList.size) return
val lineInfo = lineInfoList[position]
val level = lineInfo.level

View File

@ -0,0 +1,97 @@
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 <http://www.gnu.org/licenses>. */
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.util.Locale;
import app.fedilab.android.client.entities.api.AdminAccount;
import app.fedilab.android.databinding.DrawerAdminAccountBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
public class AdminAccountAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<AdminAccount> adminAccountList;
public AdminAccountAdapter(List<AdminAccount> adminAccountList) {
this.adminAccountList = adminAccountList;
}
public int getCount() {
return adminAccountList.size();
}
public AdminAccount getItem(int position) {
return adminAccountList.get(position);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
DrawerAdminAccountBinding itemBinding = DrawerAdminAccountBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new AccountAdminViewHolder(itemBinding);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
AdminAccount adminAccount = adminAccountList.get(position);
AccountAdminViewHolder holder = (AccountAdminViewHolder) viewHolder;
MastodonHelper.loadPPMastodon(holder.binding.pp, adminAccount.account);
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));
holder.binding.followersCount.setText(String.valueOf(adminAccount.account.followers_count));
holder.binding.email.setText(adminAccount.email);
if (adminAccount.ip != null) {
holder.binding.lastActive.setText(Helper.shortDateToString(adminAccount.ip.used_at));
holder.binding.ip.setText(adminAccount.ip.ip);
} else if (adminAccount.ips != null && adminAccount.ips.size() > 0) {
holder.binding.lastActive.setText(Helper.shortDateToString(adminAccount.ips.get(0).used_at));
holder.binding.ip.setText(adminAccount.ips.get(0).ip);
} else {
holder.binding.lastActive.setText(Helper.shortDateToString(adminAccount.created_at));
}
}
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return adminAccountList.size();
}
public static class AccountAdminViewHolder extends RecyclerView.ViewHolder {
DrawerAdminAccountBinding binding;
AccountAdminViewHolder(DrawerAdminAccountBinding itemView) {
super(itemView.getRoot());
binding = itemView;
}
}
}

View File

@ -0,0 +1,216 @@
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 <http://www.gnu.org/licenses>. */
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.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.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.viewmodel.mastodon.AdminVM;
public class FragmentAdminAccount extends Fragment {
String byDomain, username, displayName, email, ip;
private FragmentPaginationBinding binding;
private AdminVM adminVM;
private boolean flagLoading;
private List<AdminAccount> adminAccounts;
private String max_id;
private AdminAccountAdapter adminAccountAdapter;
private String viewModelKey;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
if (getArguments() != null) {
viewModelKey = getArguments().getString(Helper.ARG_VIEW_MODEL_KEY, "");
}
flagLoading = false;
binding = FragmentPaginationBinding.inflate(inflater, container, false);
binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity()));
return binding.getRoot();
}
private void fetchAccount(Callback callback) {
adminVM.getAccounts(
BaseMainActivity.currentInstance, BaseMainActivity.currentToken,
AdminActionActivity.local,
AdminActionActivity.remote,
byDomain,
AdminActionActivity.active,
AdminActionActivity.pending,
AdminActionActivity.disabled,
AdminActionActivity.silenced,
AdminActionActivity.suspended,
username, displayName, email, ip,
AdminActionActivity.staff, max_id, null,
MastodonHelper.statusesPerCall(requireActivity()))
.observe(requireActivity(), callback::accountFetched);
}
@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);
adminVM = new ViewModelProvider(FragmentAdminAccount.this).get(viewModelKey, AdminVM.class);
max_id = null;
fetchAccount(this::initializeAccountCommonView);
}
public void scrollToTop() {
binding.recyclerView.setAdapter(adminAccountAdapter);
}
/**
* Intialize the view for accounts
*
* @param adminAccounts {@link AdminAccounts}
*/
private void initializeAccountCommonView(final AdminAccounts adminAccounts) {
if (binding == 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;
fetchAccount(this::initializeAccountCommonView);
});
if (adminAccounts == null || adminAccounts.adminAccounts == null || adminAccounts.adminAccounts.size() == 0) {
binding.noAction.setVisibility(View.VISIBLE);
binding.noActionText.setText(R.string.no_accounts);
return;
}
binding.recyclerView.setVisibility(View.VISIBLE);
if (adminAccountAdapter != null && this.adminAccounts != null) {
int size = this.adminAccounts.size();
this.adminAccounts.clear();
this.adminAccounts = new ArrayList<>();
adminAccountAdapter.notifyItemRangeRemoved(0, size);
}
this.adminAccounts = adminAccounts.adminAccounts;
adminAccountAdapter = new AdminAccountAdapter(this.adminAccounts);
flagLoading = (adminAccounts.adminAccounts.size() < MastodonHelper.accountsPerCall(requireActivity()));
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(adminAccountAdapter);
//Fetch the relationship
if (max_id == null || (adminAccounts.pagination.max_id != null && adminAccounts.pagination.max_id.compareTo(max_id) < 0)) {
max_id = adminAccounts.pagination.max_id;
}
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);
fetchAccount(adminAccounts1 -> dealWithPagination(adminAccounts1));
}
} else {
binding.loadingNextElements.setVisibility(View.GONE);
}
}
}
});
}
/**
* Update view and pagination when scrolling down
*
* @param adminAccounts AdminAccounts
*/
private void dealWithPagination(AdminAccounts adminAccounts) {
flagLoading = false;
if (binding == null) {
return;
}
binding.loadingNextElements.setVisibility(View.GONE);
if (this.adminAccounts != null && adminAccounts != null && adminAccounts.adminAccounts != null) {
flagLoading = (adminAccounts.adminAccounts.size() < MastodonHelper.accountsPerCall(requireActivity()));
int startId = 0;
//There are some statuses present in the timeline
if (this.adminAccounts.size() > 0) {
startId = this.adminAccounts.size();
}
int position = this.adminAccounts.size();
this.adminAccounts.addAll(adminAccounts.adminAccounts);
if (max_id == null || (adminAccounts.pagination.max_id != null && adminAccounts.pagination.max_id.compareTo(max_id) < 0)) {
max_id = adminAccounts.pagination.max_id;
}
adminAccountAdapter.notifyItemRangeInserted(startId, adminAccounts.adminAccounts.size());
} else {
flagLoading = true;
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (binding != null) {
binding.recyclerView.setAdapter(null);
}
adminAccountAdapter = null;
binding = null;
}
interface Callback {
void accountFetched(AdminAccounts adminAccounts);
}
}

View File

@ -0,0 +1,227 @@
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 <http://www.gnu.org/licenses>. */
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.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.AdminReport;
import app.fedilab.android.client.entities.api.AdminReports;
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.StatusAdapter;
import app.fedilab.android.viewmodel.mastodon.AdminVM;
public class FragmentAdminReport extends Fragment {
private FragmentPaginationBinding binding;
private AdminVM adminVM;
private boolean flagLoading;
private List<AdminReport> adminReports;
private String max_id, min_id;
private StatusAdapter statusAdapter;
private LinearLayoutManager mLayoutManager;
private String viewModelKey;
public void scrollToTop() {
if (binding != null) {
binding.recyclerView.scrollToPosition(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(FragmentAdminReport.this).get(viewModelKey, AdminVM.class);
binding.loader.setVisibility(View.VISIBLE);
binding.recyclerView.setVisibility(View.GONE);
flagLoading = false;
adminVM.getReports(
BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, null)
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
/**
* Intialize the common view for statuses on different timelines
*
* @param adminReports {@link AdminReports}
*/
private void initializeStatusesCommonView(final AdminReports adminReports) {
if (binding == 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.getReports(
BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, null)
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
});
if (adminReports == null || adminReports.adminReports == null || adminReports.adminReports.size() == 0) {
binding.noAction.setVisibility(View.VISIBLE);
return;
}
flagLoading = (adminReports.adminReports.size() < MastodonHelper.statusesPerCall(requireActivity()));
binding.recyclerView.setVisibility(View.VISIBLE);
if (statusAdapter != null && this.adminReports != null) {
int size = this.adminReports.size();
this.adminReports.clear();
this.adminReports = new ArrayList<>();
statusAdapter.notifyItemRangeRemoved(0, size);
}
if (this.adminReports == null) {
this.adminReports = new ArrayList<>();
}
this.adminReports.addAll(adminReports.adminReports);
if (max_id == null || (adminReports.pagination.max_id != null && adminReports.pagination.max_id.compareTo(max_id) < 0)) {
max_id = adminReports.pagination.max_id;
}
if (min_id == null || (adminReports.pagination.max_id != null && adminReports.pagination.min_id.compareTo(min_id) > 0)) {
min_id = adminReports.pagination.min_id;
}
// statusAdapter = new StatusAdapter(this.statuses, timelineType, minified);
mLayoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(statusAdapter);
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.getReports(
BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, max_id)
.observe(getViewLifecycleOwner(), adminReports1 -> dealWithPagination(adminReports1));
}
} else {
binding.loadingNextElements.setVisibility(View.GONE);
}
}
}
});
}
/**
* Update view and pagination when scrolling down
*
* @param admReports AdminReports
*/
private void dealWithPagination(AdminReports admReports) {
flagLoading = false;
if (binding == null) {
return;
}
binding.loadingNextElements.setVisibility(View.GONE);
if (adminReports != null && admReports != null && admReports.adminReports != null && admReports.adminReports.size() > 0) {
flagLoading = (admReports.adminReports.size() < MastodonHelper.statusesPerCall(requireActivity()));
//There are some adminReports present in the timeline
int startId = adminReports.size();
adminReports.addAll(admReports.adminReports);
statusAdapter.notifyItemRangeInserted(startId, admReports.adminReports.size());
if (max_id == null || (admReports.pagination.max_id != null && admReports.pagination.max_id.compareTo(max_id) < 0)) {
max_id = admReports.pagination.max_id;
}
if (min_id == null || (admReports.pagination.min_id != null && admReports.pagination.min_id.compareTo(min_id) > 0)) {
min_id = admReports.pagination.min_id;
}
}
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroyView() {
if (binding != null) {
binding.recyclerView.setAdapter(null);
}
statusAdapter = null;
binding = null;
super.onDestroyView();
}
/**
* Refresh status in list
*/
public void refreshAllAdapters() {
if (statusAdapter != null && adminReports != null) {
statusAdapter.notifyItemRangeChanged(0, adminReports.size());
}
}
}

View File

@ -1,63 +0,0 @@
package app.fedilab.android.ui.fragment.settings;
/* 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 <http://www.gnu.org/licenses>. */
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import app.fedilab.android.R;
public class FragmentAdministrationSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.pref_administration);
createPref();
}
private void createPref() {
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (getActivity() != null) {
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.apply();
}
}
@Override
public void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
}
}

View File

@ -16,7 +16,6 @@ package app.fedilab.android.ui.fragment.timeline;
import static app.fedilab.android.activities.ContextActivity.displayCW;
import static app.fedilab.android.activities.ContextActivity.expand;
import static app.fedilab.android.helper.RecyclerViewThreadLinesKt.getThreadDecorationInfo;
import android.content.BroadcastReceiver;
import android.content.Intent;
@ -44,9 +43,8 @@ import app.fedilab.android.client.entities.api.Context;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.databinding.FragmentPaginationBinding;
import app.fedilab.android.helper.DividerDecoration;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.RecyclerViewThreadLines;
import app.fedilab.android.helper.RecyclerViewThreadLines.LineInfo;
import app.fedilab.android.helper.SpannableHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.StatusAdapter;
@ -60,7 +58,6 @@ public class FragmentMastodonContext extends Fragment {
private StatusesVM statusesVM;
private List<Status> statuses;
private StatusAdapter statusAdapter;
private RecyclerViewThreadLines recyclerViewThreadLines;
//Handle actions that can be done in other fragments
private final BroadcastReceiver receive_action = new BroadcastReceiver() {
@Override
@ -252,9 +249,7 @@ public class FragmentMastodonContext extends Fragment {
binding.recyclerView.removeItemDecorationAt(i);
}
}
List<LineInfo> threadDecorationInfo = getThreadDecorationInfo(context);
recyclerViewThreadLines = new RecyclerViewThreadLines(requireContext(), threadDecorationInfo);
binding.recyclerView.addItemDecoration(recyclerViewThreadLines);
binding.recyclerView.addItemDecoration(new DividerDecoration(requireActivity(), statuses));
binding.swipeContainer.setRefreshing(false);
binding.recyclerView.scrollToPosition(statusPosition);
}
@ -262,7 +257,6 @@ public class FragmentMastodonContext extends Fragment {
@Override
public void onDestroyView() {
binding.recyclerView.setAdapter(null);
binding.recyclerView.removeItemDecoration(recyclerViewThreadLines);
statusAdapter = null;
binding = null;
LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(receive_action);

View File

@ -29,8 +29,11 @@ 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.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Response;
@ -46,9 +49,9 @@ public class AdminVM extends AndroidViewModel {
.proxy(Helper.getProxy(getApplication().getApplicationContext()))
.build();
private MutableLiveData<AdminAccount> adminAccountMutableLiveData;
private MutableLiveData<List<AdminAccount>> adminAccountListMutableLiveData;
private MutableLiveData<AdminAccounts> adminAccountsListMutableLiveData;
private MutableLiveData<AdminReport> adminReportMutableLiveData;
private MutableLiveData<List<AdminReport>> adminReportListMutableLiveData;
private MutableLiveData<AdminReports> adminReporstListMutableLiveData;
public AdminVM(@NonNull Application application) {
super(application);
@ -83,47 +86,48 @@ public class AdminVM extends AndroidViewModel {
* @param staff Filter for staff accounts?
* @return {@link LiveData} containing a {@link List} of {@link AdminAccount}s
*/
public LiveData<List<AdminAccount>> getAccounts(@NonNull String instance,
String token,
Boolean local,
Boolean remote,
String byDomain,
Boolean active,
Boolean pending,
Boolean disabled,
Boolean silenced,
Boolean suspended,
String username,
String displayName,
String email,
String ip,
Boolean staff,
String maxId,
String sinceId,
Integer limit) {
public LiveData<AdminAccounts> getAccounts(@NonNull String instance,
String token,
Boolean local,
Boolean remote,
String byDomain,
Boolean active,
Boolean pending,
Boolean disabled,
Boolean silenced,
Boolean suspended,
String username,
String displayName,
String email,
String ip,
Boolean staff,
String maxId,
String sinceId,
Integer limit) {
MastodonAdminService mastodonAdminService = init(instance);
adminAccountListMutableLiveData = new MutableLiveData<>();
adminAccountsListMutableLiveData = new MutableLiveData<>();
new Thread(() -> {
List<AdminAccount> adminAccountList = null;
Call<List<AdminAccount>> getAccountsCall = mastodonAdminService.getAccounts(
token, local, remote, byDomain, active, pending, disabled, silenced, suspended,
username, displayName, email, ip, staff, maxId, sinceId, limit);
AdminAccounts adminAccounts = new AdminAccounts();
if (getAccountsCall != null) {
try {
Response<List<AdminAccount>> getAccountsResponse = getAccountsCall.execute();
if (getAccountsResponse.isSuccessful()) {
adminAccountList = getAccountsResponse.body();
adminAccounts.adminAccounts = getAccountsResponse.body();
adminAccounts.pagination = MastodonHelper.getPagination(getAccountsResponse.headers());
}
} catch (IOException e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
List<AdminAccount> finalAdminAccountList = adminAccountList;
Runnable myRunnable = () -> adminAccountListMutableLiveData.setValue(finalAdminAccountList);
Runnable myRunnable = () -> adminAccountsListMutableLiveData.setValue(adminAccounts);
mainHandler.post(myRunnable);
}).start();
return adminAccountListMutableLiveData;
return adminAccountsListMutableLiveData;
}
/**
@ -358,32 +362,35 @@ public class AdminVM extends AndroidViewModel {
* @param token Access token of the active account
* @return {@link LiveData} containing a {@link List} of {@link AdminReport}s
*/
public LiveData<List<AdminReport>> getReports(@NonNull String instance,
String token,
Boolean resolved,
String accountId,
String targetAccountId) {
public LiveData<AdminReports> getReports(@NonNull String instance,
String token,
Boolean resolved,
String accountId,
String targetAccountId,
String max_id) {
MastodonAdminService mastodonAdminService = init(instance);
adminReportListMutableLiveData = new MutableLiveData<>();
adminReporstListMutableLiveData = new MutableLiveData<>();
new Thread(() -> {
List<AdminReport> adminReportList = null;
Call<List<AdminReport>> getReportsCall = mastodonAdminService.getReports(token, resolved, accountId, targetAccountId);
List<AdminReport> adminReportList;
Call<List<AdminReport>> getReportsCall = mastodonAdminService.getReports(token, resolved, accountId, targetAccountId, max_id, MastodonHelper.statusesPerCall(getApplication()));
AdminReports adminReports = new AdminReports();
if (getReportsCall != null) {
try {
Response<List<AdminReport>> getReportsResponse = getReportsCall.execute();
if (getReportsResponse.isSuccessful()) {
adminReportList = getReportsResponse.body();
adminReports.adminReports = adminReportList;
adminReports.pagination = MastodonHelper.getPagination(getReportsResponse.headers());
}
} catch (IOException e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
List<AdminReport> finalAdminReportList = adminReportList;
Runnable myRunnable = () -> adminReportListMutableLiveData.setValue(finalAdminReportList);
Runnable myRunnable = () -> adminReporstListMutableLiveData.setValue(adminReports);
mainHandler.post(myRunnable);
}).start();
return adminReportListMutableLiveData;
return adminReporstListMutableLiveData;
}
/**

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,11c0.34,0 0.67,0.04 1,0.09V6.27L10.5,3L3,6.27v4.91c0,4.54 3.2,8.79 7.5,9.82c0.55,-0.13 1.08,-0.32 1.6,-0.55C11.41,19.47 11,18.28 11,17C11,13.69 13.69,11 17,11z" />
<path
android:fillColor="@android:color/white"
android:pathData="M17,13c-2.21,0 -4,1.79 -4,4c0,2.21 1.79,4 4,4s4,-1.79 4,-4C21,14.79 19.21,13 17,13zM17,14.38c0.62,0 1.12,0.51 1.12,1.12s-0.51,1.12 -1.12,1.12s-1.12,-0.51 -1.12,-1.12S16.38,14.38 17,14.38zM17,19.75c-0.93,0 -1.74,-0.46 -2.24,-1.17c0.05,-0.72 1.51,-1.08 2.24,-1.08s2.19,0.36 2.24,1.08C18.74,19.29 17.93,19.75 17,19.75z" />
</vector>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/button_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/reports"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:text="@string/reports"
android:textAlignment="textStart"
android:textColor="@color/cyanea_accent_dark_reference"
app:icon="@drawable/ic_baseline_navigate_next_24"
app:iconGravity="end"
app:iconTint="@color/cyanea_accent_dark_reference"
app:strokeColor="@color/cyanea_accent_dark_reference" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accounts"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:paddingVertical="12dp"
android:text="@string/accounts"
android:textAlignment="textStart"
android:textColor="@color/cyanea_accent_dark_reference"
app:icon="@drawable/ic_baseline_navigate_next_24"
app:iconGravity="end"
app:iconTint="@color/cyanea_accent_dark_reference"
app:strokeColor="@color/cyanea_accent_dark_reference" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="6dp"
android:padding="6dp">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="70dp"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/pp"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:layout_weight="2"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:lines="1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/acct"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:lines="1" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/post_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/background_status_title" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/followers_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/followers" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/last_active"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/last_active" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:layout_weight="2"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/ip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/pp"
android:layout_width="50dp"
android:layout_height="50dp" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/acc"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/indications"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/reports"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/pp"
android:layout_width="50dp"
android:layout_height="50dp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/acct"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/report_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/messages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/pictures"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/location"
android:textSize="16sp" />
<RadioGroup
android:id="@+id/location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/location_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/all" />
<RadioButton
android:id="@+id/location_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/local" />
<RadioButton
android:id="@+id/location_remote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/remote" />
</RadioGroup>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/location"
android:textSize="16sp" />
<RadioGroup
android:id="@+id/moderation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/moderation_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/all" />
<RadioButton
android:id="@+id/moderation_active"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/active" />
<RadioButton
android:id="@+id/moderation_suspended"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/suspended" />
<RadioButton
android:id="@+id/moderation_pending"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pending" />
</RadioGroup>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/permissions"
android:textSize="16sp" />
<RadioGroup
android:id="@+id/permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/permissions_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/all" />
<RadioButton
android:id="@+id/permissions_staff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/staff" />
</RadioGroup>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/order_by"
android:textSize="16sp" />
<RadioGroup
android:id="@+id/order_by"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/order_by_most_recent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/most_recent" />
<RadioButton
android:id="@+id/order_by_last_active"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/last_active" />
</RadioGroup>
</LinearLayout>

View File

@ -37,6 +37,11 @@
android:icon="@drawable/ic_baseline_group_add_24"
android:title="@string/follow_request"
android:visible="false" />
<item
android:id="@+id/nav_administration"
android:icon="@drawable/ic_baseline_admin_panel_settings_24"
android:title="@string/administration"
android:visible="false" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/ic_baseline_settings_24"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_filter"
android:icon="@drawable/ic_baseline_filter_list_24"
android:title="@string/filters"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1602,6 +1602,11 @@
<string name="also_favourite_by">"Also favourited by: "</string>
<string name="also_boosted_by">Also boosted by:</string>
<string name="admin_scope">I am a moderator</string>
<string name="last_active">Last active</string>
<string name="location">Location</string>
<string name="staff">Staff</string>
<string name="most_recent">Most recent</string>
<string name="filter">Filter</string>
</resources>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:key="app_prefs">
</PreferenceScreen>