mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-20 00:35:44 +00:00
Bug 1058806 - Implement magic year month/day boxes in Android Firefox Account create account flow. r=nalexander,vivek
========21c63a8d1a
Author: vivek <vivekb.balakrishnan@gmail.com> Bug 1058806 - Part 4: Maintain magic year state across orientation changes. ========da074438fe
Author: vivek <vivekb.balakrishnan@gmail.com> Date: Fri Jan 16 16:46:47 2015 +0200 Bug 1058806 - Part 3: Maintain magic year across create -> sign in -> create loop. ========982d692575
Author: vivek <vivekb.balakrishnan@gmail.com> Date: Sat Dec 20 20:07:45 2014 +0200 Bug 1058806 - Part 2: Add date and month to UI. ========15594d36c9
Author: vivek <vivekb.balakrishnan@gmail.com> Date: Sat Dec 20 04:36:49 2014 +0200 Bug 1058806 - Part 1: Make age pass check consider date and month. --HG-- extra : rebase_source : 11ab11c519461fa1be8d6dda34777620db05468d
This commit is contained in:
parent
3ea68f39ad
commit
a1f24342ea
@ -4,8 +4,10 @@
|
||||
|
||||
package org.mozilla.gecko.background.fxa;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
|
||||
@ -46,31 +48,69 @@ public class FxAccountAgeLockoutHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the age of somebody born in <code>yearOfBirth</code> is
|
||||
* definitely old enough to create an account.
|
||||
* Return true if the given year is the magic year.
|
||||
* <p>
|
||||
* This errs towards locking out users who might be old enough, but are not
|
||||
* definitely old enough.
|
||||
* The <i>magic year</i> is the calendar year when the user is the minimum age
|
||||
* older. That is, for part of the magic year the user is younger than the age
|
||||
* limit and for part of the magic year the user is older than the age limit.
|
||||
*
|
||||
* @param yearOfBirth
|
||||
* @return true if somebody born in <code>yearOfBirth</code> is definitely old
|
||||
* enough.
|
||||
* @return true if <code>yearOfBirth</code> is the magic year.
|
||||
*/
|
||||
public static boolean passesAgeCheck(int yearOfBirth) {
|
||||
int thisYear = Calendar.getInstance().get(Calendar.YEAR);
|
||||
int approximateAge = thisYear - yearOfBirth;
|
||||
boolean oldEnough = approximateAge >= FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
|
||||
public static boolean isMagicYear(int yearOfBirth) {
|
||||
final Calendar cal = Calendar.getInstance();
|
||||
final int thisYear = cal.get(Calendar.YEAR);
|
||||
return (thisYear - yearOfBirth) == FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the age of somebody born in
|
||||
* <code>dayOfBirth/zeroBasedMonthOfBirth/yearOfBirth</code> is old enough to
|
||||
* create an account.
|
||||
*
|
||||
* @param dayOfBirth
|
||||
* @param zeroBasedMonthOfBirth
|
||||
* @param yearOfBirth
|
||||
* @return true if somebody born in
|
||||
* <code>dayOfBirth/zeroBasedMonthOfBirth/yearOfBirth</code> is old enough.
|
||||
*/
|
||||
public static boolean passesAgeCheck(final int dayOfBirth, final int zeroBasedMonthOfBirth, final int yearOfBirth) {
|
||||
final Calendar latestBirthday = Calendar.getInstance();
|
||||
final int y = latestBirthday.get(Calendar.YEAR);
|
||||
final int m = latestBirthday.get(Calendar.MONTH);
|
||||
final int d = latestBirthday.get(Calendar.DAY_OF_MONTH);
|
||||
latestBirthday.clear();
|
||||
latestBirthday.set(y - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT, m, d);
|
||||
|
||||
// Go back one second, so that the exact same birthday and latestBirthday satisfy birthday <= latestBirthday.
|
||||
latestBirthday.add(Calendar.SECOND, 1);
|
||||
|
||||
final Calendar birthday = Calendar.getInstance();
|
||||
birthday.clear();
|
||||
birthday.set(yearOfBirth, zeroBasedMonthOfBirth, dayOfBirth);
|
||||
|
||||
boolean oldEnough = birthday.before(latestBirthday);
|
||||
|
||||
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
|
||||
FxAccountUtils.pii(LOG_TAG, "Age check " + (oldEnough ? "passes" : "fails") +
|
||||
": age is " + approximateAge + " = " + thisYear + " - " + yearOfBirth);
|
||||
final StringBuilder message = new StringBuilder();
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
|
||||
message.append("Age check ");
|
||||
message.append(oldEnough ? "passes" : "fails");
|
||||
message.append(": birthday is ");
|
||||
message.append(sdf.format(birthday.getTime()));
|
||||
message.append("; latest birthday is ");
|
||||
message.append(sdf.format(latestBirthday.getTime()));
|
||||
message.append(" (Y/M/D).");
|
||||
FxAccountUtils.pii(LOG_TAG, message.toString());
|
||||
}
|
||||
|
||||
return oldEnough;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function for UI use only.
|
||||
*/
|
||||
public static boolean passesAgeCheck(String yearText, String[] yearItems) {
|
||||
public static boolean passesAgeCheck(int dayOfBirth, int zeroBaseMonthOfBirth, String yearText, String[] yearItems) {
|
||||
if (yearText == null) {
|
||||
throw new IllegalArgumentException("yearText must not be null");
|
||||
}
|
||||
@ -91,6 +131,7 @@ public class FxAccountAgeLockoutHelper {
|
||||
FxAccountUtils.pii(LOG_TAG, "Passed age check: year text was found in item list but was not a number.");
|
||||
return true;
|
||||
}
|
||||
return passesAgeCheck(yearOfBirth);
|
||||
|
||||
return passesAgeCheck(dayOfBirth, zeroBaseMonthOfBirth, yearOfBirth);
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ public class FxAccountConstants {
|
||||
public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://api-accounts.stage.mozaws.net/v1";
|
||||
public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://token.stage.mozaws.net/1.0/sync/1.5";
|
||||
|
||||
// You must be at least 14 years old to create a Firefox Account.
|
||||
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 14;
|
||||
// You must be at least 13 years old, on the day of creation, to create a Firefox Account.
|
||||
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13;
|
||||
|
||||
// You must wait 15 minutes after failing an age check before trying to create a different account.
|
||||
public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
|
||||
|
@ -63,6 +63,8 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
|
||||
public static final String EXTRA_PASSWORD = "password";
|
||||
public static final String EXTRA_PASSWORD_SHOWN = "password_shown";
|
||||
public static final String EXTRA_YEAR = "year";
|
||||
public static final String EXTRA_MONTH = "month";
|
||||
public static final String EXTRA_DAY = "day";
|
||||
public static final String EXTRA_EXTRAS = "extras";
|
||||
|
||||
public static final String JSON_KEY_AUTH = "auth";
|
||||
|
@ -7,6 +7,7 @@ package org.mozilla.gecko.fxa.activities;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
@ -52,8 +53,13 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
private static final int CHILD_REQUEST_CODE = 2;
|
||||
|
||||
protected String[] yearItems;
|
||||
protected String[] monthItems;
|
||||
protected String[] dayItems;
|
||||
protected EditText yearEdit;
|
||||
protected EditText monthEdit;
|
||||
protected EditText dayEdit;
|
||||
protected CheckBox chooseCheckBox;
|
||||
protected View monthDaycombo;
|
||||
|
||||
protected Map<String, Boolean> selectedEngines;
|
||||
|
||||
@ -71,6 +77,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
|
||||
showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
|
||||
yearEdit = (EditText) ensureFindViewById(null, R.id.year_edit, "year edit");
|
||||
monthEdit = (EditText) ensureFindViewById(null, R.id.month_edit, "month edit");
|
||||
dayEdit = (EditText) ensureFindViewById(null, R.id.day_edit, "day edit");
|
||||
monthDaycombo = ensureFindViewById(null, R.id.month_day_combo, "month day combo");
|
||||
remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
|
||||
button = (Button) ensureFindViewById(null, R.id.button, "create account button");
|
||||
progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
|
||||
@ -84,6 +93,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
createShowPasswordButton();
|
||||
linkifyPolicy();
|
||||
createChooseCheckBox();
|
||||
initializeMonthAndDayValues();
|
||||
|
||||
View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link");
|
||||
signInInsteadLink.setOnClickListener(new OnClickListener() {
|
||||
@ -97,11 +107,23 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
updateFromIntentExtras();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
updateMonthAndDayFromBundle(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
updateBundleWithMonthAndDay(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle makeExtrasBundle(String email, String password) {
|
||||
final Bundle extras = super.makeExtrasBundle(email, password);
|
||||
final String year = yearEdit.getText().toString();
|
||||
extras.putString(EXTRA_YEAR, year);
|
||||
extras.putString(EXTRA_YEAR, yearEdit.getText().toString());
|
||||
updateBundleWithMonthAndDay(extras);
|
||||
return extras;
|
||||
}
|
||||
|
||||
@ -111,9 +133,40 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
|
||||
if (getIntent() != null) {
|
||||
yearEdit.setText(getIntent().getStringExtra(EXTRA_YEAR));
|
||||
updateMonthAndDayFromBundle(getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBundleWithMonthAndDay(final Bundle bundle) {
|
||||
if (monthEdit.getTag() != null) {
|
||||
bundle.putInt(EXTRA_MONTH, (Integer) monthEdit.getTag());
|
||||
}
|
||||
if (dayEdit.getTag() != null) {
|
||||
bundle.putInt(EXTRA_DAY, (Integer) dayEdit.getTag());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMonthAndDayFromBundle(final Bundle extras) {
|
||||
final Integer zeroBasedMonthIndex = (Integer) extras.get(EXTRA_MONTH);
|
||||
final Integer oneBasedDayIndex = (Integer) extras.get(EXTRA_DAY);
|
||||
maybeEnableMonthAndDayButtons();
|
||||
|
||||
if (zeroBasedMonthIndex != null) {
|
||||
monthEdit.setText(monthItems[zeroBasedMonthIndex]);
|
||||
monthEdit.setTag(Integer.valueOf(zeroBasedMonthIndex));
|
||||
createDayEdit(zeroBasedMonthIndex);
|
||||
|
||||
if (oneBasedDayIndex != null && dayItems != null) {
|
||||
dayEdit.setText(dayItems[oneBasedDayIndex - 1]);
|
||||
dayEdit.setTag(Integer.valueOf(oneBasedDayIndex));
|
||||
}
|
||||
} else {
|
||||
monthEdit.setText("");
|
||||
dayEdit.setText("");
|
||||
}
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showClientRemoteException(final FxAccountClientRemoteException e) {
|
||||
if (!e.isAccountAlreadyExists()) {
|
||||
@ -186,6 +239,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
yearEdit.setText(yearItems[which]);
|
||||
maybeEnableMonthAndDayButtons();
|
||||
updateButtonState();
|
||||
}
|
||||
};
|
||||
@ -200,6 +254,114 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMonthAndDayValues() {
|
||||
// Hide Month and day pickers
|
||||
monthDaycombo.setVisibility(View.GONE);
|
||||
dayEdit.setEnabled(false);
|
||||
|
||||
// Populate month names.
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
final Map<String, Integer> monthNamesMap = calendar.getDisplayNames(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
|
||||
monthItems = new String[monthNamesMap.size()];
|
||||
for (Map.Entry<String, Integer> entry : monthNamesMap.entrySet()) {
|
||||
monthItems[entry.getValue()] = entry.getKey();
|
||||
}
|
||||
createMonthEdit();
|
||||
}
|
||||
|
||||
protected void createMonthEdit() {
|
||||
monthEdit.setText("");
|
||||
monthEdit.setTag(null);
|
||||
monthEdit.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
android.content.DialogInterface.OnClickListener listener = new Dialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
monthEdit.setText(monthItems[which]);
|
||||
monthEdit.setTag(Integer.valueOf(which));
|
||||
createDayEdit(which);
|
||||
updateButtonState();
|
||||
}
|
||||
};
|
||||
final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this)
|
||||
.setTitle(R.string.fxaccount_create_account_month_of_birth)
|
||||
.setItems(monthItems, listener)
|
||||
.setIcon(R.drawable.icon)
|
||||
.create();
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void createDayEdit(final int monthIndex) {
|
||||
dayEdit.setText("");
|
||||
dayEdit.setTag(null);
|
||||
dayEdit.setEnabled(true);
|
||||
|
||||
String yearText = yearEdit.getText().toString();
|
||||
Integer birthYear;
|
||||
try {
|
||||
birthYear = Integer.parseInt(yearText);
|
||||
} catch (NumberFormatException e) {
|
||||
// Ideal this should never happen.
|
||||
Logger.debug(LOG_TAG, "Exception while parsing year value" + e);
|
||||
return;
|
||||
}
|
||||
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.set(birthYear, monthIndex, 1);
|
||||
LinkedList<String> days = new LinkedList<String>();
|
||||
for (int i = c.getActualMinimum(Calendar.DAY_OF_MONTH); i <= c.getActualMaximum(Calendar.DAY_OF_MONTH); i++) {
|
||||
days.add(Integer.toString(i));
|
||||
}
|
||||
dayItems = days.toArray(new String[days.size()]);
|
||||
|
||||
dayEdit.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
android.content.DialogInterface.OnClickListener listener = new Dialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dayEdit.setText(dayItems[which]);
|
||||
dayEdit.setTag(Integer.valueOf(which + 1)); // Days are 1-based.
|
||||
updateButtonState();
|
||||
}
|
||||
};
|
||||
final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this)
|
||||
.setTitle(R.string.fxaccount_create_account_day_of_birth)
|
||||
.setItems(dayItems, listener)
|
||||
.setIcon(R.drawable.icon)
|
||||
.create();
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void maybeEnableMonthAndDayButtons() {
|
||||
Integer yearOfBirth = null;
|
||||
try {
|
||||
yearOfBirth = Integer.valueOf(yearEdit.getText().toString(), 10);
|
||||
} catch (NumberFormatException e) {
|
||||
Logger.debug(LOG_TAG, "Year text is not a number; assuming year is a range and that user is old enough.");
|
||||
}
|
||||
|
||||
// Check if the selected year is the magic year.
|
||||
if (yearOfBirth == null || !FxAccountAgeLockoutHelper.isMagicYear(yearOfBirth)) {
|
||||
// Year/Dec/31 is the latest birthday in the selected year, corresponding
|
||||
// to the youngest person.
|
||||
monthEdit.setTag(Integer.valueOf(11));
|
||||
dayEdit.setTag(Integer.valueOf(31));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show month and date field.
|
||||
yearEdit.setVisibility(View.GONE);
|
||||
monthDaycombo.setVisibility(View.VISIBLE);
|
||||
monthEdit.setTag(null);
|
||||
dayEdit.setTag(null);
|
||||
}
|
||||
|
||||
public void createAccount(String email, String password, Map<String, Boolean> engines) {
|
||||
String serverURI = getAuthServerEndpoint();
|
||||
PasswordStretcher passwordStretcher = makePasswordStretcher(password);
|
||||
@ -230,7 +392,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
@Override
|
||||
protected boolean shouldButtonBeEnabled() {
|
||||
return super.shouldButtonBeEnabled() &&
|
||||
(yearEdit.length() > 0);
|
||||
(yearEdit.length() > 0) &&
|
||||
(monthEdit.getTag() != null) &&
|
||||
(dayEdit.getTag() != null);
|
||||
}
|
||||
|
||||
protected void createCreateAccountButton() {
|
||||
@ -242,11 +406,13 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
|
||||
}
|
||||
final String email = emailEdit.getText().toString();
|
||||
final String password = passwordEdit.getText().toString();
|
||||
final int dayOfBirth = (Integer) dayEdit.getTag();
|
||||
final int zeroBasedMonthOfBirth = (Integer) monthEdit.getTag();
|
||||
// Only include selected engines if the user currently has the option checked.
|
||||
final Map<String, Boolean> engines = chooseCheckBox.isChecked()
|
||||
? selectedEngines
|
||||
: null;
|
||||
if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) {
|
||||
if (FxAccountAgeLockoutHelper.passesAgeCheck(dayOfBirth, zeroBasedMonthOfBirth, yearEdit.getText().toString(), yearItems)) {
|
||||
FxAccountUtils.pii(LOG_TAG, "Passed age check.");
|
||||
createAccount(email, password, engines);
|
||||
} else {
|
||||
|
@ -150,6 +150,8 @@
|
||||
etc). The account remains a "Firefox Account". -->
|
||||
<!ENTITY fxaccount_create_account_header2 'Create a Firefox Account'>
|
||||
<!ENTITY fxaccount_create_account_password_length_restriction 'Must be at least 8 characters'>
|
||||
<!ENTITY fxaccount_create_account_day_of_birth 'Day'>
|
||||
<!ENTITY fxaccount_create_account_month_of_birth 'Month'>
|
||||
<!ENTITY fxaccount_create_account_year_of_birth 'Year of birth'>
|
||||
<!-- Localization note: &formatS1; is fxaccount_policy_linktos, &formatS2; is fxaccount_policy_linkprivacy, both hyperlinked. -->
|
||||
<!ENTITY fxaccount_create_account_policy_text2 'By proceeding, I agree to the &formatS1; and &formatS2; of Firefox cloud services.'>
|
||||
|
@ -40,6 +40,35 @@
|
||||
android:hint="@string/fxaccount_create_account_year_of_birth"
|
||||
android:inputType="none" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/month_day_combo"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="1" >
|
||||
|
||||
<EditText
|
||||
android:id="@+id/month_edit"
|
||||
style="@style/FxAccountEditItem"
|
||||
android:layout_marginRight="10dp"
|
||||
android:drawableRight="@drawable/fxaccount_ddarrow_inactive"
|
||||
android:focusable="false"
|
||||
android:layout_weight="0.5"
|
||||
android:maxLength="3"
|
||||
android:hint="@string/fxaccount_create_account_month_of_birth"
|
||||
android:inputType="none" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/day_edit"
|
||||
style="@style/FxAccountEditItem"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:drawableRight="@drawable/fxaccount_ddarrow_inactive"
|
||||
android:focusable="false"
|
||||
android:layout_weight="0.5"
|
||||
android:hint="@string/fxaccount_create_account_day_of_birth"
|
||||
android:inputType="none" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/policy"
|
||||
style="@style/FxAccountLinkifiedItem"
|
||||
|
@ -138,6 +138,8 @@
|
||||
|
||||
<string name="fxaccount_create_account_header">&fxaccount_create_account_header2;</string>
|
||||
<string name="fxaccount_create_account_password_length_restriction">&fxaccount_create_account_password_length_restriction;</string>
|
||||
<string name="fxaccount_create_account_day_of_birth">&fxaccount_create_account_day_of_birth;</string>
|
||||
<string name="fxaccount_create_account_month_of_birth">&fxaccount_create_account_month_of_birth;</string>
|
||||
<string name="fxaccount_create_account_year_of_birth">&fxaccount_create_account_year_of_birth;</string>
|
||||
|
||||
<string name="fxaccount_create_account_policy_text">&fxaccount_create_account_policy_text2;</string>
|
||||
|
Loading…
Reference in New Issue
Block a user