mirror of
https://codeberg.org/Bazsalanszky/Infinity-For-Lemmy.git
synced 2025-01-24 00:44:43 +01:00
Preparing to support multi user. Use the database to store accounts' info. LoginActivity is successfully refactored. Any other features are unavailable for now.
This commit is contained in:
parent
77d83654aa
commit
7f2bc01180
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="FieldCanBeLocal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="EXCLUDE_ANNOS">
|
||||||
|
<value>
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="androidx.room.ColumnInfo" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="IGNORE_FIELDS_USED_IN_MULTIPLE_METHODS" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
72
app/src/main/java/Account/Account.java
Normal file
72
app/src/main/java/Account/Account.java
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package Account;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.ColumnInfo;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
@Entity(tableName = "accounts")
|
||||||
|
public class Account {
|
||||||
|
@PrimaryKey
|
||||||
|
@NonNull
|
||||||
|
@ColumnInfo(name = "username")
|
||||||
|
private String username;
|
||||||
|
@ColumnInfo(name = "profile_image_url")
|
||||||
|
private String profileImageUrl;
|
||||||
|
@ColumnInfo(name = "banner_image_url")
|
||||||
|
private String bannerImageUrl;
|
||||||
|
@ColumnInfo(name = "karma")
|
||||||
|
private int karma;
|
||||||
|
@ColumnInfo(name = "access_token")
|
||||||
|
private String accessToken;
|
||||||
|
@ColumnInfo(name = "refresh_token")
|
||||||
|
private String refreshToken;
|
||||||
|
@ColumnInfo(name = "code")
|
||||||
|
private String code;
|
||||||
|
@ColumnInfo(name = "is_current_user")
|
||||||
|
private boolean isCurrentUser;
|
||||||
|
|
||||||
|
public Account(@NonNull String username, String accessToken, String refreshToken, String code,
|
||||||
|
String profileImageUrl, String bannerImageUrl, int karma, boolean isCurrentUser) {
|
||||||
|
this.username = username;
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
this.code = code;
|
||||||
|
this.profileImageUrl = profileImageUrl;
|
||||||
|
this.bannerImageUrl = bannerImageUrl;
|
||||||
|
this.karma = karma;
|
||||||
|
this.isCurrentUser = isCurrentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProfileImageUrl() {
|
||||||
|
return profileImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBannerImageUrl() {
|
||||||
|
return bannerImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKarma() {
|
||||||
|
return karma;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessToken() {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRefreshToken() {
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCurrentUser() {
|
||||||
|
return isCurrentUser;
|
||||||
|
}
|
||||||
|
}
|
22
app/src/main/java/Account/AccountDao.java
Normal file
22
app/src/main/java/Account/AccountDao.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package Account;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface AccountDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insert(Account account);
|
||||||
|
|
||||||
|
@Query("DELETE FROM accounts")
|
||||||
|
void deleteAllAccounts();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM accounts WHERE username = :userName COLLATE NOCASE LIMIT 1")
|
||||||
|
LiveData<Account> getAccountLiveData(String userName);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM accounts WHERE username = :userName COLLATE NOCASE LIMIT 1")
|
||||||
|
Account getAccountData(String userName);
|
||||||
|
}
|
40
app/src/main/java/Account/AccountRepository.java
Normal file
40
app/src/main/java/Account/AccountRepository.java
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package Account;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
public class AccountRepository {
|
||||||
|
private AccountDao mAccountDao;
|
||||||
|
private LiveData<Account> mAccountLiveData;
|
||||||
|
|
||||||
|
AccountRepository(Application application, String username) {
|
||||||
|
mAccountDao = AccountRoomDatabase.getDatabase(application).accountDao();
|
||||||
|
|
||||||
|
mAccountLiveData = mAccountDao.getAccountLiveData(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<Account> getAccountLiveData() {
|
||||||
|
return mAccountLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insert(Account Account) {
|
||||||
|
new InsertAsyncTask(mAccountDao).execute(Account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InsertAsyncTask extends AsyncTask<Account, Void, Void> {
|
||||||
|
|
||||||
|
private AccountDao mAsyncTaskDao;
|
||||||
|
|
||||||
|
InsertAsyncTask(AccountDao dao) {
|
||||||
|
mAsyncTaskDao = dao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(final Account... params) {
|
||||||
|
mAsyncTaskDao.insert(params[0]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
app/src/main/java/Account/AccountRoomDatabase.java
Normal file
27
app/src/main/java/Account/AccountRoomDatabase.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package Account;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.room.Database;
|
||||||
|
import androidx.room.Room;
|
||||||
|
import androidx.room.RoomDatabase;
|
||||||
|
|
||||||
|
@Database(entities = {Account.class}, version = 1)
|
||||||
|
public abstract class AccountRoomDatabase extends RoomDatabase {
|
||||||
|
private static AccountRoomDatabase INSTANCE;
|
||||||
|
|
||||||
|
public abstract AccountDao accountDao();
|
||||||
|
|
||||||
|
public static AccountRoomDatabase getDatabase(final Context context) {
|
||||||
|
if(INSTANCE == null) {
|
||||||
|
synchronized (AccountRoomDatabase.class) {
|
||||||
|
if(INSTANCE == null) {
|
||||||
|
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
|
||||||
|
AccountRoomDatabase.class, "accounts")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
47
app/src/main/java/Account/AccountViewModel.java
Normal file
47
app/src/main/java/Account/AccountViewModel.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package Account;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
public class AccountViewModel extends AndroidViewModel {
|
||||||
|
private AccountRepository mAccountRepository;
|
||||||
|
private LiveData<Account> mAccountLiveData;
|
||||||
|
|
||||||
|
public AccountViewModel(Application application, String id) {
|
||||||
|
super(application);
|
||||||
|
mAccountRepository = new AccountRepository(application, id);
|
||||||
|
mAccountLiveData = mAccountRepository.getAccountLiveData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Account> getAccountLiveData() {
|
||||||
|
return mAccountLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insert(Account userData) {
|
||||||
|
mAccountRepository.insert(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Application mApplication;
|
||||||
|
|
||||||
|
private final String userName;
|
||||||
|
|
||||||
|
public Factory(@NonNull Application application, String userName) {
|
||||||
|
mApplication = application;
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends ViewModel> T create(Class<T> modelClass) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (T) new AccountViewModel(mApplication, userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
package ml.docilealligator.infinityforreddit;
|
package ml.docilealligator.infinityforreddit;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Retrofit;
|
import retrofit2.Retrofit;
|
||||||
@ -39,4 +40,28 @@ class FetchMyInfo {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void fetchMyInfo(final Retrofit retrofit, String accessToken,
|
||||||
|
final FetchUserMyListener fetchUserMyListener) {
|
||||||
|
RedditAPI api = retrofit.create(RedditAPI.class);
|
||||||
|
|
||||||
|
Call<String> userInfo = api.getMyInfo(RedditUtils.getOAuthHeader(accessToken));
|
||||||
|
userInfo.enqueue(new Callback<String>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<String> call, @NonNull retrofit2.Response<String> response) {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
fetchUserMyListener.onFetchMyInfoSuccess(response.body());
|
||||||
|
} else {
|
||||||
|
Log.i("call failed", response.message());
|
||||||
|
fetchUserMyListener.onFetchMyInfoFail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
|
||||||
|
Log.i("call failed", t.getMessage());
|
||||||
|
fetchUserMyListener.onFetchMyInfoFail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,9 @@ import android.webkit.WebView;
|
|||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@ -21,8 +24,7 @@ import java.util.Map;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import Account.AccountRoomDatabase;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
@ -36,6 +38,10 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
@Named("no_oauth")
|
@Named("no_oauth")
|
||||||
Retrofit mRetrofit;
|
Retrofit mRetrofit;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named("oauth")
|
||||||
|
Retrofit mOauthRetrofit;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@ -85,41 +91,69 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
|
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
|
||||||
if(response.isSuccessful()) {
|
if(response.isSuccessful()) {
|
||||||
try {
|
try {
|
||||||
JSONObject responseJSON = new JSONObject(response.body());
|
String accountResponse = response.body();
|
||||||
|
if(accountResponse == null) {
|
||||||
|
//Handle error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject responseJSON = new JSONObject(accountResponse);
|
||||||
String accessToken = responseJSON.getString(RedditUtils.ACCESS_TOKEN_KEY);
|
String accessToken = responseJSON.getString(RedditUtils.ACCESS_TOKEN_KEY);
|
||||||
String refreshToken = responseJSON.getString(RedditUtils.REFRESH_TOKEN_KEY);
|
String refreshToken = responseJSON.getString(RedditUtils.REFRESH_TOKEN_KEY);
|
||||||
|
|
||||||
editor.putString(SharedPreferencesUtils.ACCESS_TOKEN_KEY, accessToken);
|
FetchMyInfo.fetchMyInfo(mOauthRetrofit, accessToken, new FetchMyInfo.FetchUserMyListener() {
|
||||||
editor.putString(SharedPreferencesUtils.REFRESH_TOKEN_KEY, refreshToken);
|
@Override
|
||||||
editor.apply();
|
public void onFetchMyInfoSuccess(String response) {
|
||||||
|
ParseMyInfo.parseMyInfo(response, new ParseMyInfo.ParseMyInfoListener() {
|
||||||
|
@Override
|
||||||
|
public void onParseMyInfoSuccess(String name, String profileImageUrl, String bannerImageUrl, int karma) {
|
||||||
|
new ParseAndInsertAccount(name, accessToken, refreshToken, profileImageUrl, bannerImageUrl,
|
||||||
|
karma, authCode, AccountRoomDatabase.getDatabase(LoginActivity.this).accountDao(),
|
||||||
|
() -> {
|
||||||
Intent resultIntent = new Intent();
|
Intent resultIntent = new Intent();
|
||||||
setResult(Activity.RESULT_OK, resultIntent);
|
setResult(Activity.RESULT_OK, resultIntent);
|
||||||
finish();
|
finish();
|
||||||
|
}).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onParseMyInfoFail() {
|
||||||
|
Toast.makeText(LoginActivity.this, R.string.parse_user_info_error, Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchMyInfoFail() {
|
||||||
|
Toast.makeText(LoginActivity.this, R.string.cannot_fetch_user_info, Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
Toast.makeText(LoginActivity.this, "Error occurred when parsing the JSON response", Toast.LENGTH_SHORT).show();
|
Toast.makeText(LoginActivity.this, R.string.parse_json_response_error, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(LoginActivity.this, "Error Retrieving the token", Toast.LENGTH_SHORT).show();
|
Toast.makeText(LoginActivity.this, R.string.retrieve_token_error, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<String> call, Throwable t) {
|
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
|
||||||
Toast.makeText(LoginActivity.this, "Error Retrieving the token", Toast.LENGTH_SHORT).show();
|
Toast.makeText(LoginActivity.this, R.string.retrieve_token_error, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(LoginActivity.this, "Something went wrong. Try again later.", Toast.LENGTH_SHORT).show();
|
Toast.makeText(LoginActivity.this, R.string.something_went_wrong, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (url.contains("error=access_denied")) {
|
} else if (url.contains("error=access_denied")) {
|
||||||
Toast.makeText(LoginActivity.this, "Access denied", Toast.LENGTH_SHORT).show();
|
Toast.makeText(LoginActivity.this, R.string.access_denied, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
view.loadUrl(url);
|
view.loadUrl(url);
|
||||||
@ -142,8 +176,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
if (item.getItemId() == android.R.id.home) {
|
||||||
case android.R.id.home:
|
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package ml.docilealligator.infinityforreddit;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import Account.Account;
|
||||||
|
import Account.AccountDao;
|
||||||
|
|
||||||
|
class ParseAndInsertAccount extends AsyncTask<Void, Void, Void> {
|
||||||
|
|
||||||
|
interface ParseAndInsertAccountListener {
|
||||||
|
void success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
private String accessToken;
|
||||||
|
private String refreshToken;
|
||||||
|
private String profileImageUrl;
|
||||||
|
private String bannerImageUrl;
|
||||||
|
private int karma;
|
||||||
|
private String code;
|
||||||
|
private AccountDao accountDao;
|
||||||
|
private ParseAndInsertAccountListener parseAndInsertAccountListener;
|
||||||
|
|
||||||
|
ParseAndInsertAccount(String username, String accessToken, String refreshToken, String profileImageUrl, String bannerImageUrl,
|
||||||
|
int karma, String code, AccountDao accountDao,
|
||||||
|
ParseAndInsertAccountListener parseAndInsertAccountListener) {
|
||||||
|
this.username = username;
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
this.profileImageUrl = profileImageUrl;
|
||||||
|
this.bannerImageUrl = bannerImageUrl;
|
||||||
|
this.karma = karma;
|
||||||
|
this.code = code;
|
||||||
|
this.accountDao = accountDao;
|
||||||
|
this.parseAndInsertAccountListener = parseAndInsertAccountListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... voids) {
|
||||||
|
Account account = new Account(username, accessToken, refreshToken, code, profileImageUrl,
|
||||||
|
bannerImageUrl, karma, true);
|
||||||
|
accountDao.insert(account);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void aVoid) {
|
||||||
|
parseAndInsertAccountListener.success();
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,12 @@
|
|||||||
<string name="action_send">Send</string>
|
<string name="action_send">Send</string>
|
||||||
<string name="action_sort">Sort</string>
|
<string name="action_sort">Sort</string>
|
||||||
|
|
||||||
|
<string name="parse_json_response_error">Error occurred when parsing the JSON response</string>
|
||||||
|
<string name="retrieve_token_error">Error Retrieving the token</string>
|
||||||
|
<string name="something_went_wrong">Something went wrong. Try again later.</string>
|
||||||
|
<string name="access_denied">Access denied</string>
|
||||||
|
<string name="parse_user_info_error">Error occurred when parsing the user info</string>
|
||||||
|
|
||||||
<string name="tap_to_retry">Error loading image. Tap to retry.</string>
|
<string name="tap_to_retry">Error loading image. Tap to retry.</string>
|
||||||
<string name="load_posts_error">Error loading posts.\nTap to retry.</string>
|
<string name="load_posts_error">Error loading posts.\nTap to retry.</string>
|
||||||
<string name="search_subreddits_error">Error searching subreddits.\nTap to retry.</string>
|
<string name="search_subreddits_error">Error searching subreddits.\nTap to retry.</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user