In-app notification options #1023
8 changed files with 282 additions and 12 deletions
|
@ -11,6 +11,7 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.ClipData;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -36,6 +37,7 @@ import android.text.style.TypefaceSpan;
|
|||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Menu;
|
||||
import android.view.WindowManager;
|
||||
|
@ -75,7 +77,9 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
@ -98,6 +102,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.java_websocket.client.WebSocketClient;
|
||||
import org.java_websocket.handshake.ServerHandshake;
|
||||
|
@ -142,6 +147,7 @@ import io.lbry.browser.listener.FilePickerListener;
|
|||
import io.lbry.browser.listener.PIPModeListener;
|
||||
import io.lbry.browser.listener.ScreenOrientationListener;
|
||||
import io.lbry.browser.listener.SdkStatusListener;
|
||||
import io.lbry.browser.listener.SelectionModeListener;
|
||||
import io.lbry.browser.listener.StoragePermissionListener;
|
||||
import io.lbry.browser.listener.WalletBalanceListener;
|
||||
import io.lbry.browser.model.Claim;
|
||||
|
@ -165,6 +171,7 @@ import io.lbry.browser.tasks.lbryinc.FetchRewardsTask;
|
|||
import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
|
||||
import io.lbry.browser.tasks.MergeSubscriptionsTask;
|
||||
import io.lbry.browser.tasks.claim.ResolveTask;
|
||||
import io.lbry.browser.tasks.lbryinc.NotificationDeleteTask;
|
||||
import io.lbry.browser.tasks.lbryinc.NotificationListTask;
|
||||
import io.lbry.browser.tasks.lbryinc.NotificationUpdateTask;
|
||||
import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask;
|
||||
|
@ -209,7 +216,9 @@ import lombok.Setter;
|
|||
import lombok.SneakyThrows;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements SdkStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public class MainActivity extends AppCompatActivity implements SdkStatusListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
ActionMode.Callback, SelectionModeListener {
|
||||
private static final String CHANNEL_ID_PLAYBACK = "io.lbry.browser.LBRY_PLAYBACK_CHANNEL";
|
||||
private static final int PLAYBACK_NOTIFICATION_ID = 3;
|
||||
private static final String SPECIAL_URL_PREFIX = "lbry://?";
|
||||
|
@ -244,6 +253,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
public static boolean startingPermissionRequest = false;
|
||||
public static boolean startingSignInFlowActivity = false;
|
||||
|
||||
private ActionMode actionMode;
|
||||
private BillingClient billingClient;
|
||||
@Getter
|
||||
private boolean enteringPIPMode = false;
|
||||
|
@ -366,6 +376,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
private MediaSessionCompat mediaSession;
|
||||
private boolean receivedStopService;
|
||||
private ActionBarDrawerToggle toggle;
|
||||
private SwipeRefreshLayout notificationsSwipeContainer;
|
||||
private SyncSetTask syncSetTask = null;
|
||||
private List<WalletSync> pendingSyncSetQueue;
|
||||
@Getter
|
||||
|
@ -581,6 +592,16 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
}
|
||||
});
|
||||
|
||||
notificationsSwipeContainer = findViewById(R.id.notifications_list_swipe_container);
|
||||
notificationsSwipeContainer.setColorSchemeResources(R.color.nextLbryGreen);
|
||||
notificationsSwipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
notificationsSwipeContainer.setRefreshing(true);
|
||||
loadRemoteNotifications(false);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
|
@ -1874,6 +1895,103 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
this.actionMode = mode;
|
||||
if (isDarkMode()) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
|
||||
}
|
||||
|
||||
actionMode.getMenuInflater().inflate(R.menu.menu_notification, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (R.id.action_delete == item.getItemId()) {
|
||||
if (notificationListAdapter != null && notificationListAdapter.getSelectedCount() > 0) {
|
||||
|
||||
final List<LbryNotification> selectedNotifications = new ArrayList<>(notificationListAdapter.getSelectedItems());
|
||||
String message = getResources().getQuantityString(R.plurals.confirm_delete_notifications, selectedNotifications.size());
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this).
|
||||
setTitle(R.string.delete_selection).
|
||||
setMessage(message)
|
||||
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
handleDeleteSelectedNotifications(selectedNotifications);
|
||||
}
|
||||
}).setNegativeButton(R.string.no, null);
|
||||
builder.show();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
if (notificationListAdapter != null) {
|
||||
notificationListAdapter.clearSelectedItems();
|
||||
notificationListAdapter.setInSelectionMode(false);
|
||||
notificationListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
if (isDarkMode()) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
this.actionMode = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnterSelectionMode() {
|
||||
startSupportActionMode(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExitSelectionMode() {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemSelectionToggled() {
|
||||
if (actionMode != null) {
|
||||
actionMode.setTitle(notificationListAdapter != null ? String.valueOf(notificationListAdapter.getSelectedCount()) : "");
|
||||
actionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeleteSelectedNotifications(List<LbryNotification> notifications) {
|
||||
List<Long> remoteIds = new ArrayList<>();
|
||||
for (LbryNotification notification : notifications) {
|
||||
remoteIds.add(notification.getRemoteId());
|
||||
}
|
||||
(new AsyncTask<Void, Void, Void>() {
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||
DatabaseHelper.deleteNotifications(notifications, db);
|
||||
} catch (Exception ex) {
|
||||
// pass
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
new NotificationDeleteTask(remoteIds).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
if (notificationListAdapter != null) {
|
||||
notificationListAdapter.removeNotifications(notifications);
|
||||
}
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerNotificationDescriptionAdapter implements PlayerNotificationManager.MediaDescriptionAdapter {
|
||||
|
||||
@Override
|
||||
|
@ -3390,6 +3508,10 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
if (markRead && findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) {
|
||||
markNotificationsRead();
|
||||
}
|
||||
|
||||
if (notificationsSwipeContainer != null) {
|
||||
notificationsSwipeContainer.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -3397,6 +3519,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
// pass
|
||||
Log.e(TAG, "error loading remote notifications", exception);
|
||||
loadLocalNotifications();
|
||||
if (notificationsSwipeContainer != null) {
|
||||
notificationsSwipeContainer.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
@ -3426,6 +3551,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
|||
|
||||
if (notificationListAdapter == null) {
|
||||
notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this);
|
||||
notificationListAdapter.setSelectionModeListener(MainActivity.this);
|
||||
((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter);
|
||||
} else {
|
||||
notificationListAdapter.addNotifications(notifications);
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
|
@ -24,6 +25,7 @@ import java.util.List;
|
|||
import java.util.TimeZone;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.listener.SelectionModeListener;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.model.lbryinc.LbryNotification;
|
||||
import io.lbry.browser.ui.controls.SolidIconView;
|
||||
|
@ -43,15 +45,19 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
|
|||
|
||||
private Context context;
|
||||
private List<LbryNotification> items;
|
||||
private List<LbryNotification> selectedItems;
|
||||
@Setter
|
||||
private NotificationClickListener clickListener;
|
||||
@Getter
|
||||
@Setter
|
||||
private int customizeMode;
|
||||
private boolean inSelectionMode;
|
||||
@Setter
|
||||
private SelectionModeListener selectionModeListener;
|
||||
|
||||
public NotificationListAdapter(List<LbryNotification> notifications, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>(notifications);
|
||||
this.selectedItems = new ArrayList<>();
|
||||
Collections.sort(items, Collections.reverseOrder(new LbryNotification()));
|
||||
}
|
||||
|
||||
|
@ -62,6 +68,7 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
|
|||
protected TextView timeView;
|
||||
protected SolidIconView iconView;
|
||||
protected ImageView thumbnailView;
|
||||
protected View selectedOverlayView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
layoutView = v.findViewById(R.id.notification_layout);
|
||||
|
@ -70,12 +77,25 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
|
|||
timeView = v.findViewById(R.id.notification_time);
|
||||
iconView = v.findViewById(R.id.notification_icon);
|
||||
thumbnailView = v.findViewById(R.id.notification_author_thumbnail);
|
||||
selectedOverlayView = v.findViewById(R.id.notification_selected_overlay);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
public List<LbryNotification> getSelectedItems() {
|
||||
return this.selectedItems;
|
||||
}
|
||||
public int getSelectedCount() {
|
||||
return selectedItems != null ? selectedItems.size() : 0;
|
||||
}
|
||||
public void clearSelectedItems() {
|
||||
this.selectedItems.clear();
|
||||
}
|
||||
public boolean isNotificationSelected(LbryNotification notification) {
|
||||
return selectedItems.contains(notification);
|
||||
}
|
||||
|
||||
public void insertNotification(LbryNotification notification, int index) {
|
||||
if (!items.contains(notification)) {
|
||||
|
@ -90,6 +110,12 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
|
|||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
public void removeNotifications(List<LbryNotification> notifications) {
|
||||
for (LbryNotification notification : notifications) {
|
||||
items.remove(notification);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<String> getAuthorUrls() {
|
||||
List<String> urls = new ArrayList<>();
|
||||
|
@ -158,9 +184,8 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
|
|||
@Override
|
||||
public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) {
|
||||
LbryNotification notification = items.get(position);
|
||||
|
||||
|
||||
vh.layoutView.setBackgroundColor(ContextCompat.getColor(context, notification.isSeen() ? android.R.color.transparent : R.color.nextLbryGreenSemiTransparent));
|
||||
vh.selectedOverlayView.setVisibility(isNotificationSelected(notification) ? View.VISIBLE : View.GONE);
|
||||
|
||||
vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE);
|
||||
vh.titleView.setText(notification.getTitle());
|
||||
|
@ -181,11 +206,49 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
|
|||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (clickListener != null) {
|
||||
clickListener.onNotificationClicked(notification);
|
||||
if (inSelectionMode) {
|
||||
toggleSelectedNotification(notification);
|
||||
} else {
|
||||
if (clickListener != null) {
|
||||
clickListener.onNotificationClicked(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (!inSelectionMode) {
|
||||
inSelectionMode = true;
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onEnterSelectionMode();
|
||||
}
|
||||
}
|
||||
toggleSelectedNotification(notification);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleSelectedNotification(LbryNotification notification) {
|
||||
if (selectedItems.contains(notification)) {
|
||||
selectedItems.remove(notification);
|
||||
} else {
|
||||
selectedItems.add(notification);
|
||||
}
|
||||
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onItemSelectionToggled();
|
||||
}
|
||||
|
||||
if (selectedItems.size() == 0) {
|
||||
inSelectionMode = false;
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onExitSelectionMode();
|
||||
}
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private long getLocalNotificationTime(LbryNotification notification) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase;
|
|||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.SQLInput;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
@ -372,6 +373,20 @@ public class DatabaseHelper extends SQLiteOpenHelper {
|
|||
}
|
||||
return notifications;
|
||||
}
|
||||
public static void deleteNotifications(List<LbryNotification> notifications, SQLiteDatabase db) {
|
||||
StringBuilder sb = new StringBuilder("DELETE FROM notifications WHERE remote_id IN (");
|
||||
List<Object> remoteIds = new ArrayList<>();
|
||||
String delim = "";
|
||||
for (int i = 0; i < notifications.size(); i++) {
|
||||
remoteIds.add(String.valueOf(notifications.get(i).getRemoteId()));
|
||||
sb.append(delim).append("?");
|
||||
delim = ",";
|
||||
}
|
||||
sb.append(")");
|
||||
|
||||
String sql = sb.toString();
|
||||
db.execSQL(sql, remoteIds.toArray());
|
||||
}
|
||||
public static int getUnreadNotificationsCount(SQLiteDatabase db) {
|
||||
int count = 0;
|
||||
Cursor cursor = null;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package io.lbry.browser.tasks.lbryinc;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.lbry.browser.exceptions.LbryioRequestException;
|
||||
import io.lbry.browser.exceptions.LbryioResponseException;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.Lbryio;
|
||||
|
||||
public class NotificationDeleteTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private List<Long> ids;
|
||||
|
||||
public NotificationDeleteTask(List<Long> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Map<String, String> options = new HashMap<>();
|
||||
options.put("notification_ids", Helper.joinL(ids, ","));
|
||||
|
||||
try {
|
||||
Object result = Lbryio.parseResponse(Lbryio.call("notification", "delete", options, null));
|
||||
return "ok".equalsIgnoreCase(result.toString());
|
||||
} catch (LbryioResponseException | LbryioRequestException ex) {
|
||||
// pass
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -71,13 +71,18 @@
|
|||
android:textSize="16sp"
|
||||
android:textAlignment="center" />
|
||||
</LinearLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/notifications_list"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/notifications_list_swipe_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="8dp"
|
||||
android:clipToPadding="false"
|
||||
/>
|
||||
android:layout_height="match_parent">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/notifications_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="8dp"
|
||||
android:clipToPadding="false"
|
||||
/>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<include layout="@layout/floating_wallet_balance" />
|
||||
|
|
|
@ -28,6 +28,20 @@
|
|||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:visibility="invisible" />
|
||||
<RelativeLayout
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="@drawable/bg_channel_overlay_icon"
|
||||
android:id="@+id/notification_selected_overlay"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@drawable/ic_check"
|
||||
android:tint="@color/nextLbryGreen" />
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
|
|
10
app/src/main/res/menu/menu_notification.xml
Normal file
10
app/src/main/res/menu/menu_notification.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:title="@string/delete"
|
||||
app:iconTint="@color/actionBarForeground"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -628,6 +628,10 @@
|
|||
|
||||
<!-- Notifications -->
|
||||
<string name="no_notifications">It\'s quiet here! New notifications will be displayed when you receive them.</string>
|
||||
<plurals name="confirm_delete_notifications">
|
||||
<item quantity="one">Are you sure you want to remove the selected notification?</item>
|
||||
<item quantity="other">Are you sure you want to remove the selected notifications?</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<string name="fa_gift" translatable="false"></string>
|
||||
|
|
Loading…
Reference in a new issue