mirror of
https://github.com/nuclearfog/Shitter.git
synced 2025-01-31 11:25:03 +01:00
finalized instance implementation
This commit is contained in:
parent
5d68a6387e
commit
8bfd045f0a
@ -1,5 +1,8 @@
|
||||
package org.nuclearfog.twidda.backend.api.mastodon.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@ -50,7 +53,7 @@ public class MastodonInstance implements Instance {
|
||||
version = json.getString("version");
|
||||
maxHashtagFeature = accounts.getInt("max_featured_tags");
|
||||
maxCharacters = statuses.getInt("max_characters");
|
||||
maxImages = media.getInt("max_media_attachments");
|
||||
maxImages = statuses.getInt("max_media_attachments");
|
||||
maxImageSize = media.getInt("image_size_limit");
|
||||
maxVideoSize = media.getInt("video_size_limit");
|
||||
maxPollOptions = polls.getInt("max_options");
|
||||
@ -192,4 +195,22 @@ public class MastodonInstance implements Instance {
|
||||
public boolean isTranslationSupported() {
|
||||
return translationSupported;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof MastodonInstance))
|
||||
return false;
|
||||
MastodonInstance instance = (MastodonInstance) obj;
|
||||
return instance.domain.equals(domain) && instance.timestamp == timestamp;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "domain=\"" + domain + " \" version=\"" + version + "\"";
|
||||
}
|
||||
}
|
@ -257,7 +257,7 @@ public class TwitterV1 implements Connection {
|
||||
|
||||
@Override
|
||||
public Instance getInformation() {
|
||||
return new TwitterV1Instance();
|
||||
return new TwitterV1Instance(settings.getLogin().getHostname());
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
package org.nuclearfog.twidda.backend.api.twitter.v1.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.nuclearfog.twidda.model.Instance;
|
||||
|
||||
/**
|
||||
@ -12,6 +15,15 @@ public class TwitterV1Instance implements Instance {
|
||||
|
||||
private static final long serialVersionUID = 6248302391974167770L;
|
||||
|
||||
private String hostname;
|
||||
|
||||
/**
|
||||
* @param hostname currently used hostname
|
||||
*/
|
||||
public TwitterV1Instance(String hostname) {
|
||||
this.hostname = hostname;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
@ -21,7 +33,7 @@ public class TwitterV1Instance implements Instance {
|
||||
|
||||
@Override
|
||||
public String getDomain() {
|
||||
return "https://twitter.com";
|
||||
return hostname;
|
||||
}
|
||||
|
||||
|
||||
@ -137,4 +149,22 @@ public class TwitterV1Instance implements Instance {
|
||||
public boolean isTranslationSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof TwitterV1Instance))
|
||||
return false;
|
||||
TwitterV1Instance instance = (TwitterV1Instance) obj;
|
||||
return instance.hostname.equals(hostname);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "domain=\"" + getDomain() + " \" version=\"2.0\"";
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.nuclearfog.twidda.BuildConfig;
|
||||
import org.nuclearfog.twidda.backend.api.ConnectionException;
|
||||
import org.nuclearfog.twidda.backend.api.twitter.TwitterException;
|
||||
import org.nuclearfog.twidda.backend.api.twitter.v1.TwitterV1;
|
||||
import org.nuclearfog.twidda.backend.api.twitter.v2.impl.AccountV2;
|
||||
@ -68,7 +67,7 @@ public class TwitterV2 extends TwitterV1 {
|
||||
|
||||
@Override
|
||||
public Instance getInformation() {
|
||||
return new TwitterV2Instance();
|
||||
return new TwitterV2Instance(settings.getLogin().getHostname());
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.nuclearfog.twidda.backend.api.twitter.v2.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.nuclearfog.twidda.backend.api.twitter.v1.impl.TwitterV1Instance;
|
||||
import org.nuclearfog.twidda.model.Instance;
|
||||
|
||||
/**
|
||||
* Twitter API v2.0 configuration
|
||||
@ -13,9 +15,34 @@ public class TwitterV2Instance extends TwitterV1Instance {
|
||||
|
||||
private static final long serialVersionUID = 5979539035652732059L;
|
||||
|
||||
/**
|
||||
* @param hostname currently used hostname
|
||||
*/
|
||||
public TwitterV2Instance(String hostname) {
|
||||
super(hostname);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "2.0";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof TwitterV2Instance))
|
||||
return false;
|
||||
TwitterV2Instance instance = (TwitterV2Instance) obj;
|
||||
return instance.getDomain().equals(getDomain());
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "domain=\"" + getDomain() + " \" version=\"2.0\"";
|
||||
}
|
||||
}
|
@ -14,13 +14,13 @@ import org.nuclearfog.twidda.model.Instance;
|
||||
*
|
||||
* @author nuclearfog
|
||||
*/
|
||||
public class InstanceLoader extends AsyncExecutor<InstanceLoader.InstanceLoaderParam, Instance> {
|
||||
public class InstanceLoader extends AsyncExecutor<Void, Instance> {
|
||||
|
||||
/**
|
||||
* time difference to update instance information
|
||||
* if database instance is older than this time, an update will be triggered
|
||||
*/
|
||||
private static final long MAX_TIME_DIFF = 1000 * 60 * 60 * 24 * 2;
|
||||
private static final long MAX_TIME_DIFF = 172800000L;
|
||||
|
||||
private Connection connection;
|
||||
private AppDatabase db;
|
||||
@ -35,40 +35,17 @@ public class InstanceLoader extends AsyncExecutor<InstanceLoader.InstanceLoaderP
|
||||
|
||||
|
||||
@Override
|
||||
protected Instance doInBackground(@NonNull InstanceLoaderParam param) {
|
||||
protected Instance doInBackground(@NonNull Void param) {
|
||||
Instance instance = null;
|
||||
try {
|
||||
switch (param.mode) {
|
||||
case InstanceLoaderParam.LOAD_DB:
|
||||
instance = db.getInstance(param.domain);
|
||||
if (instance != null && (System.currentTimeMillis() - instance.getTimestamp()) < MAX_TIME_DIFF)
|
||||
break;
|
||||
// fall through
|
||||
|
||||
case InstanceLoaderParam.LOAD_ONLINE:
|
||||
instance = connection.getInformation();
|
||||
break;
|
||||
instance = db.getInstance();
|
||||
if (instance == null || (System.currentTimeMillis() - instance.getTimestamp()) >= MAX_TIME_DIFF) {
|
||||
instance = connection.getInformation();
|
||||
db.saveInstance(instance);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static class InstanceLoaderParam {
|
||||
|
||||
public static final int LOAD_DB = 1;
|
||||
public static final int LOAD_ONLINE = 2;
|
||||
|
||||
final int mode;
|
||||
final String domain;
|
||||
|
||||
public InstanceLoaderParam(int mode, String domain) {
|
||||
this.domain = domain;
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,8 +9,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.nuclearfog.twidda.config.Configuration;
|
||||
import org.nuclearfog.twidda.config.GlobalSettings;
|
||||
import org.nuclearfog.twidda.model.Instance;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
@ -73,6 +72,8 @@ public class StatusUpdate {
|
||||
private PollUpdate poll;
|
||||
@Nullable
|
||||
private LocationUpdate location;
|
||||
@Nullable
|
||||
private Instance instance;
|
||||
|
||||
private int attachment = EMPTY;
|
||||
private List<Uri> mediaUris = new ArrayList<>(5);
|
||||
@ -107,8 +108,7 @@ public class StatusUpdate {
|
||||
*/
|
||||
public int addMedia(Context context, Uri mediaUri) {
|
||||
String mime = context.getContentResolver().getType(mediaUri);
|
||||
Configuration configuration = GlobalSettings.getInstance(context).getLogin().getConfiguration();
|
||||
if (mime == null) {
|
||||
if (mime == null || instance == null) {
|
||||
return MEDIA_ERROR;
|
||||
}
|
||||
// check if file is a 'gif' image
|
||||
@ -121,7 +121,7 @@ public class StatusUpdate {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri);
|
||||
if (file != null && file.length() > 0) {
|
||||
mediaUris.add(mediaUri);
|
||||
if (mediaUris.size() == configuration.getGifLimit()) {
|
||||
if (mediaUris.size() == instance.getGifLimit()) {
|
||||
attachmentLimitReached = true;
|
||||
}
|
||||
return MEDIA_GIF;
|
||||
@ -140,7 +140,7 @@ public class StatusUpdate {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri);
|
||||
if (file != null && file.length() > 0) {
|
||||
mediaUris.add(mediaUri);
|
||||
if (mediaUris.size() == configuration.getImageLimit()) {
|
||||
if (mediaUris.size() == instance.getImageLimit()) {
|
||||
attachmentLimitReached = true;
|
||||
}
|
||||
return MEDIA_IMAGE;
|
||||
@ -158,7 +158,7 @@ public class StatusUpdate {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri);
|
||||
if (file != null && file.length() > 0) {
|
||||
mediaUris.add(mediaUri);
|
||||
if (mediaUris.size() == configuration.getVideoLimit()) {
|
||||
if (mediaUris.size() == instance.getVideoLimit()) {
|
||||
attachmentLimitReached = true;
|
||||
}
|
||||
return MEDIA_VIDEO;
|
||||
@ -204,17 +204,28 @@ public class StatusUpdate {
|
||||
}
|
||||
|
||||
/**
|
||||
* set spoiler flag
|
||||
*/
|
||||
public void setSpoiler(boolean spoiler) {
|
||||
this.spoiler = spoiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* set sensitive flag
|
||||
*/
|
||||
public void setSensitive(boolean sensitive) {
|
||||
this.sensitive = sensitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* set instance imformation such as status limitations
|
||||
*
|
||||
* @param instance instance imformation
|
||||
*/
|
||||
public void setInstanceInformation(Instance instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* get ID of the replied status
|
||||
*
|
||||
|
@ -41,9 +41,6 @@ public enum Configuration {
|
||||
private final boolean statusSpoilerSupported;
|
||||
private final boolean statusVisibilitySupported;
|
||||
private final boolean directMessageSupported;
|
||||
private final int maxImages;
|
||||
private final int maxGifs;
|
||||
private final int maxVideos;
|
||||
|
||||
/**
|
||||
* @param accountType account login type, see {@link Account}
|
||||
@ -64,9 +61,6 @@ public enum Configuration {
|
||||
statusSpoilerSupported = false;
|
||||
statusVisibilitySupported = false;
|
||||
directMessageSupported = true;
|
||||
maxImages = 4;
|
||||
maxGifs = 1;
|
||||
maxVideos = 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -82,9 +76,6 @@ public enum Configuration {
|
||||
statusSpoilerSupported = true;
|
||||
statusVisibilitySupported = true;
|
||||
directMessageSupported = false;
|
||||
maxImages = 4;
|
||||
maxGifs = 1;
|
||||
maxVideos = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -172,25 +163,4 @@ public enum Configuration {
|
||||
public boolean directmessageSupported() {
|
||||
return directMessageSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return image limit for posts
|
||||
*/
|
||||
public int getImageLimit() {
|
||||
return maxImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return video limit for posts
|
||||
*/
|
||||
public int getVideoLimit() {
|
||||
return maxVideos;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return gif limit for posts
|
||||
*/
|
||||
public int getGifLimit() {
|
||||
return maxGifs;
|
||||
}
|
||||
}
|
@ -915,14 +915,13 @@ public class AppDatabase {
|
||||
/**
|
||||
* get a single instance of a domain
|
||||
*
|
||||
* @param domain domain name of the instance
|
||||
* @return instance or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
public Instance getInstance(String domain) {
|
||||
public Instance getInstance() {
|
||||
synchronized (LOCK) {
|
||||
SQLiteDatabase db = adapter.getDbRead();
|
||||
String[] args = {domain};
|
||||
String[] args = {settings.getLogin().getHostname()};
|
||||
Instance result = null;
|
||||
Cursor cursor = db.query(InstanceTable.NAME, DatabaseInstance.COLUMNS, INSTANCE_SELECTION, args, null, null, null);
|
||||
if (cursor.moveToFirst()) {
|
||||
|
@ -22,6 +22,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.nuclearfog.twidda.R;
|
||||
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
|
||||
import org.nuclearfog.twidda.backend.async.InstanceLoader;
|
||||
import org.nuclearfog.twidda.backend.async.StatusUpdater;
|
||||
import org.nuclearfog.twidda.backend.async.StatusUpdater.StatusUpdateResult;
|
||||
import org.nuclearfog.twidda.backend.helper.PollUpdate;
|
||||
@ -29,6 +30,7 @@ import org.nuclearfog.twidda.backend.helper.StatusUpdate;
|
||||
import org.nuclearfog.twidda.backend.utils.AppStyles;
|
||||
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
|
||||
import org.nuclearfog.twidda.config.GlobalSettings;
|
||||
import org.nuclearfog.twidda.model.Instance;
|
||||
import org.nuclearfog.twidda.ui.adapter.IconAdapter;
|
||||
import org.nuclearfog.twidda.ui.adapter.IconAdapter.OnMediaClickListener;
|
||||
import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog;
|
||||
@ -45,7 +47,7 @@ import org.nuclearfog.twidda.ui.dialogs.StatusPreferenceDialog;
|
||||
* @author nuclearfog
|
||||
*/
|
||||
public class StatusEditor extends MediaActivity implements OnClickListener, OnProgressStopListener, OnConfirmListener,
|
||||
OnMediaClickListener, AsyncCallback<StatusUpdateResult>, TextWatcher, PollUpdateCallback {
|
||||
OnMediaClickListener, TextWatcher, PollUpdateCallback {
|
||||
|
||||
/**
|
||||
* key to add a statusd ID to reply
|
||||
@ -59,14 +61,18 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
*/
|
||||
public static final String KEY_STATUS_EDITOR_TEXT = "status_text";
|
||||
|
||||
private AsyncCallback<StatusUpdateResult> statusUpdateResult = this::onStatusUpdated;
|
||||
private AsyncCallback<Instance> instanceResult = this::onInstanceResult;
|
||||
|
||||
private View mediaBtn;
|
||||
private View locationBtn;
|
||||
private View pollBtn;
|
||||
private View locationPending;
|
||||
|
||||
private StatusUpdater uploaderAsync;
|
||||
private GlobalSettings settings;
|
||||
private StatusUpdater statusUpdater;
|
||||
private InstanceLoader instanceLoader;
|
||||
|
||||
private GlobalSettings settings;
|
||||
private ConfirmDialog confirmDialog;
|
||||
private ProgressDialog loadingCircle;
|
||||
private PollDialog pollDialog;
|
||||
@ -98,7 +104,8 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
mediaBtn = findViewById(R.id.popup_status_add_media);
|
||||
locationPending = findViewById(R.id.popup_status_location_loading);
|
||||
|
||||
uploaderAsync = new StatusUpdater(this);
|
||||
instanceLoader = new InstanceLoader(this);
|
||||
statusUpdater = new StatusUpdater(this);
|
||||
settings = GlobalSettings.getInstance(this);
|
||||
loadingCircle = new ProgressDialog(this);
|
||||
confirmDialog = new ConfirmDialog(this);
|
||||
@ -120,6 +127,8 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
iconList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true));
|
||||
iconList.setAdapter(adapter);
|
||||
|
||||
instanceLoader.execute(null, instanceResult);
|
||||
|
||||
statusText.addTextChangedListener(this);
|
||||
closeButton.setOnClickListener(this);
|
||||
preference.setOnClickListener(this);
|
||||
@ -151,7 +160,8 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
loadingCircle.dismiss();
|
||||
uploaderAsync.cancel();
|
||||
statusUpdater.cancel();
|
||||
instanceLoader.cancel();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@ -175,7 +185,7 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
Toast.makeText(getApplicationContext(), R.string.info_location_pending, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
// check if gps locating is not pending
|
||||
else if (uploaderAsync.isIdle()) {
|
||||
else if (statusUpdater.isIdle()) {
|
||||
updateStatus();
|
||||
}
|
||||
}
|
||||
@ -223,6 +233,7 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
statusUpdate.addText(s.toString());
|
||||
// todo add character limit check
|
||||
}
|
||||
|
||||
|
||||
@ -272,7 +283,7 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
|
||||
@Override
|
||||
public void stopProgress() {
|
||||
uploaderAsync.cancel();
|
||||
statusUpdater.cancel();
|
||||
}
|
||||
|
||||
|
||||
@ -310,19 +321,6 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onResult(@NonNull StatusUpdateResult result) {
|
||||
if (result.success) {
|
||||
Toast.makeText(getApplicationContext(), R.string.info_status_sent, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
} else {
|
||||
String message = ErrorHandler.getErrorMessage(this, result.exception);
|
||||
confirmDialog.show(ConfirmDialog.STATUS_EDITOR_ERROR, message);
|
||||
loadingCircle.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPollUpdate(@Nullable PollUpdate update) {
|
||||
statusUpdate.addPoll(update);
|
||||
@ -339,6 +337,27 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called when the status was successfully updated
|
||||
*/
|
||||
private void onStatusUpdated(@NonNull StatusUpdateResult result) {
|
||||
if (result.success) {
|
||||
Toast.makeText(getApplicationContext(), R.string.info_status_sent, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
} else {
|
||||
String message = ErrorHandler.getErrorMessage(this, result.exception);
|
||||
confirmDialog.show(ConfirmDialog.STATUS_EDITOR_ERROR, message);
|
||||
loadingCircle.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set instance information such as upload limits
|
||||
*/
|
||||
private void onInstanceResult(Instance instance) {
|
||||
statusUpdate.setInstanceInformation(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* start uploading status and media files
|
||||
*/
|
||||
@ -346,7 +365,7 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
|
||||
// first initialize filestreams of the media files
|
||||
if (statusUpdate.prepare(getContentResolver())) {
|
||||
// send status
|
||||
uploaderAsync.execute(statusUpdate, this);
|
||||
statusUpdater.execute(statusUpdate, statusUpdateResult);
|
||||
// show progress dialog
|
||||
loadingCircle.show();
|
||||
} else {
|
||||
|
@ -24,8 +24,10 @@ import android.content.DialogInterface.OnDismissListener;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.location.Location;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.MotionEvent;
|
||||
@ -321,6 +323,10 @@ public class VideoViewer extends MediaActivity implements OnSeekBarChangeListene
|
||||
}
|
||||
// setup video looping for gif
|
||||
else {
|
||||
// disable audiofocus for gif
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
|
||||
videoView.setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE);
|
||||
}
|
||||
loadingCircle.setVisibility(INVISIBLE);
|
||||
mp.setLooping(true);
|
||||
mp.start();
|
||||
|
Loading…
x
Reference in New Issue
Block a user