Fix notifications

This commit is contained in:
M M Arif 2023-10-09 13:07:54 +05:00
parent 74ba524a10
commit 87b595b262
7 changed files with 130 additions and 308 deletions

View File

@ -110,7 +110,7 @@ dependencies {
implementation 'com.github.chrisvest:stormpot:2.4.2'
implementation 'androidx.browser:browser:1.6.0'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation('org.codeberg.gitnex:tea4j-autodeploy:65f700d036') {
implementation('org.codeberg.gitnex:tea4j-autodeploy:89e78b1585') {
exclude module: 'org.apache.oltu.oauth2.common'
}
implementation 'io.github.amrdeveloper:codeview:1.3.8'

View File

@ -1,17 +1,12 @@
package org.mian.gitnex.activities;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.NumberPicker;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.skydoves.colorpickerview.ColorPickerDialog;
import com.skydoves.colorpickerview.flag.BubbleFlag;
import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener;
import org.mian.gitnex.R;
import org.mian.gitnex.databinding.ActivitySettingsNotificationsBinding;
import org.mian.gitnex.fragments.SettingsFragment;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.SnackBar;
import org.mian.gitnex.notifications.Notifications;
@ -22,6 +17,8 @@ import org.mian.gitnex.notifications.Notifications;
public class SettingsNotificationsActivity extends BaseActivity {
private ActivitySettingsNotificationsBinding viewBinding;
private static String[] pollingDelayList;
private static int pollingDelayListSelectedChoice = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -33,31 +30,11 @@ public class SettingsNotificationsActivity extends BaseActivity {
viewBinding.topAppBar.setNavigationOnClickListener(v -> finish());
viewBinding.pollingDelaySelected.setText(
String.format(
getString(R.string.pollingDelaySelectedText),
tinyDB.getInt("pollingDelayMinutes", Constants.defaultPollingDelay)));
viewBinding.chooseColorState.setCardBackgroundColor(
tinyDB.getInt("notificationsLightColor", Color.GREEN));
viewBinding.enableNotificationsMode.setChecked(
tinyDB.getBoolean("notificationsEnabled", true));
viewBinding.enableLightsMode.setChecked(
tinyDB.getBoolean("notificationsEnableLights", false));
viewBinding.enableVibrationMode.setChecked(
tinyDB.getBoolean("notificationsEnableVibration", false));
if (!viewBinding.enableNotificationsMode.isChecked()) {
AppUtil.setMultiVisibility(
View.GONE,
viewBinding.chooseColorFrame,
viewBinding.enableLightsFrame,
viewBinding.enableVibrationFrame,
viewBinding.pollingDelayFrame);
}
if (!viewBinding.enableLightsMode.isChecked()) {
viewBinding.chooseColorFrame.setVisibility(View.GONE);
AppUtil.setMultiVisibility(View.GONE, viewBinding.pollingDelayFrame);
}
viewBinding.enableNotificationsMode.setOnCheckedChangeListener(
@ -66,20 +43,10 @@ public class SettingsNotificationsActivity extends BaseActivity {
if (isChecked) {
Notifications.startWorker(ctx);
AppUtil.setMultiVisibility(
View.VISIBLE,
viewBinding.chooseColorFrame,
viewBinding.enableLightsFrame,
viewBinding.enableVibrationFrame,
viewBinding.pollingDelayFrame);
AppUtil.setMultiVisibility(View.VISIBLE, viewBinding.pollingDelayFrame);
} else {
Notifications.stopWorker(ctx);
AppUtil.setMultiVisibility(
View.GONE,
viewBinding.chooseColorFrame,
viewBinding.enableLightsFrame,
viewBinding.enableVibrationFrame,
viewBinding.pollingDelayFrame);
AppUtil.setMultiVisibility(View.GONE, viewBinding.pollingDelayFrame);
}
SnackBar.success(
@ -93,109 +60,39 @@ public class SettingsNotificationsActivity extends BaseActivity {
!viewBinding.enableNotificationsMode.isChecked()));
// polling delay
viewBinding.pollingDelayFrame.setOnClickListener(
v -> {
NumberPicker numberPicker = new NumberPicker(ctx);
numberPicker.setMinValue(Constants.minimumPollingDelay);
numberPicker.setMaxValue(Constants.maximumPollingDelay);
numberPicker.setValue(
tinyDB.getInt("pollingDelayMinutes", Constants.defaultPollingDelay));
numberPicker.setWrapSelectorWheel(true);
pollingDelayList = getResources().getStringArray(R.array.notificationsPollingDelay);
pollingDelayListSelectedChoice = tinyDB.getInt("notificationsPollingDelayId");
viewBinding.pollingDelaySelected.setText(pollingDelayList[pollingDelayListSelectedChoice]);
viewBinding.pollingDelayFrame.setOnClickListener(
view -> {
MaterialAlertDialogBuilder materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.pollingDelayDialogHeaderText)
.setMessage(
getString(R.string.pollingDelayDialogDescriptionText))
.setCancelable(true)
.setNeutralButton(
R.string.cancelButton,
(dialog, which) -> dialog.dismiss())
.setPositiveButton(
getString(R.string.okButton),
(dialog, which) -> {
tinyDB.putInt(
"pollingDelayMinutes",
numberPicker.getValue());
.setSingleChoiceItems(
pollingDelayList,
pollingDelayListSelectedChoice,
(dialogInterfaceColor, i) -> {
pollingDelayListSelectedChoice = i;
viewBinding.pollingDelaySelected.setText(
pollingDelayList[
pollingDelayListSelectedChoice]);
tinyDB.putInt("notificationsPollingDelayId", i);
Notifications.stopWorker(ctx);
Notifications.startWorker(ctx);
viewBinding.pollingDelaySelected.setText(
String.format(
getString(
R.string
.pollingDelaySelectedText),
numberPicker.getValue()));
SettingsFragment.refreshParent = true;
this.recreate();
this.overridePendingTransition(0, 0);
dialogInterfaceColor.dismiss();
SnackBar.success(
ctx,
findViewById(android.R.id.content),
getString(R.string.settingsSave));
});
materialAlertDialogBuilder.setView(numberPicker);
materialAlertDialogBuilder.create().show();
});
// lights switcher
viewBinding.enableLightsMode.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (!isChecked) {
viewBinding.chooseColorFrame.setVisibility(View.GONE);
} else {
viewBinding.chooseColorFrame.setVisibility(View.VISIBLE);
}
tinyDB.putBoolean("notificationsEnableLights", isChecked);
SnackBar.success(
ctx,
findViewById(android.R.id.content),
getString(R.string.settingsSave));
});
viewBinding.enableLightsFrame.setOnClickListener(
v ->
viewBinding.enableLightsMode.setChecked(
!viewBinding.enableLightsMode.isChecked()));
// lights color chooser
viewBinding.chooseColorFrame.setOnClickListener(
v -> {
ColorPickerDialog.Builder builder =
new ColorPickerDialog.Builder(this)
.setPreferenceName("colorPickerDialogLabels")
.setPositiveButton(
getString(R.string.okButton),
(ColorEnvelopeListener)
(envelope, clicked) -> {
tinyDB.putInt(
"notificationsLightColor",
envelope.getColor());
viewBinding.chooseColorState
.setCardBackgroundColor(
envelope.getColor());
})
.attachAlphaSlideBar(true)
.attachBrightnessSlideBar(true)
.setBottomSpace(16);
builder.getColorPickerView().setFlagView(new BubbleFlag(this));
builder.getColorPickerView().setLifecycleOwner(this);
builder.show();
});
// vibration switcher
viewBinding.enableVibrationMode.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
tinyDB.putBoolean("notificationsEnableVibration", isChecked);
SnackBar.success(
ctx,
findViewById(android.R.id.content),
getString(R.string.settingsSave));
});
viewBinding.enableVibrationFrame.setOnClickListener(
v ->
viewBinding.enableVibrationMode.setChecked(
!viewBinding.enableVibrationMode.isChecked()));
}
}

View File

@ -3,7 +3,6 @@ package org.mian.gitnex.notifications;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
@ -34,16 +33,11 @@ public class Notifications {
public static void createChannels(Context context) {
TinyDB tinyDB = TinyDB.getInstance(context);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Delete old notification channels
notificationManager.deleteNotificationChannel(
context.getPackageName()); // TODO Can be removed in future versions
// Create new notification channels
NotificationChannel mainChannel =
new NotificationChannel(
@ -53,20 +47,6 @@ public class Notifications {
mainChannel.setDescription(
context.getString(R.string.mainNotificationChannelDescription));
if (tinyDB.getBoolean("notificationsEnableVibration", true)) {
mainChannel.setVibrationPattern(Constants.defaultVibrationPattern);
mainChannel.enableVibration(true);
} else {
mainChannel.enableVibration(false);
}
if (tinyDB.getBoolean("notificationsEnableLights", true)) {
mainChannel.setLightColor(tinyDB.getInt("notificationsLightColor", Color.GREEN));
mainChannel.enableLights(true);
} else {
mainChannel.enableLights(false);
}
NotificationChannel downloadChannel =
new NotificationChannel(
Constants.downloadNotificationChannelId,
@ -89,6 +69,19 @@ public class Notifications {
TinyDB tinyDB = TinyDB.getInstance(context);
int delay;
if (tinyDB.getInt("notificationsPollingDelayId") == 0) {
delay = 15;
} else if (tinyDB.getInt("notificationsPollingDelayId") == 1) {
delay = 30;
} else if (tinyDB.getInt("notificationsPollingDelayId") == 2) {
delay = 45;
} else if (tinyDB.getInt("notificationsPollingDelayId") == 3) {
delay = 60;
} else {
delay = Constants.defaultPollingDelay;
}
if (tinyDB.getBoolean("notificationsEnabled", true)) {
Constraints.Builder constraints =
@ -98,20 +91,11 @@ public class Notifications {
.setRequiresStorageNotLow(false)
.setRequiresCharging(false);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
constraints.setRequiresDeviceIdle(false);
}
int pollingDelayMinutes =
Math.max(
tinyDB.getInt("pollingDelayMinutes", Constants.defaultPollingDelay),
15);
constraints.setRequiresDeviceIdle(false);
PeriodicWorkRequest periodicWorkRequest =
new PeriodicWorkRequest.Builder(
NotificationsWorker.class,
pollingDelayMinutes,
TimeUnit.MINUTES)
NotificationsWorker.class, delay, TimeUnit.MINUTES)
.setConstraints(constraints.build())
.addTag(Constants.notificationsWorkerId)
.build();
@ -119,7 +103,7 @@ public class Notifications {
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
Constants.notificationsWorkerId,
ExistingPeriodicWorkPolicy.KEEP,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
periodicWorkRequest);
}
}

View File

@ -1,17 +1,23 @@
package org.mian.gitnex.notifications;
import android.Manifest;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.content.pm.PackageManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.TaskStackBuilder;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.util.Date;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -22,7 +28,6 @@ import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.database.api.BaseApi;
import org.mian.gitnex.database.api.UserAccountsApi;
import org.mian.gitnex.database.models.UserAccount;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.Version;
@ -35,7 +40,6 @@ import retrofit2.Response;
public class NotificationsWorker extends Worker {
private final Context context;
private final TinyDB tinyDB;
private final Map<UserAccount, Map<String, String>> userAccounts;
public NotificationsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
@ -45,9 +49,27 @@ public class NotificationsWorker extends Worker {
UserAccountsApi userAccountsApi = BaseApi.getInstance(context, UserAccountsApi.class);
this.context = context;
this.tinyDB = TinyDB.getInstance(context);
TinyDB tinyDB = TinyDB.getInstance(context);
assert userAccountsApi != null;
this.userAccounts = new HashMap<>(userAccountsApi.getCount());
int delay;
if (tinyDB.getInt("notificationsPollingDelayId") == 0) {
delay = 15;
} else if (tinyDB.getInt("notificationsPollingDelayId") == 1) {
delay = 30;
} else if (tinyDB.getInt("notificationsPollingDelayId") == 2) {
delay = 45;
} else if (tinyDB.getInt("notificationsPollingDelayId") == 3) {
delay = 60;
} else {
delay = Constants.defaultPollingDelay;
}
ZonedDateTime zdt = ZonedDateTime.now();
zdt = zdt.minusMinutes(delay);
String previousTimestamp = String.valueOf(zdt.toOffsetDateTime());
for (UserAccount userAccount : userAccountsApi.loggedInUserAccounts()) {
// We do also accept empty values, since the server version was not saved properly in
@ -57,8 +79,7 @@ public class NotificationsWorker extends Worker {
|| new Version(userAccount.getServerVersion()).higherOrEqual("1.12.3")) {
Map<String, String> userAccountParameters = new HashMap<>();
userAccountParameters.put(
"previousTimestamp", AppUtil.getTimestampFromDate(context, new Date()));
userAccountParameters.put("previousTimestamp", previousTimestamp);
userAccounts.put(userAccount, userAccountParameters);
}
@ -67,56 +88,31 @@ public class NotificationsWorker extends Worker {
@NonNull @Override
public Result doWork() {
pollingLoops();
startPolling();
return Result.success();
}
/** Used to bypass the 15-minute limit of {@code WorkManager}. */
private void pollingLoops() {
int notificationLoops =
tinyDB.getInt("pollingDelayMinutes", Constants.defaultPollingDelay) < 15
? Math.min(
15
- tinyDB.getInt(
"pollingDelayMinutes",
Constants.defaultPollingDelay),
10)
: 1;
for (int i = 0; i < notificationLoops; i++) {
long startPollingTime = System.currentTimeMillis();
startPolling();
try {
if (notificationLoops > 1 && i < (notificationLoops - 1)) {
Thread.sleep(60000 - (System.currentTimeMillis() - startPollingTime));
}
} catch (InterruptedException ignored) {
}
}
}
private void startPolling() {
for (UserAccount userAccount : userAccounts.keySet()) {
Map<String, String> userAccountParameters = userAccounts.get(userAccount);
assert userAccountParameters != null;
try {
assert userAccountParameters != null;
Call<List<NotificationThread>> call =
RetrofitClient.getApiInterface(
context,
userAccount.getInstanceUrl(),
userAccount.getToken(),
"token " + userAccount.getToken(),
null)
.notifyGetList(
.notifyGetList2(
false,
List.of("unread"),
null,
new Date(userAccountParameters.get("previousTimestamp")),
userAccountParameters.get("previousTimestamp"),
null,
null,
1);
1,
25);
Response<List<NotificationThread>> response = call.execute();
@ -125,8 +121,6 @@ public class NotificationsWorker extends Worker {
if (!notificationThreads.isEmpty()) {
sendNotifications(userAccount, notificationThreads);
}
userAccountParameters.put(
"previousTimestamp", AppUtil.getTimestampFromDate(context, new Date()));
}
} catch (Exception ignored) {
}
@ -153,10 +147,37 @@ public class NotificationsWorker extends Worker {
.setSmallIcon(R.drawable.gitnex_transparent)
.setGroup(userAccount.getUserName())
.setGroupSummary(true)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
if (ActivityCompat.checkSelfPermission(
getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
MaterialAlertDialogBuilder materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.pageTitleNotifications)
.setMessage(context.getString(R.string.openAppSettings))
.setNeutralButton(
R.string.cancelButton, (dialog, which) -> dialog.dismiss())
.setPositiveButton(
R.string.isOpen,
(dialog, which) -> {
Intent intent =
new Intent(
Settings
.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri =
Uri.fromParts(
"package", context.getPackageName(), null);
intent.setData(uri);
context.startActivity(intent);
});
materialAlertDialogBuilder.create().show();
return;
}
notificationManagerCompat.notify(userAccount.getAccountId(), summaryNotification);
for (NotificationThread notificationThread : notificationThreads) {
@ -189,25 +210,12 @@ public class NotificationsWorker extends Worker {
private NotificationCompat.Builder getBaseNotificationBuilder() {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, Constants.mainNotificationChannelId)
.setSmallIcon(R.drawable.gitnex_transparent)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);
if (tinyDB.getBoolean("notificationsEnableLights", true)) {
builder.setLights(tinyDB.getInt("notificationsLightColor", Color.GREEN), 1500, 1500);
}
if (tinyDB.getBoolean("notificationsEnableVibration", true)) {
builder.setVibrate(Constants.defaultVibrationPattern);
} else {
builder.setVibrate(null);
}
return builder;
return new NotificationCompat.Builder(context, Constants.mainNotificationChannelId)
.setSmallIcon(R.drawable.gitnex_transparent)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);
}
private PendingIntent getPendingIntent(@NonNull UserAccount userAccount) {
@ -218,7 +226,10 @@ public class NotificationsWorker extends Worker {
intent.putExtra("switchAccountId", userAccount.getAccountId());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
return PendingIntent.getActivity(
context, userAccount.getAccountId(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addNextIntentWithParentStack(intent);
return stackBuilder.getPendingIntent(
1, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
}

View File

@ -89,93 +89,12 @@
android:id="@+id/pollingDelaySelected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pollingDelaySelectedText"
android:text="@string/pollingDelay15Minutes"
android:textColor="?attr/selectedTextColor"
android:textSize="@dimen/dimen16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/enableLightsFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen24dp"
android:orientation="horizontal">
<TextView
android:id="@+id/enableLightsHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight=".90"
android:text="@string/enableLightsHeaderText"
android:textColor="?attr/primaryTextColor"
android:layout_marginTop="@dimen/dimen4dp"
android:textSize="@dimen/dimen18sp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/enableLightsMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/enableLightsHeaderText"
android:layout_weight=".10" />
</LinearLayout>
<LinearLayout
android:id="@+id/chooseColorFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen24dp"
android:orientation="horizontal">
<TextView
android:id="@+id/chooseColorHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight=".90"
android:text="@string/chooseColorSelectorHeader"
android:textColor="?attr/primaryTextColor"
android:layout_marginTop="@dimen/dimen4dp"
android:textSize="@dimen/dimen18sp" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/chooseColorState"
style="?attr/materialCardViewFilledStyle"
android:layout_width="@dimen/dimen24dp"
android:layout_height="@dimen/dimen32dp"
android:layout_weight=".10"
android:gravity="end"
app:cardCornerRadius="@dimen/dimen16dp"
app:cardElevation="@dimen/dimen0dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/enableVibrationFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen24dp"
android:orientation="horizontal">
<TextView
android:id="@+id/enableVibrationHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight=".90"
android:text="@string/enableVibrationHeaderText"
android:textColor="?attr/primaryTextColor"
android:layout_marginTop="@dimen/dimen4dp"
android:textSize="@dimen/dimen18sp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/enableVibrationMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/enableVibrationHeaderText"
android:layout_weight=".10" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -119,6 +119,13 @@
<item>@string/none</item>
</string-array>
<string-array name="notificationsPollingDelay">
<item>@string/pollingDelay15Minutes</item>
<item>@string/pollingDelay30Minutes</item>
<item>@string/pollingDelay45Minutes</item>
<item>@string/pollingDelay1Hour</item>
</string-array>
<string-array name="licenses">
<item>0BSD</item>
<item>AAL</item>

View File

@ -666,14 +666,17 @@
<string name="pageTitleNotifications">Notifications</string>
<string name="noDataNotifications">All caught up 🚀</string>
<string name="notificationsPollingHeaderText">Notifications Polling Delay</string>
<string name="pollingDelaySelectedText">%d Minutes</string>
<string name="pollingDelay15Minutes">15 Minutes</string>
<string name="pollingDelay30Minutes">30 Minutes</string>
<string name="pollingDelay45Minutes">45 Minutes</string>
<string name="pollingDelay1Hour">1 Hour</string>
<string name="pollingDelayDialogHeaderText">Select Polling Delay</string>
<string name="pollingDelayDialogDescriptionText">Choose a minutely delay in which GitNex tries to poll new notifications</string>
<string name="markAsRead">Mark Read</string>
<string name="markAsUnread">Mark Unread</string>
<string name="pinNotification">Pin</string>
<string name="markedNotificationsAsRead">Successfully marked all notifications as read</string>
<string name="notificationsHintText">Polling delay, light, vibration</string>
<string name="notificationsHintText">Polling delay</string>
<string name="enableNotificationsHeaderText">Enable Notifications</string>
<string name="enableLightsHeaderText">Enable Light</string>
<string name="enableVibrationHeaderText">Enable Vibration</string>
@ -687,6 +690,7 @@
<item quantity="one">You have %s new notification</item>
<item quantity="other">You have %s new notifications</item>
</plurals>
<string name="openAppSettings">To receive notifications, you must enable notifications for GitNex. Tap Open to access your phone settings and enable notifications.</string>
<string name="isRead">Read</string>
<string name="isUnread">Unread</string>