added report rule selector, added report dialog to profile

This commit is contained in:
nuclearfog 2023-10-10 22:22:09 +02:00
parent 56f4de4eda
commit d2728716b8
No known key found for this signature in database
GPG Key ID: 03488A185C476379
15 changed files with 425 additions and 46 deletions

View File

@ -30,6 +30,7 @@ import org.nuclearfog.twidda.model.lists.Domains;
import org.nuclearfog.twidda.model.lists.Filters;
import org.nuclearfog.twidda.model.lists.Hashtags;
import org.nuclearfog.twidda.model.lists.Notifications;
import org.nuclearfog.twidda.model.lists.Rules;
import org.nuclearfog.twidda.model.lists.ScheduledStatuses;
import org.nuclearfog.twidda.model.lists.Statuses;
import org.nuclearfog.twidda.model.lists.UserLists;
@ -711,4 +712,9 @@ public interface Connection {
* @param update report contianing information about status/user
*/
void createReport(ReportUpdate update) throws ConnectionException;
/**
* get rules of an instance
*/
Rules getRules() throws ConnectionException;
}

View File

@ -25,6 +25,7 @@ import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonNotification;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonPoll;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonPush;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonRelation;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonRule;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonStatus;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonTranslation;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonUser;
@ -61,6 +62,7 @@ import org.nuclearfog.twidda.model.lists.Domains;
import org.nuclearfog.twidda.model.lists.Filters;
import org.nuclearfog.twidda.model.lists.Hashtags;
import org.nuclearfog.twidda.model.lists.Notifications;
import org.nuclearfog.twidda.model.lists.Rules;
import org.nuclearfog.twidda.model.lists.ScheduledStatuses;
import org.nuclearfog.twidda.model.lists.Statuses;
import org.nuclearfog.twidda.model.lists.UserLists;
@ -154,6 +156,7 @@ public class Mastodon implements Connection {
private static final String ENDPOINT_FILTER = "/api/v2/filters";
private static final String ENDPOINT_REPORT = "/api/v1/reports";
private static final String ENDPOINT_SCHEDULED_STATUS = "/api/v1/scheduled_statuses";
private static final String ENDPOINT_GET_RULES = "/api/v1/instance/rules";
private static final MediaType TYPE_TEXT = MediaType.parse("text/plain");
private static final MediaType TYPE_STREAM = MediaType.parse("application/octet-stream");
@ -1340,7 +1343,7 @@ public class Mastodon implements Connection {
params.add("account_id=" + update.getUserId());
for (long statusId : update.getStatusIds())
params.add("status_ids[]=" + statusId);
for (int ruleId : update.getRuleIds())
for (long ruleId : update.getRuleIds())
params.add("rule_ids[]=" + ruleId);
if (!update.getComment().trim().isEmpty())
params.add("comment=" + StringUtils.encode(update.getComment()));
@ -1361,6 +1364,32 @@ public class Mastodon implements Connection {
}
}
@Override
public Rules getRules() throws ConnectionException {
try {
Response response = get(ENDPOINT_GET_RULES, new ArrayList<>());
ResponseBody body = response.body();
if (response.code() == 200 && body != null) {
JSONArray jsonArray = new JSONArray(body.string());
Rules rules = new Rules(jsonArray.length());
for (int i = 0; i < jsonArray.length(); i++) {
try {
rules.add(new MastodonRule(jsonArray.getJSONObject(i)));
} catch (JSONException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
}
return rules;
}
throw new MastodonException(response);
} catch (IOException | JSONException e) {
throw new MastodonException(e);
}
}
/**
* get information about the current user
*

View File

@ -0,0 +1,61 @@
package org.nuclearfog.twidda.backend.api.mastodon.impl;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import org.nuclearfog.twidda.model.Rule;
/**
* Mastodon implementation of a {@link Rule}
*
* @author nuclearfog
*/
public class MastodonRule implements Rule {
private static final long serialVersionUID = 735539108133555221L;
private long id;
private String description;
/**
*
*/
public MastodonRule(JSONObject json) throws JSONException {
String idStr = json.getString("id");
description = json.getString("text");
try {
id = Long.parseLong(idStr);
} catch (NumberFormatException exception) {
throw new JSONException("bad ID: " + idStr);
}
}
@Override
public long getId() {
return id;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Rule))
return false;
return ((Rule) obj).getId() == getId();
}
@NonNull
@Override
public String toString() {
return "id=" + getId() + " description=\"" + getDescription() + "\"";
}
}

View File

@ -0,0 +1,38 @@
package org.nuclearfog.twidda.backend.async;
import android.content.Context;
import androidx.annotation.NonNull;
import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.api.ConnectionManager;
import org.nuclearfog.twidda.model.lists.Rules;
/**
* Loader class used to load instance rules
*
* @author nuclearfog
* @see org.nuclearfog.twidda.ui.dialogs.ReportDialog
*/
public class RuleLoader extends AsyncExecutor<Void, Rules> {
private Connection connection;
/**
*
*/
public RuleLoader(Context context) {
connection = ConnectionManager.getDefaultConnection(context);
}
@Override
protected Rules doInBackground(@NonNull Void param) {
try {
return connection.getRules();
} catch (ConnectionException exception) {
return null;
}
}
}

View File

@ -20,7 +20,7 @@ public class ReportUpdate implements Serializable {
private long userId;
private long[] statusIds = {};
private int[] ruleIds = {};
private long[] ruleIds = {};
private String comment = "";
private int category = CATEGORY_OTHER;
private boolean forward = false;
@ -64,7 +64,7 @@ public class ReportUpdate implements Serializable {
*
* @param ruleIds array of rule IDs
*/
public void setRuleIds(int[] ruleIds) {
public void setRuleIds(long[] ruleIds) {
this.ruleIds = Arrays.copyOf(ruleIds, ruleIds.length);
}
@ -73,7 +73,7 @@ public class ReportUpdate implements Serializable {
*
* @return array of rule IDs
*/
public int[] getRuleIds() {
public long[] getRuleIds() {
return Arrays.copyOf(ruleIds, ruleIds.length);
}

View File

@ -0,0 +1,19 @@
package org.nuclearfog.twidda.model;
import java.io.Serializable;
/**
* Represents a rule of an {@link Instance}
*/
public interface Rule extends Serializable {
/**
* get ID of the rule
*/
long getId();
/**
* get detailed description of this rule
*/
String getDescription();
}

View File

@ -0,0 +1,26 @@
package org.nuclearfog.twidda.model.lists;
import org.nuclearfog.twidda.model.Rule;
import java.util.ArrayList;
/**
* @author nuclearfog
*/
public class Rules extends ArrayList<Rule> {
private static final long serialVersionUID = -8984532893479237315L;
/**
*
*/
public Rules() {
}
/**
*
*/
public Rules(int cap) {
super(cap);
}
}

View File

@ -54,6 +54,7 @@ import org.nuclearfog.twidda.model.User;
import org.nuclearfog.twidda.ui.adapter.viewpager.ProfileAdapter;
import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog;
import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog.OnConfirmListener;
import org.nuclearfog.twidda.ui.dialogs.ReportDialog;
import org.nuclearfog.twidda.ui.views.LockableConstraintLayout;
import org.nuclearfog.twidda.ui.views.TabSelector;
import org.nuclearfog.twidda.ui.views.TabSelector.OnTabSelectedListener;
@ -118,6 +119,7 @@ public class ProfileActivity extends AppCompatActivity implements OnClickListene
private GlobalSettings settings;
private Picasso picasso;
private ConfirmDialog confirmDialog;
private ReportDialog reportDialog;
private DomainAction domainAction;
private RelationLoader relationLoader;
@ -173,6 +175,7 @@ public class ProfileActivity extends AppCompatActivity implements OnClickListene
userLoader = new UserLoader(this);
emojiLoader = new TextEmojiLoader(this);
confirmDialog = new ConfirmDialog(this, this);
reportDialog = new ReportDialog(this);
picasso = PicassoBuilder.get(this);
settings = GlobalSettings.get(this);
adapter = new ProfileAdapter(this);
@ -289,15 +292,14 @@ public class ProfileActivity extends AppCompatActivity implements OnClickListene
boolean result = super.onPrepareOptionsMenu(m);
if (user != null) {
MenuItem listItem = m.findItem(R.id.profile_lists);
MenuItem domainBlock = m.findItem(R.id.profile_block_domain);
MenuItem domainItem = m.findItem(R.id.profile_block_domain);
MenuItem reportItem = m.findItem(R.id.profile_report);
switch (settings.getLogin().getConfiguration()) {
case MASTODON:
if (user.isCurrentUser()) {
listItem.setVisible(true);
} else {
domainBlock.setVisible(true);
}
listItem.setVisible(user.isCurrentUser());
domainItem.setVisible(!user.isCurrentUser());
reportItem .setVisible(!user.isCurrentUser());
break;
}
if (user.followRequested()) {
@ -408,6 +410,12 @@ public class ProfileActivity extends AppCompatActivity implements OnClickListene
confirmDialog.show(ConfirmDialog.DOMAIN_BLOCK_ADD);
}
}
// report user
else if (item.getItemId() == R.id.profile_report) {
if (user != null) {
reportDialog.show(user.getId());
}
}
return super.onOptionsItemSelected(item);
}

View File

@ -0,0 +1,126 @@
package org.nuclearfog.twidda.ui.adapter.listview;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.Rule;
import org.nuclearfog.twidda.model.lists.Rules;
import java.util.Set;
import java.util.TreeSet;
/**
* A {@link android.widget.ListView} adapter used to show instance rules and provide function to select rule items and their IDs
*
* @author nuclearfog
*/
public class RuleAdapter extends BaseAdapter {
private GlobalSettings settings;
private Rules items = new Rules();
private Set<Long> selection = new TreeSet<>();
/**
*
*/
public RuleAdapter(Context context) {
settings = GlobalSettings.get(context);
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return items.get(position).getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView description;
ImageView button;
final Rule item = items.get(position);
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_rule, parent, false);
description = convertView.findViewById(R.id.item_rule_description);
button = convertView.findViewById(R.id.item_rule_select);
button.setColorFilter(settings.getIconColor());
description.setTextColor(settings.getTextColor());
description.setTypeface(settings.getTypeFace());
} else {
description = convertView.findViewById(R.id.item_rule_description);
button = convertView.findViewById(R.id.item_rule_select);
}
description.setText(item.getDescription());
if (selection.contains(item.getId())) {
button.setImageResource(R.drawable.check);
} else {
button.setImageResource(R.drawable.circle);
}
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (selection.contains(item.getId())) {
button.setImageResource(R.drawable.circle);
selection.remove(item.getId());
} else {
button.setImageResource(R.drawable.check);
selection.add(item.getId());
}
}
});
return convertView;
}
/**
* set adapter items
*
* @param rules adapter items to set
*/
public void setItems(Rules rules) {
items.clear();
items.addAll(rules);
notifyDataSetChanged();
}
/**
* get user selected item IDs
*
* @return an array containing selected item IDs
*/
public long[] getSelectedIds() {
int i = 0;
long[] result = new long[selection.size()];
for (Long select : selection) {
result[i++] = select;
}
return result;
}
/**
* @return true if adapter doesn't contain any items
*/
public boolean isEmpty() {
return items.isEmpty();
}
}

View File

@ -7,6 +7,7 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
@ -18,11 +19,14 @@ import com.kyleduo.switchbutton.SwitchButton;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
import org.nuclearfog.twidda.backend.async.ReportUpdater;
import org.nuclearfog.twidda.backend.async.RuleLoader;
import org.nuclearfog.twidda.backend.helper.update.ReportUpdate;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.ErrorUtils;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.lists.Rules;
import org.nuclearfog.twidda.ui.adapter.listview.DropdownAdapter;
import org.nuclearfog.twidda.ui.adapter.listview.RuleAdapter;
import java.io.Serializable;
@ -31,11 +35,16 @@ import java.io.Serializable;
*
* @author nuclearfog
*/
public class ReportDialog extends Dialog implements OnClickListener, AsyncCallback<ReportUpdater.Result> {
public class ReportDialog extends Dialog implements OnClickListener {
private static final String KEY_SAVE = "reportupdate-data";
private DropdownAdapter adapter;
private AsyncCallback<ReportUpdater.Result> reportResult = this::onReportResult;
private AsyncCallback<Rules> rulesResult = this::onRulesLoaded;
private DropdownAdapter selectorAdapter;
private RuleAdapter ruleAdapter;
private RuleLoader ruleLoader;
private ReportUpdater reportUpdater;
private GlobalSettings settings;
@ -51,10 +60,12 @@ public class ReportDialog extends Dialog implements OnClickListener, AsyncCallba
*/
public ReportDialog(Activity activity) {
super(activity, R.style.DefaultDialog);
adapter = new DropdownAdapter(activity.getApplicationContext());
selectorAdapter = new DropdownAdapter(activity.getApplicationContext());
reportUpdater = new ReportUpdater(activity.getApplicationContext());
ruleLoader = new RuleLoader(activity.getApplicationContext());
ruleAdapter = new RuleAdapter(activity.getApplicationContext());
settings = GlobalSettings.get(getContext());
adapter.setItems(R.array.reports);
selectorAdapter.setItems(R.array.reports);
}
@ -64,18 +75,28 @@ public class ReportDialog extends Dialog implements OnClickListener, AsyncCallba
setContentView(R.layout.dialog_report);
ViewGroup rootView = findViewById(R.id.dialog_report_root);
View reportButton = findViewById(R.id.dialog_report_apply);
ListView ruleSelector = findViewById(R.id.dialog_report_rule_selector);
reportCategory = findViewById(R.id.dialog_report_category);
textTitle = findViewById(R.id.dialog_report_title);
switchForward = findViewById(R.id.dialog_report_switch_forward);
editDescription = findViewById(R.id.dialog_report_description);
AppStyles.setTheme(rootView, settings.getPopupColor());
reportCategory.setAdapter(adapter);
reportCategory.setAdapter(selectorAdapter);
ruleSelector.setAdapter(ruleAdapter);
reportButton.setOnClickListener(this);
}
@Override
protected void onStart() {
super.onStart();
if (ruleAdapter.isEmpty() && ruleLoader.isIdle()) {
ruleLoader.execute(null, rulesResult);
}
}
@NonNull
@Override
public Bundle onSaveInstanceState() {
@ -120,16 +141,37 @@ public class ReportDialog extends Dialog implements OnClickListener, AsyncCallba
} else {
update.setCategory(ReportUpdate.CATEGORY_OTHER);
}
update.setRuleIds(ruleAdapter.getSelectedIds());
update.setComment(editDescription.getText().toString());
update.setForward(switchForward.isChecked());
reportUpdater.execute(update, this);
reportUpdater.execute(update, reportResult);
}
}
}
/**
* show this dialog
*
* @param userId Id of the user to report to instance
* @param statusId additional status IDs
*/
public void show(long userId, long... statusId) {
if (!isShowing()) {
super.show();
update = new ReportUpdate(userId);
if (statusId.length > 0) {
update.setStatusIds(statusId);
textTitle.setText(R.string.dialog_report_title_status);
} else {
textTitle.setText(R.string.dialog_report_title_user);
}
}
}
@Override
public void onResult(@NonNull ReportUpdater.Result result) {
/**
* callback for {@link ReportUpdater}
*/
private void onReportResult(@NonNull ReportUpdater.Result result) {
if (result.reported) {
if (update != null && update.getStatusIds().length > 0) {
Toast.makeText(getContext(), R.string.info_status_reported, Toast.LENGTH_SHORT).show();
@ -142,18 +184,9 @@ public class ReportDialog extends Dialog implements OnClickListener, AsyncCallba
}
/**
*
* callback for {@link RuleLoader}
*/
public void show(long userId, long statusId) {
if (!isShowing()) {
super.show();
update = new ReportUpdate(userId);
if (statusId != 0L) {
update.setStatusIds(new long[]{statusId});
textTitle.setText(R.string.dialog_report_title_status);
} else {
textTitle.setText(R.string.dialog_report_title_user);
}
}
private void onRulesLoaded(@NonNull Rules rules) {
ruleAdapter.setItems(rules);
}
}

View File

@ -39,11 +39,20 @@
app:layout_constraintTop_toBottomOf="@id/dialog_report_title"
app:layout_constraintEnd_toEndOf="parent" />
<ListView
android:id="@+id/dialog_report_rule_selector"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dialog_report_category"
app:layout_constraintEnd_toEndOf="parent"/>
<EditText
android:id="@+id/dialog_report_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lines="5"
android:lines="3"
android:inputType="textMultiLine"
android:fadeScrollbars="false"
android:scrollbars="vertical"
@ -53,7 +62,7 @@
android:gravity="top"
android:layout_margin="@dimen/dialog_report_margin_items_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dialog_report_category"
app:layout_constraintTop_toBottomOf="@id/dialog_report_rule_selector"
app:layout_constraintEnd_toEndOf="parent"
android:importantForAutofill="no" />

View File

@ -10,18 +10,9 @@
android:layout_width="@dimen/item_option_icon_size"
android:layout_height="@dimen/item_option_icon_size"
android:contentDescription="@string/description_poll_vote_icon"
android:layout_marginEnd="@dimen/item_option_button_margin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/item_option_button_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/item_option_button_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="item_option_count_bar,item_option_name" />
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/item_option_name"
@ -30,9 +21,10 @@
android:textSize="@dimen/item_option_text_size"
android:maxLines="2"
android:ellipsize="end"
android:layout_marginStart="@dimen/item_option_layout_padding"
android:layout_marginEnd="@dimen/item_option_layout_padding"
app:layout_constraintHorizontal_weight="3"
app:layout_constraintStart_toEndOf="@id/item_option_button_barrier"
app:layout_constraintStart_toEndOf="@id/item_option_voted_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/item_option_count_text" />
@ -41,8 +33,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:thumb="@android:color/transparent"
android:layout_marginStart="@dimen/item_option_layout_padding"
app:layout_constraintHorizontal_weight="3"
app:layout_constraintStart_toEndOf="@id/item_option_button_barrier"
app:layout_constraintStart_toEndOf="@id/item_option_voted_icon"
app:layout_constraintTop_toBottomOf="@id/item_option_name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_rule_select"
android:layout_width="@dimen/item_rule_icon_size"
android:layout_height="@dimen/item_option_icon_size"
android:contentDescription="@string/description_poll_vote_icon"
android:layout_margin="@dimen/item_rule_margin_layout" />
<TextView
android:id="@+id/item_rule_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="@dimen/item_rule_margin_layout"
android:maxLines="3" />
</LinearLayout>

View File

@ -28,6 +28,11 @@
android:title="@string/menu_mute_user"
android:visible="false" />
<item
android:id="@+id/profile_report"
android:title="@string/menu_status_report"
android:visible="false" />
<item
android:id="@+id/profile_lists"
android:visible="false"

View File

@ -256,11 +256,14 @@
<!-- dimens of item_icon.xml -->
<dimen name="item_icon_indicator_margin">2dp</dimen>
<!--dimens of item_rule.xml-->
<dimen name="item_rule_icon_size">20sp</dimen>
<dimen name="item_rule_margin_layout">5dp</dimen>
<!-- dimens of item_option.xml -->
<dimen name="item_option_icon_size">20sp</dimen>
<dimen name="item_option_text_size">13sp</dimen>
<dimen name="item_option_layout_padding">5dp</dimen>
<dimen name="item_option_button_margin">10dp</dimen>
<dimen name="item_option_emoji_size">13sp</dimen>
<!--dimens of item_option_edit.xml -->