mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Use updated country picker when deleting account.
This commit is contained in:
committed by
jeffrey-signal
parent
6f213158ed
commit
c38fafe9fd
@@ -1,49 +0,0 @@
|
||||
package org.thoughtcrime.securesms.delete;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class Country {
|
||||
private final String displayName;
|
||||
private final int code;
|
||||
private final String normalized;
|
||||
private final String region;
|
||||
|
||||
Country(@NonNull String displayName, int code, @NonNull String region) {
|
||||
this.displayName = displayName;
|
||||
this.code = code;
|
||||
this.normalized = displayName.toLowerCase();
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@NonNull String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getNormalizedDisplayName() {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@NonNull String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Country country = (Country) o;
|
||||
return displayName.equals(country.displayName) &&
|
||||
code == country.code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(displayName, code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.delete
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeSelectScreen
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeViewModel
|
||||
|
||||
/**
|
||||
* Country code picker specific to deleting an account.
|
||||
*/
|
||||
class DeleteAccountCountryCodeFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
const val RESULT_KEY = "result_key"
|
||||
const val RESULT_COUNTRY = "result_country"
|
||||
}
|
||||
|
||||
private val viewModel: CountryCodeViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
CountryCodeSelectScreen(
|
||||
state = state,
|
||||
title = stringResource(R.string.CountryCodeFragment__your_country),
|
||||
onSearch = { search -> viewModel.filterCountries(search) },
|
||||
onDismissed = { findNavController().popBackStack() },
|
||||
onClick = { country ->
|
||||
setFragmentResult(RESULT_KEY, bundleOf(RESULT_COUNTRY to country))
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.loadCountries()
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package org.thoughtcrime.securesms.delete;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class DeleteAccountCountryPickerAdapter extends ListAdapter<Country, DeleteAccountCountryPickerAdapter.ViewHolder> {
|
||||
|
||||
private final Callback callback;
|
||||
|
||||
protected DeleteAccountCountryPickerAdapter(@NonNull Callback callback) {
|
||||
super(new CountryDiffCallback());
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.delete_account_country_adapter_item, parent, false);
|
||||
|
||||
return new ViewHolder(view, position -> callback.onItemSelected(getItem(position)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.textView.setText(getItem(position).getDisplayName());
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView textView;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull Consumer<Integer> onItemClickedConsumer) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(android.R.id.text1);
|
||||
|
||||
itemView.setOnClickListener(unused -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
onItemClickedConsumer.accept(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class CountryDiffCallback extends DiffUtil.ItemCallback<Country> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Country oldItem, @NonNull Country newItem) {
|
||||
return Objects.equals(oldItem.getCode(), newItem.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Country oldItem, @NonNull Country newItem) {
|
||||
return Objects.equals(oldItem, newItem);
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
void onItemSelected(@NonNull Country country);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.thoughtcrime.securesms.delete;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
public class DeleteAccountCountryPickerFragment extends DialogFragment {
|
||||
|
||||
private DeleteAccountViewModel viewModel;
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager) {
|
||||
new DeleteAccountCountryPickerFragment().show(fragmentManager, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.delete_account_country_picker, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
Toolbar toolbar = view.findViewById(R.id.delete_account_country_picker_toolbar);
|
||||
EditText searchFilter = view.findViewById(R.id.delete_account_country_picker_filter);
|
||||
RecyclerView recycler = view.findViewById(R.id.delete_account_country_picker_recycler);
|
||||
DeleteAccountCountryPickerAdapter adapter = new DeleteAccountCountryPickerAdapter(this::onCountryPicked);
|
||||
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> dismiss());
|
||||
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(DeleteAccountViewModel.class);
|
||||
viewModel.getFilteredCountries().observe(getViewLifecycleOwner(), adapter::submitList);
|
||||
|
||||
searchFilter.addTextChangedListener(new AfterTextChanged(this::onQueryChanged));
|
||||
}
|
||||
|
||||
private void onQueryChanged(@NonNull Editable e) {
|
||||
viewModel.onQueryChanged(e.toString());
|
||||
}
|
||||
|
||||
private void onCountryPicked(@NonNull Country country) {
|
||||
viewModel.onRegionSelected(country.getRegion());
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
package org.thoughtcrime.securesms.delete;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -28,6 +21,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
@@ -36,8 +30,10 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText;
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.Country;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -45,7 +41,7 @@ import java.util.Optional;
|
||||
|
||||
public class DeleteAccountFragment extends Fragment {
|
||||
|
||||
private ArrayAdapter<String> countrySpinnerAdapter;
|
||||
private TextView countryPicker;
|
||||
private TextView bullets;
|
||||
private LabeledEditText countryCode;
|
||||
private LabeledEditText number;
|
||||
@@ -60,13 +56,13 @@ public class DeleteAccountFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
Spinner countrySpinner = view.findViewById(R.id.delete_account_fragment_country_spinner);
|
||||
View confirm = view.findViewById(R.id.delete_account_fragment_delete);
|
||||
Toolbar toolbar = view.findViewById(R.id.toolbar);
|
||||
|
||||
bullets = view.findViewById(R.id.delete_account_fragment_bullets);
|
||||
countryCode = view.findViewById(R.id.delete_account_fragment_country_code);
|
||||
number = view.findViewById(R.id.delete_account_fragment_number);
|
||||
bullets = view.findViewById(R.id.delete_account_fragment_bullets);
|
||||
countryCode = view.findViewById(R.id.delete_account_fragment_country_code);
|
||||
number = view.findViewById(R.id.delete_account_fragment_number);
|
||||
countryPicker = view.findViewById(R.id.delete_account_fragment_country_picker);
|
||||
|
||||
viewModel = new ViewModelProvider(requireActivity(), new DeleteAccountViewModel.Factory(new DeleteAccountRepository())).get(DeleteAccountViewModel.class);
|
||||
viewModel.getCountryDisplayName().observe(getViewLifecycleOwner(), this::setCountryDisplay);
|
||||
@@ -80,8 +76,14 @@ public class DeleteAccountFragment extends Fragment {
|
||||
countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
|
||||
confirm.setOnClickListener(unused -> viewModel.submit());
|
||||
toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(v).popBackStack());
|
||||
countryPicker.setOnClickListener(v -> SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_deleteAccountFragment_to_deleteAccountCountryFragment));
|
||||
|
||||
initializeSpinner(countrySpinner);
|
||||
getParentFragmentManager().setFragmentResultListener(DeleteAccountCountryCodeFragment.RESULT_KEY, this, (key, bundle) -> {
|
||||
Country country = bundle.getParcelable(DeleteAccountCountryCodeFragment.RESULT_COUNTRY);
|
||||
if (country != null) {
|
||||
viewModel.onRegionSelected(country.getRegionCode());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateBullets(@NonNull Optional<String> formattedBalance) {
|
||||
@@ -101,38 +103,11 @@ public class DeleteAccountFragment extends Fragment {
|
||||
return builder;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void initializeSpinner(@NonNull Spinner countrySpinner) {
|
||||
countrySpinnerAdapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item);
|
||||
countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
countrySpinner.setAdapter(countrySpinnerAdapter);
|
||||
countrySpinner.setOnTouchListener((view, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
pickCountry();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
countrySpinner.setOnKeyListener((view, keyCode, event) -> {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
|
||||
pickCountry();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void pickCountry() {
|
||||
countryCode.clearFocus();
|
||||
DeleteAccountCountryPickerFragment.show(requireFragmentManager());
|
||||
}
|
||||
|
||||
private void setCountryDisplay(@NonNull String regionDisplayName) {
|
||||
countrySpinnerAdapter.clear();
|
||||
if (TextUtils.isEmpty(regionDisplayName)) {
|
||||
countrySpinnerAdapter.add(requireContext().getString(R.string.RegistrationActivity_select_your_country));
|
||||
countryPicker.setText(requireContext().getString(R.string.RegistrationActivity_select_your_country));
|
||||
} else {
|
||||
countrySpinnerAdapter.add(regionDisplayName);
|
||||
countryPicker.setText(regionDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,20 +24,10 @@ import org.whispersystems.signalservice.internal.EmptyResponse;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.Collator;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
class DeleteAccountRepository {
|
||||
private static final String TAG = Log.tag(DeleteAccountRepository.class);
|
||||
|
||||
@NonNull List<Country> getAllCountries() {
|
||||
return Stream.of(PhoneNumberUtil.getInstance().getSupportedRegions())
|
||||
.map(DeleteAccountRepository::getCountryForRegion)
|
||||
.sorted(new RegionComparator())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@NonNull String getRegionDisplayName(@NonNull String region) {
|
||||
return E164Util.getRegionDisplayName(region).orElse("");
|
||||
}
|
||||
@@ -126,25 +116,4 @@ class DeleteAccountRepository {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static @NonNull Country getCountryForRegion(@NonNull String region) {
|
||||
return new Country(E164Util.getRegionDisplayName(region).orElse(""),
|
||||
PhoneNumberUtil.getInstance().getCountryCodeForRegion(region),
|
||||
region);
|
||||
}
|
||||
|
||||
private static class RegionComparator implements Comparator<Country> {
|
||||
|
||||
private final Collator collator;
|
||||
|
||||
RegionComparator() {
|
||||
collator = Collator.getInstance();
|
||||
collator.setStrength(Collator.PRIMARY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Country lhs, Country rhs) {
|
||||
return collator.compare(lhs.getDisplayName(), rhs.getDisplayName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,23 +29,17 @@ import java.util.Optional;
|
||||
public class DeleteAccountViewModel extends ViewModel {
|
||||
|
||||
private final DeleteAccountRepository repository;
|
||||
private final List<Country> allCountries;
|
||||
private final LiveData<List<Country>> filteredCountries;
|
||||
private final MutableLiveData<String> regionCode;
|
||||
private final LiveData<String> countryDisplayName;
|
||||
private final MutableLiveData<Long> nationalNumber;
|
||||
private final MutableLiveData<String> query;
|
||||
private final SingleLiveEvent<DeleteAccountEvent> events;
|
||||
private final LiveData<Optional<String>> walletBalance;
|
||||
|
||||
public DeleteAccountViewModel(@NonNull DeleteAccountRepository repository) {
|
||||
this.repository = repository;
|
||||
this.allCountries = repository.getAllCountries();
|
||||
this.regionCode = new DefaultValueLiveData<>("ZZ"); // PhoneNumberUtil private static final String UNKNOWN_REGION = "ZZ";
|
||||
this.nationalNumber = new MutableLiveData<>();
|
||||
this.query = new DefaultValueLiveData<>("");
|
||||
this.countryDisplayName = Transformations.map(regionCode, repository::getRegionDisplayName);
|
||||
this.filteredCountries = Transformations.map(query, q -> Stream.of(allCountries).filter(country -> isMatch(q, country)).toList());
|
||||
this.events = new SingleLiveEvent<>();
|
||||
this.walletBalance = Transformations.map(SignalStore.payments().liveMobileCoinBalance(),
|
||||
DeleteAccountViewModel::getFormattedWalletBalance);
|
||||
@@ -55,10 +49,6 @@ public class DeleteAccountViewModel extends ViewModel {
|
||||
return walletBalance;
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<Country>> getFilteredCountries() {
|
||||
return filteredCountries;
|
||||
}
|
||||
|
||||
@NonNull LiveData<String> getCountryDisplayName() {
|
||||
return Transformations.distinctUntilChanged(countryDisplayName);
|
||||
}
|
||||
@@ -75,10 +65,6 @@ public class DeleteAccountViewModel extends ViewModel {
|
||||
return nationalNumber.getValue();
|
||||
}
|
||||
|
||||
void onQueryChanged(@NonNull String query) {
|
||||
this.query.setValue(query.toLowerCase());
|
||||
}
|
||||
|
||||
void deleteAccount() {
|
||||
repository.deleteAccount(events::postValue);
|
||||
}
|
||||
@@ -146,14 +132,6 @@ public class DeleteAccountViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMatch(@NonNull String query, @NonNull Country country) {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
return true;
|
||||
} else {
|
||||
return country.getNormalizedDisplayName().contains(query.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final DeleteAccountRepository repository;
|
||||
|
||||
@@ -83,11 +83,14 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/delete_account_fragment_enter_phone_number">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/delete_account_fragment_country_spinner"
|
||||
android:layout_width="fill_parent"
|
||||
<TextView
|
||||
android:id="@+id/delete_account_fragment_country_picker"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:drawableEnd="@drawable/ic_chevron_16"
|
||||
android:drawableTint="@color/signal_colorOnSurface"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -223,7 +223,21 @@
|
||||
android:id="@+id/deleteAccountFragment"
|
||||
android:name="org.thoughtcrime.securesms.delete.DeleteAccountFragment"
|
||||
android:label="delete_account_fragment"
|
||||
tools:layout="@layout/delete_account_fragment" />
|
||||
tools:layout="@layout/delete_account_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_deleteAccountFragment_to_deleteAccountCountryFragment"
|
||||
app:destination="@id/deleteAccountCountryFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/deleteAccountCountryFragment"
|
||||
android:name="org.thoughtcrime.securesms.delete.DeleteAccountCountryCodeFragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/exportAccountDataFragment"
|
||||
|
||||
Reference in New Issue
Block a user