summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas <tschneider.ac@gmail.com>2020-06-08 18:04:54 +0200
committerThomas <tschneider.ac@gmail.com>2020-06-08 18:04:54 +0200
commitdc636d1e1d1fd4745215530d47f65708a1215813 (patch)
treeeab74f1f7ed82026b677e34c83bd3954d5fdec16
parentc1f7c87d7ecbe2ca6a3c969f6985f64e4a783111 (diff)
parentb22a73613153602e791c663aa66273a378ecd3e0 (diff)
Merge branch 'develop'
-rw-r--r--app/build.gradle4
-rw-r--r--app/src/main/assets/changelogs/371.txt6
-rw-r--r--app/src/main/java/app/fedilab/android/activities/HashTagActivity.java18
-rw-r--r--app/src/main/java/app/fedilab/android/activities/SearchResultActivity.java17
-rw-r--r--app/src/main/java/app/fedilab/android/activities/SlideMediaActivity.java12
-rw-r--r--app/src/main/java/app/fedilab/android/activities/TootActivity.java6
-rw-r--r--app/src/main/java/app/fedilab/android/client/Entities/Status.java323
-rw-r--r--app/src/main/java/app/fedilab/android/helper/Helper.java3
-rw-r--r--app/src/main/res/drawable-anydpi-v24/ic_plain_atom.xml23
-rw-r--r--app/src/main/res/drawable-anydpi-v24/ic_plain_bubbles.xml21
-rw-r--r--app/src/main/res/drawable-anydpi-v24/ic_plain_crash.xml83
-rw-r--r--app/src/main/res/drawable-anydpi-v24/ic_plain_fediverse.xml71
-rw-r--r--app/src/main/res/drawable-anydpi-v24/ic_plain_hero.xml21
-rw-r--r--app/src/main/res/drawable-anydpi-v24/ic_plain_mastalab.xml23
-rw-r--r--app/src/main/res/layout/activity_hashtag.xml11
-rw-r--r--fastlane/metadata/android/en-US/changelogs/371.txt6
16 files changed, 225 insertions, 423 deletions
diff --git a/app/build.gradle b/app/build.gradle
index cbbc02398..c8d432b23 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,8 +6,8 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 29
- versionCode 370
- versionName "2.35.6"
+ versionCode 371
+ versionName "2.35.7"
multiDexEnabled true
renderscriptTargetApi 28 as int
renderscriptSupportModeEnabled true
diff --git a/app/src/main/assets/changelogs/371.txt b/app/src/main/assets/changelogs/371.txt
new file mode 100644
index 000000000..32676c8b6
--- /dev/null
+++ b/app/src/main/assets/changelogs/371.txt
@@ -0,0 +1,6 @@
+Added:
+- Automatically add hashtag to messages when composing from a search
+
+Fixed:
+- Some issues with content and URLs
+- Some crashes \ No newline at end of file
diff --git a/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java
index 07101a079..f148ef613 100644
--- a/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java
+++ b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java
@@ -32,6 +32,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -42,6 +44,7 @@ import app.fedilab.android.asynctasks.RetrieveFeedsAsyncTask;
import app.fedilab.android.client.APIResponse;
import app.fedilab.android.client.Entities.Status;
import app.fedilab.android.client.Entities.StatusDrawerParams;
+import app.fedilab.android.client.Entities.StoredStatus;
import app.fedilab.android.drawers.StatusListAdapter;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.interfaces.OnRetrieveFeedsInterface;
@@ -112,6 +115,21 @@ public class HashTagActivity extends BaseActivity implements OnRetrieveFeedsInte
swipeRefreshLayout.setColorSchemeColors(
c1, c2, c1
);
+
+ FloatingActionButton toot = findViewById(R.id.toot);
+ toot.setOnClickListener(v -> {
+ Intent intentToot = new Intent(HashTagActivity.this, TootActivity.class);
+ Bundle val = new Bundle();
+ StoredStatus storedStatus = new StoredStatus();
+ Status tagStatus = new Status();
+ tagStatus.setVisibility("public");
+ tagStatus.setContent(HashTagActivity.this, String.format("#%s ", tag));
+ storedStatus.setStatus(tagStatus);
+ val.putParcelable("storedStatus", storedStatus);
+ intentToot.putExtras(val);
+ startActivity(intentToot);
+ });
+
toolbar.setBackgroundColor(ContextCompat.getColor(HashTagActivity.this, R.color.cyanea_primary));
final RecyclerView lv_status = findViewById(R.id.lv_status);
tootsPerPage = sharedpreferences.getInt(Helper.SET_TOOT_PER_PAGE, Helper.TOOTS_PER_PAGE);
diff --git a/app/src/main/java/app/fedilab/android/activities/SearchResultActivity.java b/app/src/main/java/app/fedilab/android/activities/SearchResultActivity.java
index 61ccaf4ef..1a86dbcab 100644
--- a/app/src/main/java/app/fedilab/android/activities/SearchResultActivity.java
+++ b/app/src/main/java/app/fedilab/android/activities/SearchResultActivity.java
@@ -109,12 +109,7 @@ public class SearchResultActivity extends BaseActivity implements OnRetrieveSear
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
ImageView toolbar_close = actionBar.getCustomView().findViewById(R.id.toolbar_close);
TextView toolbar_title = actionBar.getCustomView().findViewById(R.id.toolbar_title);
- toolbar_close.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- finish();
- }
- });
+ toolbar_close.setOnClickListener(v -> finish());
if (!forTrends) {
toolbar_title.setText(search);
} else {
@@ -134,13 +129,11 @@ public class SearchResultActivity extends BaseActivity implements OnRetrieveSear
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- default:
- return super.onOptionsItemSelected(item);
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
}
+ return super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/java/app/fedilab/android/activities/SlideMediaActivity.java b/app/src/main/java/app/fedilab/android/activities/SlideMediaActivity.java
index 58807de8f..e566b65c0 100644
--- a/app/src/main/java/app/fedilab/android/activities/SlideMediaActivity.java
+++ b/app/src/main/java/app/fedilab/android/activities/SlideMediaActivity.java
@@ -92,10 +92,14 @@ public class SlideMediaActivity extends BaseFragmentActivity implements OnDownlo
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
ContentResolver cR = context.getContentResolver();
- shareIntent.setType(cR.getType(uri));
- try {
- startActivity(shareIntent);
- } catch (Exception ignored) {
+ if( cR != null && uri != null) {
+ shareIntent.setType(cR.getType(uri));
+ try {
+ startActivity(shareIntent);
+ } catch (Exception ignored) {
+ }
+ }else {
+ Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_LONG).show();
}
} else {
Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show();
diff --git a/app/src/main/java/app/fedilab/android/activities/TootActivity.java b/app/src/main/java/app/fedilab/android/activities/TootActivity.java
index b57b77ae5..0e1dd5c40 100644
--- a/app/src/main/java/app/fedilab/android/activities/TootActivity.java
+++ b/app/src/main/java/app/fedilab/android/activities/TootActivity.java
@@ -3243,6 +3243,12 @@ public class TootActivity extends BaseActivity implements UploadStatusDelegate,
toot_cw_content.setText("");
toot_cw_content.setVisibility(View.GONE);
}
+ if( status.getVisibility() == null) {
+ SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, MODE_PRIVATE);
+ String defaultVisibility = account.isLocked() ? "private" : "public";
+ String settingsVisibility = sharedpreferences.getString(Helper.SET_TOOT_VISIBILITY + "@" + account.getAcct() + "@" + account.getInstance(), defaultVisibility);
+ status.setVisibility(settingsVisibility);
+ }
toot_content.setText(content);
toot_space_left.setText(String.valueOf(countLength(social, toot_content, toot_cw_content)));
diff --git a/app/src/main/java/app/fedilab/android/client/Entities/Status.java b/app/src/main/java/app/fedilab/android/client/Entities/Status.java
index d0e14c305..dd805f19f 100644
--- a/app/src/main/java/app/fedilab/android/client/Entities/Status.java
+++ b/app/src/main/java/app/fedilab/android/client/Entities/Status.java
@@ -66,6 +66,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -420,7 +421,7 @@ public class Status implements Parcelable {
Matcher matcher;
Pattern linkPattern = Pattern.compile("<a((?!href).)*href=\"([^\"]*)\"[^>]*(((?!</a).)*)</a>");
matcher = linkPattern.matcher(spannableString);
- HashMap<String, String> targetedURL = new HashMap<>();
+ LinkedHashMap<String, String> targetedURL = new LinkedHashMap<>();
HashMap<String, Account> accountsMentionUnknown = new HashMap<>();
String liveInstance = Helper.getLiveInstance(context);
int i = 1;
@@ -553,9 +554,9 @@ public class Status implements Parcelable {
}
if (accountsMentionUnknown.size() > 0) {
- Iterator it = accountsMentionUnknown.entrySet().iterator();
+ Iterator<Map.Entry<String, Account>> it = accountsMentionUnknown.entrySet().iterator();
while (it.hasNext()) {
- Map.Entry pair = (Map.Entry) it.next();
+ Map.Entry<String, Account> pair = (Map.Entry<String, Account>) it.next();
String key = (String) pair.getKey();
Account account = (Account) pair.getValue();
String targetedAccount = "@" + account.getAcct();
@@ -603,182 +604,182 @@ public class Status implements Parcelable {
}
}
if (targetedURL.size() > 0) {
- Iterator it = targetedURL.entrySet().iterator();
+ Iterator<Map.Entry<String, String>> it = targetedURL.entrySet().iterator();
int endPosition = 0;
while (it.hasNext()) {
- Map.Entry pair = (Map.Entry) it.next();
+ Map.Entry<String, String> pair = (Map.Entry<String, String>) it.next();
String key = ((String) pair.getKey()).split("\\|")[0];
String url = (String) pair.getValue();
if (spannableStringT.toString().toLowerCase().contains(key.toLowerCase())) {
//Accounts can be mentioned several times so we have to loop
int startPosition = spannableStringT.toString().toLowerCase().indexOf(key.toLowerCase(), endPosition);
- if (startPosition < 0) {
- startPosition = 0;
- }
- endPosition = startPosition + key.length();
- if (key.contains("…") && !key.endsWith("…")) {
- key = key.split("…")[0] + "…";
- SpannableStringBuilder ssb = new SpannableStringBuilder();
- ssb.append(spannableStringT, 0, spannableStringT.length());
- if (ssb.length() >= endPosition) {
- ssb.replace(startPosition, endPosition, key);
- }
- spannableStringT = SpannableString.valueOf(ssb);
+ if (startPosition > 0) {
endPosition = startPosition + key.length();
- }
- if (endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) {
- spannableStringT.setSpan(new LongClickableSpan() {
- @Override
- public void onClick(@NonNull View textView) {
- String finalUrl = url;
- Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
- Matcher matcherLink = link.matcher(url);
- if (matcherLink.find() && !url.contains("medium.com")) {
- if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot
- CrossActions.doCrossConversation(context, finalUrl);
- } else {//It's an account
- Account account = new Account();
- String acct = matcherLink.group(2);
- if (acct != null) {
- if (acct.startsWith("@"))
- acct = acct.substring(1);
- account.setAcct(acct);
- account.setInstance(matcherLink.group(1));
- CrossActions.doCrossProfile(context, account);
+ if (key.contains("…") && !key.endsWith("…")) {
+ key = key.split("…")[0] + "…";
+ SpannableStringBuilder ssb = new SpannableStringBuilder();
+ ssb.append(spannableStringT, 0, spannableStringT.length());
+ if (ssb.length() >= endPosition) {
+ ssb.replace(startPosition, endPosition, key);
+ }
+ spannableStringT = SpannableString.valueOf(ssb);
+ endPosition = startPosition + key.length();
+ }
+ if (endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) {
+ spannableStringT.setSpan(new LongClickableSpan() {
+ @Override
+ public void onClick(@NonNull View textView) {
+ String finalUrl = url;
+ Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
+ Matcher matcherLink = link.matcher(url);
+ if (matcherLink.find() && !url.contains("medium.com")) {
+ if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot
+ CrossActions.doCrossConversation(context, finalUrl);
+ } else {//It's an account
+ Account account = new Account();
+ String acct = matcherLink.group(2);
+ if (acct != null) {
+ if (acct.startsWith("@"))
+ acct = acct.substring(1);
+ account.setAcct(acct);
+ account.setInstance(matcherLink.group(1));
+ CrossActions.doCrossProfile(context, account);
+ }
}
- }
- } else {
- link = Pattern.compile("(https?://[\\da-z.-]+\\.[a-z.]{2,10})/videos/watch/(\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12})$");
- matcherLink = link.matcher(url);
- if (matcherLink.find()) { //Peertubee video
- Intent intent = new Intent(context, PeertubeActivity.class);
- Bundle b = new Bundle();
- String url = matcherLink.group(1) + "/videos/watch/" + matcherLink.group(2);
- b.putString("peertubeLinkToFetch", url);
- b.putString("peertube_instance", Objects.requireNonNull(matcherLink.group(1)).replace("https://", "").replace("http://", ""));
- b.putString("video_id", matcherLink.group(2));
- intent.putExtras(b);
- context.startActivity(intent);
} else {
- if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://"))
- finalUrl = "http://" + url;
- Helper.openBrowser(context, finalUrl);
- }
-
- }
- }
+ link = Pattern.compile("(https?://[\\da-z.-]+\\.[a-z.]{2,10})/videos/watch/(\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12})$");
+ matcherLink = link.matcher(url);
+ if (matcherLink.find()) { //Peertubee video
+ Intent intent = new Intent(context, PeertubeActivity.class);
+ Bundle b = new Bundle();
+ String url = matcherLink.group(1) + "/videos/watch/" + matcherLink.group(2);
+ b.putString("peertubeLinkToFetch", url);
+ b.putString("peertube_instance", Objects.requireNonNull(matcherLink.group(1)).replace("https://", "").replace("http://", ""));
+ b.putString("video_id", matcherLink.group(2));
+ intent.putExtras(b);
+ context.startActivity(intent);
+ } else {
+ if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://"))
+ finalUrl = "http://" + url;
+ Helper.openBrowser(context, finalUrl);
+ }
- @Override
- public void onLongClick(@NonNull View textView) {
- PopupMenu popup = new PopupMenu(context, textView);
- popup.getMenuInflater()
- .inflate(R.menu.links_popup, popup.getMenu());
- int style;
- if (theme == Helper.THEME_DARK) {
- style = R.style.DialogDark;
- } else if (theme == Helper.THEME_BLACK) {
- style = R.style.DialogBlack;
- } else {
- style = R.style.Dialog;
+ }
}
- popup.setOnMenuItemClickListener(item -> {
- switch (item.getItemId()) {
- case R.id.action_show_link:
- AlertDialog.Builder builder = new AlertDialog.Builder(context, style);
- builder.setMessage(url);
- builder.setTitle(context.getString(R.string.display_full_link));
- builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
- .show();
- break;
- case R.id.action_share_link:
- Intent sendIntent = new Intent(Intent.ACTION_SEND);
- sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
- sendIntent.putExtra(Intent.EXTRA_TEXT, url);
- sendIntent.setType("text/plain");
- context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.share_with)));
- break;
-
- case R.id.action_open_other_app:
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(Uri.parse(url));
- try {
- context.startActivity(intent);
- } catch (Exception e) {
- Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show();
- }
- break;
- case R.id.action_copy_link:
- ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, url);
- if (clipboard != null) {
- clipboard.setPrimaryClip(clip);
- Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
- }
- break;
- case R.id.action_unshorten:
- Thread thread = new Thread() {
- @Override
- public void run() {
- String response = new HttpsConnection(context, null).checkUrl(url);
-
- Handler mainHandler = new Handler(context.getMainLooper());
-
- Runnable myRunnable = () -> {
- AlertDialog.Builder builder1 = new AlertDialog.Builder(context, style);
- if (response != null) {
- builder1.setMessage(context.getString(R.string.redirect_detected, url, response));
- builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> {
- ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, response);
- if (clipboard1 != null) {
- clipboard1.setPrimaryClip(clip1);
- Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
- }
- dialog.dismiss();
- });
- builder1.setNeutralButton(R.string.share_link, (dialog, which) -> {
- Intent sendIntent1 = new Intent(Intent.ACTION_SEND);
- sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
- sendIntent1.putExtra(Intent.EXTRA_TEXT, url);
- sendIntent1.setType("text/plain");
- context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with)));
- dialog.dismiss();
- });
- } else {
- builder1.setMessage(R.string.no_redirect);
- }
- builder1.setTitle(context.getString(R.string.check_redirect));
- builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
- .show();
-
- };
- mainHandler.post(myRunnable);
- }
- };
- thread.start();
- break;
+ @Override
+ public void onLongClick(@NonNull View textView) {
+ PopupMenu popup = new PopupMenu(context, textView);
+ popup.getMenuInflater()
+ .inflate(R.menu.links_popup, popup.getMenu());
+ int style;
+ if (theme == Helper.THEME_DARK) {
+ style = R.style.DialogDark;
+ } else if (theme == Helper.THEME_BLACK) {
+ style = R.style.DialogBlack;
+ } else {
+ style = R.style.Dialog;
}
- return true;
- });
- popup.setOnDismissListener(menu -> BaseActivity.canShowActionMode = true);
- popup.show();
- textView.clearFocus();
- BaseActivity.canShowActionMode = false;
- }
+ popup.setOnMenuItemClickListener(item -> {
+ switch (item.getItemId()) {
+ case R.id.action_show_link:
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, style);
+ builder.setMessage(url);
+ builder.setTitle(context.getString(R.string.display_full_link));
+ builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
+ .show();
+ break;
+ case R.id.action_share_link:
+ Intent sendIntent = new Intent(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
+ sendIntent.putExtra(Intent.EXTRA_TEXT, url);
+ sendIntent.setType("text/plain");
+ context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.share_with)));
+ break;
+
+ case R.id.action_open_other_app:
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ try {
+ context.startActivity(intent);
+ } catch (Exception e) {
+ Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show();
+ }
+ break;
+ case R.id.action_copy_link:
+ ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, url);
+ if (clipboard != null) {
+ clipboard.setPrimaryClip(clip);
+ Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
+ }
+ break;
+ case R.id.action_unshorten:
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ String response = new HttpsConnection(context, null).checkUrl(url);
+
+ Handler mainHandler = new Handler(context.getMainLooper());
+
+ Runnable myRunnable = () -> {
+ AlertDialog.Builder builder1 = new AlertDialog.Builder(context, style);
+ if (response != null) {
+ builder1.setMessage(context.getString(R.string.redirect_detected, url, response));
+ builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> {
+ ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, response);
+ if (clipboard1 != null) {
+ clipboard1.setPrimaryClip(clip1);
+ Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
+ }
+ dialog.dismiss();
+ });
+ builder1.setNeutralButton(R.string.share_link, (dialog, which) -> {
+ Intent sendIntent1 = new Intent(Intent.ACTION_SEND);
+ sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
+ sendIntent1.putExtra(Intent.EXTRA_TEXT, url);
+ sendIntent1.setType("text/plain");
+ context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with)));
+ dialog.dismiss();
+ });
+ } else {
+ builder1.setMessage(R.string.no_redirect);
+ }
+ builder1.setTitle(context.getString(R