diff --git a/.editorconfig b/.editorconfig
index 44c3cd2c..3a04062e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,7 +9,6 @@ max_line_length = 150
[*.java]
indent_style = tab
-max_line_length = 220
line_comment = //
block_comment_start = /*
block_comment = *
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 5c9943cc..b0dfb6a7 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -29,6 +29,7 @@
+
@@ -36,9 +37,13 @@
+
+
+
+
+
-
diff --git a/app/build.gradle b/app/build.gradle
index e3ed9bd2..38fa983f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -37,7 +37,8 @@ configurations {
dependencies {
def lifecycle_version = "2.3.0-alpha05"
- def markwon_version = '4.4.0'
+ def markwon_version = "4.4.0"
+ def work_version = "2.3.4"
def acra = "5.5.0"
implementation fileTree(include: ['*.jar'], dir: 'libs')
@@ -80,6 +81,7 @@ dependencies {
implementation "com.hendraanggrian.appcompat:socialview-commons:0.2"
implementation "com.github.HamidrezaAmz:BreadcrumbsView:0.2.9"
implementation "commons-io:commons-io:20030203.000550"
+ implementation "org.apache.commons:commons-lang3:3.10"
implementation "com.github.chrisbanes:PhotoView:2.3.0"
implementation "com.github.barteksc:android-pdf-viewer:3.2.0-beta.1"
implementation "ch.acra:acra-mail:$acra"
@@ -87,6 +89,8 @@ dependencies {
implementation "ch.acra:acra-notification:$acra"
implementation "androidx.room:room-runtime:2.2.5"
annotationProcessor "androidx.room:room-compiler:2.2.5"
+ implementation "androidx.work:work-runtime:$work_version"
implementation "com.eightbitlab:blurview:1.6.3"
implementation "io.mikael:urlbuilder:2.0.9"
+
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3fc4baac..8518fe2c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
package="org.mian.gitnex">
+
+
+
diff --git a/app/src/main/java/org/mian/gitnex/actions/NotificationsActions.java b/app/src/main/java/org/mian/gitnex/actions/NotificationsActions.java
new file mode 100644
index 00000000..35239248
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/actions/NotificationsActions.java
@@ -0,0 +1,59 @@
+package org.mian.gitnex.actions;
+
+import android.content.Context;
+import org.mian.gitnex.clients.RetrofitClient;
+import org.mian.gitnex.helpers.AppUtil;
+import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.models.NotificationThread;
+import java.io.IOException;
+import java.util.Date;
+import okhttp3.ResponseBody;
+import retrofit2.Call;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationsActions {
+
+ public enum NotificationStatus {READ, UNREAD, PINNED}
+
+ private TinyDB tinyDB;
+ private Context context;
+ private String instanceUrl;
+ private String instanceToken;
+
+ public NotificationsActions(Context context) {
+
+ this.context = context;
+ this.tinyDB = new TinyDB(context);
+
+ String loginUid = tinyDB.getString("loginUid");
+
+ instanceUrl = tinyDB.getString("instanceUrl");
+ instanceToken = "token " + tinyDB.getString(loginUid + "-token");
+
+ }
+
+ public void setNotificationStatus(NotificationThread notificationThread, NotificationStatus notificationStatus) throws IOException {
+
+ Call call = RetrofitClient.getInstance(instanceUrl, context).getApiInterface()
+ .markNotificationThreadAsRead(instanceToken, notificationThread.getId(), notificationStatus.name());
+
+ if(!call.execute().isSuccessful()) {
+
+ throw new IllegalStateException();
+ }
+ }
+
+ public boolean setAllNotificationsRead(Date date) throws IOException {
+
+ Call call = RetrofitClient.getInstance(instanceUrl, context).getApiInterface()
+ .markNotificationThreadsAsRead(instanceToken, AppUtil.getTimestampFromDate(context, date), true,
+ new String[]{"unread", "pinned"}, "read");
+
+ return call.execute().isSuccessful();
+
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java b/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java
index 5cc309f3..2d4059d6 100644
--- a/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java
@@ -1,5 +1,6 @@
package org.mian.gitnex.activities;
+import android.content.Context;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import org.acra.ACRA;
@@ -14,6 +15,7 @@ import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.FontsOverride;
import org.mian.gitnex.helpers.TimeHelper;
import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.notifications.NotificationsMaster;
/**
* Author M M Arif
@@ -26,10 +28,13 @@ import org.mian.gitnex.helpers.TinyDB;
public abstract class BaseActivity extends AppCompatActivity {
+ private Context appCtx;
+
@Override
public void onCreate(Bundle savedInstanceState) {
- final TinyDB tinyDb = new TinyDB(getApplicationContext());
+ appCtx = getApplicationContext();
+ final TinyDB tinyDb = new TinyDB(appCtx);
switch(tinyDb.getInt("themeId")) {
@@ -83,11 +88,17 @@ public abstract class BaseActivity extends AppCompatActivity {
}
- // enabling counter badges by default
- if(tinyDb.getString("enableCounterBadgesInit").isEmpty()) {
- tinyDb.putBoolean("enableCounterBadges", true);
- tinyDb.putString("enableCounterBadgesInit", "yes");
- }
+ if(tinyDb.getInt("pollingDelayMinutes") == 0) {
+ tinyDb.putInt("pollingDelayMinutes", 15);
+ }
+
+ NotificationsMaster.hireWorker(appCtx);
+
+ // enabling counter badges by default
+ if(tinyDb.getString("enableCounterBadgesInit").isEmpty()) {
+ tinyDb.putBoolean("enableCounterBadges", true);
+ tinyDb.putString("enableCounterBadgesInit", "yes");
+ }
// enable crash reports by default
if(tinyDb.getString("crashReportingEnabledInit").isEmpty()) {
diff --git a/app/src/main/java/org/mian/gitnex/activities/EditIssueActivity.java b/app/src/main/java/org/mian/gitnex/activities/EditIssueActivity.java
index 91c42dd5..42fb8866 100644
--- a/app/src/main/java/org/mian/gitnex/activities/EditIssueActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/EditIssueActivity.java
@@ -121,7 +121,7 @@ public class EditIssueActivity extends BaseActivity implements View.OnClickListe
if(!tinyDb.getString("issueNumber").isEmpty()) {
- if(tinyDb.getString("issueType").equals("pr")) {
+ if(tinyDb.getString("issueType").equalsIgnoreCase("Pull")) {
toolbar_title.setText(getString(R.string.editPrNavHeader, String.valueOf(issueIndex)));
}
else {
@@ -266,7 +266,7 @@ public class EditIssueActivity extends BaseActivity implements View.OnClickListe
if(response.code() == 201) {
- if(tinyDb.getString("issueType").equals("pr")) {
+ if(tinyDb.getString("issueType").equalsIgnoreCase("Pull")) {
Toasty.info(ctx, getString(R.string.editPrSuccessMessage));
}
else {
diff --git a/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java b/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
index 2c296476..5220ea56 100644
--- a/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
@@ -25,9 +25,7 @@ import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
-import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -41,6 +39,7 @@ import org.mian.gitnex.clients.PicassoService;
import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.fragments.BottomSheetSingleIssueFragment;
import org.mian.gitnex.helpers.AlertDialogs;
+import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Authorization;
import org.mian.gitnex.helpers.ClickListener;
import org.mian.gitnex.helpers.ColorInverter;
@@ -50,7 +49,6 @@ import org.mian.gitnex.helpers.TimeHelper;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.UserMentions;
import org.mian.gitnex.helpers.Version;
-import org.mian.gitnex.models.IssueComments;
import org.mian.gitnex.models.Issues;
import org.mian.gitnex.models.WatchInfo;
import org.mian.gitnex.viewmodels.IssueCommentsViewModel;
@@ -58,7 +56,6 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
import java.util.Locale;
import java.util.Objects;
import io.noties.markwon.AbstractMarkwonPlugin;
@@ -191,7 +188,9 @@ public class IssueDetailActivity extends BaseActivity {
swipeRefresh.setOnRefreshListener(() -> new Handler().postDelayed(() -> {
swipeRefresh.setRefreshing(false);
- IssueCommentsViewModel.loadIssueComments(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex, ctx);
+ IssueCommentsViewModel
+ .loadIssueComments(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex,
+ ctx);
}, 500));
@@ -265,7 +264,9 @@ public class IssueDetailActivity extends BaseActivity {
if(tinyDb.getBoolean("commentPosted")) {
scrollViewComments.post(() -> {
- IssueCommentsViewModel.loadIssueComments(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex, ctx);
+ IssueCommentsViewModel
+ .loadIssueComments(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex,
+ ctx);
new Handler().postDelayed(() -> scrollViewComments.fullScroll(ScrollView.FOCUS_DOWN), 1000);
@@ -277,7 +278,9 @@ public class IssueDetailActivity extends BaseActivity {
if(tinyDb.getBoolean("commentEdited")) {
scrollViewComments.post(() -> {
- IssueCommentsViewModel.loadIssueComments(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex, ctx);
+ IssueCommentsViewModel
+ .loadIssueComments(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex,
+ ctx);
tinyDb.putBoolean("commentEdited", false);
});
@@ -315,12 +318,11 @@ public class IssueDetailActivity extends BaseActivity {
IssueCommentsViewModel issueCommentsModel = new ViewModelProvider(this).get(IssueCommentsViewModel.class);
- issueCommentsModel.getIssueCommentList(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), owner, repo, index, ctx).observe(this, new Observer>() {
-
- @Override
- public void onChanged(@Nullable List issueCommentsMain) {
+ issueCommentsModel.getIssueCommentList(instanceUrl, Authorization.returnAuthentication(ctx, loginUid, instanceToken), owner, repo, index, ctx)
+ .observe(this, issueCommentsMain -> {
assert issueCommentsMain != null;
+
if(issueCommentsMain.size() > 0) {
divider.setVisibility(View.VISIBLE);
}
@@ -328,15 +330,15 @@ public class IssueDetailActivity extends BaseActivity {
adapter = new IssueCommentsAdapter(ctx, issueCommentsMain);
mRecyclerView.setAdapter(adapter);
- }
- });
+ });
}
private void getSingleIssue(String instanceUrl, String instanceToken, String repoOwner, String repoName, int issueIndex, String loginUid) {
final TinyDB tinyDb = new TinyDB(appCtx);
- Call call = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface().getIssueByIndex(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
+ Call call = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface()
+ .getIssueByIndex(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
call.enqueue(new Callback() {
@@ -348,43 +350,47 @@ public class IssueDetailActivity extends BaseActivity {
Issues singleIssue = response.body();
assert singleIssue != null;
- final Markwon markwon = Markwon.builder(Objects.requireNonNull(ctx)).usePlugin(CorePlugin.create()).usePlugin(ImagesPlugin.create(plugin -> {
- plugin.addSchemeHandler(new SchemeHandler() {
+ final Markwon markwon = Markwon.builder(Objects.requireNonNull(ctx)).usePlugin(CorePlugin.create())
+ .usePlugin(ImagesPlugin.create(plugin -> {
+ plugin.addSchemeHandler(new SchemeHandler() {
+
+ @NonNull
+ @Override
+ public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
+
+ final int resourceId = ctx.getResources()
+ .getIdentifier(raw.substring("drawable://".length()), "drawable", ctx.getPackageName());
+
+ final Drawable drawable = ctx.getDrawable(resourceId);
+
+ assert drawable != null;
+ return ImageItem.withResult(drawable);
+ }
+
+ @NonNull
+ @Override
+ public Collection supportedSchemes() {
+
+ return Collections.singleton("drawable");
+ }
+ });
+ plugin.placeholderProvider(drawable -> null);
+ plugin.addMediaDecoder(GifMediaDecoder.create(false));
+ plugin.addMediaDecoder(SvgMediaDecoder.create(ctx.getResources()));
+ plugin.addMediaDecoder(SvgMediaDecoder.create());
+ plugin.defaultMediaDecoder(DefaultMediaDecoder.create(ctx.getResources()));
+ plugin.defaultMediaDecoder(DefaultMediaDecoder.create());
+
+ })).usePlugin(new AbstractMarkwonPlugin() {
- @NonNull
@Override
- public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
+ public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
- final int resourceId = ctx.getResources().getIdentifier(raw.substring("drawable://".length()), "drawable", ctx.getPackageName());
-
- final Drawable drawable = ctx.getDrawable(resourceId);
-
- assert drawable != null;
- return ImageItem.withResult(drawable);
+ builder.codeTextColor(tinyDb.getInt("codeBlockColor")).codeBackgroundColor(tinyDb.getInt("codeBlockBackground"))
+ .linkColor(getResources().getColor(R.color.lightBlue));
}
-
- @NonNull
- @Override
- public Collection supportedSchemes() {
-
- return Collections.singleton("drawable");
- }
- });
- plugin.placeholderProvider(drawable -> null);
- plugin.addMediaDecoder(GifMediaDecoder.create(false));
- plugin.addMediaDecoder(SvgMediaDecoder.create(ctx.getResources()));
- plugin.addMediaDecoder(SvgMediaDecoder.create());
- plugin.defaultMediaDecoder(DefaultMediaDecoder.create(ctx.getResources()));
- plugin.defaultMediaDecoder(DefaultMediaDecoder.create());
-
- })).usePlugin(new AbstractMarkwonPlugin() {
-
- @Override
- public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
-
- builder.codeTextColor(tinyDb.getInt("codeBlockColor")).codeBackgroundColor(tinyDb.getInt("codeBlockBackground")).linkColor(getResources().getColor(R.color.lightBlue));
- }
- }).usePlugin(TablePlugin.create(ctx)).usePlugin(TaskListPlugin.create(ctx)).usePlugin(HtmlPlugin.create()).usePlugin(StrikethroughPlugin.create()).usePlugin(LinkifyPlugin.create()).build();
+ }).usePlugin(TablePlugin.create(ctx)).usePlugin(TaskListPlugin.create(ctx)).usePlugin(HtmlPlugin.create())
+ .usePlugin(StrikethroughPlugin.create()).usePlugin(LinkifyPlugin.create()).build();
TinyDB tinyDb = new TinyDB(appCtx);
final String locale = tinyDb.getString("locale");
@@ -392,8 +398,10 @@ public class IssueDetailActivity extends BaseActivity {
tinyDb.putString("issueState", singleIssue.getState());
tinyDb.putString("issueTitle", singleIssue.getTitle());
- PicassoService.getInstance(ctx).get().load(singleIssue.getUser().getAvatar_url()).placeholder(R.drawable.loader_animated).transform(new RoundedTransformation(8, 0)).resize(120, 120).centerCrop().into(assigneeAvatar);
- String issueNumber_ = "" + appCtx.getResources().getString(R.string.hash) + singleIssue.getNumber() + "";
+ PicassoService.getInstance(ctx).get().load(singleIssue.getUser().getAvatar_url()).placeholder(R.drawable.loader_animated)
+ .transform(new RoundedTransformation(8, 0)).resize(120, 120).centerCrop().into(assigneeAvatar);
+ String issueNumber_ = "" + appCtx.getResources()
+ .getString(R.string.hash) + singleIssue.getNumber() + "";
issueTitle.setText(Html.fromHtml(issueNumber_ + " " + singleIssue.getTitle()));
String cleanIssueDescription = singleIssue.getBody().trim();
Spanned bodyWithMD = markwon.toMarkdown(EmojiParser.parseToUnicode(cleanIssueDescription));
@@ -410,15 +418,19 @@ public class IssueDetailActivity extends BaseActivity {
ImageView assigneesView = new ImageView(ctx);
- PicassoService.getInstance(ctx).get().load(singleIssue.getAssignees().get(i).getAvatar_url()).placeholder(R.drawable.loader_animated).transform(new RoundedTransformation(8, 0)).resize(100, 100).centerCrop().into(assigneesView);
+ PicassoService.getInstance(ctx).get().load(singleIssue.getAssignees().get(i).getAvatar_url())
+ .placeholder(R.drawable.loader_animated).transform(new RoundedTransformation(8, 0)).resize(100, 100).centerCrop()
+ .into(assigneesView);
assigneesLayout.addView(assigneesView);
assigneesView.setLayoutParams(params1);
if(!singleIssue.getAssignees().get(i).getFull_name().equals("")) {
- assigneesView.setOnClickListener(new ClickListener(getString(R.string.assignedTo, singleIssue.getAssignees().get(i).getFull_name()), ctx));
+ assigneesView.setOnClickListener(
+ new ClickListener(getString(R.string.assignedTo, singleIssue.getAssignees().get(i).getFull_name()), ctx));
}
else {
- assigneesView.setOnClickListener(new ClickListener(getString(R.string.assignedTo, singleIssue.getAssignees().get(i).getLogin()), ctx));
+ assigneesView.setOnClickListener(
+ new ClickListener(getString(R.string.assignedTo, singleIssue.getAssignees().get(i).getLogin()), ctx));
}
}
@@ -427,12 +439,13 @@ public class IssueDetailActivity extends BaseActivity {
assigneesScrollView.setVisibility(View.GONE);
}
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 15, 0);
if(singleIssue.getLabels() != null) {
labelsScrollView.setVisibility(View.VISIBLE);
- int width = 25;
+
for(int i = 0; i < singleIssue.getLabels().size(); i++) {
String labelColor = singleIssue.getLabels().get(i).getColor();
@@ -444,9 +457,15 @@ public class IssueDetailActivity extends BaseActivity {
labelsLayout.setGravity(Gravity.START | Gravity.TOP);
labelsView.setLayoutParams(params);
- TextDrawable drawable = TextDrawable.builder().beginConfig().useFont(Typeface.DEFAULT).textColor(new ColorInverter().getContrastColor(color)).fontSize(30).width(LabelWidthCalculator.calculateLabelWidth(labelName, Typeface.DEFAULT, 30, 15)).height(50).endConfig().buildRoundRect(labelName, color, 10);
- labelsView.setImageDrawable(drawable);
+ int height = AppUtil.getPixelsFromDensity(ctx, 25);
+ int textSize = AppUtil.getPixelsFromScaledDensity(ctx, 15);
+ TextDrawable drawable = TextDrawable.builder().beginConfig().useFont(Typeface.DEFAULT)
+ .textColor(new ColorInverter().getContrastColor(color)).fontSize(textSize)
+ .width(LabelWidthCalculator.calculateLabelWidth(labelName, Typeface.DEFAULT, textSize, AppUtil.getPixelsFromDensity(ctx, 10)))
+ .height(height).endConfig().buildRoundRect(labelName, color, AppUtil.getPixelsFromDensity(ctx, 5));
+
+ labelsView.setImageDrawable(drawable);
labelsLayout.addView(labelsView);
}
@@ -461,7 +480,8 @@ public class IssueDetailActivity extends BaseActivity {
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", new Locale(locale));
String dueDate = formatter.format(singleIssue.getDue_date());
issueDueDate.setText(dueDate);
- issueDueDate.setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(singleIssue.getDue_date()), ctx));
+ issueDueDate
+ .setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(singleIssue.getDue_date()), ctx));
}
else if(timeFormat.equals("normal1")) {
DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy", new Locale(locale));
@@ -481,7 +501,8 @@ public class IssueDetailActivity extends BaseActivity {
edited = getString(R.string.colorfulBulletSpan) + getString(R.string.modifiedText);
issueModified.setVisibility(View.VISIBLE);
issueModified.setText(edited);
- issueModified.setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(singleIssue.getUpdated_at()), ctx));
+ issueModified
+ .setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(singleIssue.getUpdated_at()), ctx));
}
else {
issueModified.setVisibility(View.INVISIBLE);
@@ -508,7 +529,8 @@ public class IssueDetailActivity extends BaseActivity {
issueCreatedTime.setVisibility(View.VISIBLE);
if(timeFormat.equals("pretty")) {
- issueCreatedTime.setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(singleIssue.getCreated_at()), ctx));
+ issueCreatedTime
+ .setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(singleIssue.getCreated_at()), ctx));
}
if(singleIssue.getMilestone() != null) {
@@ -519,10 +541,12 @@ public class IssueDetailActivity extends BaseActivity {
}
if(!singleIssue.getUser().getFull_name().equals("")) {
- assigneeAvatar.setOnClickListener(new ClickListener(ctx.getResources().getString(R.string.issueCreator) + singleIssue.getUser().getFull_name(), ctx));
+ assigneeAvatar.setOnClickListener(
+ new ClickListener(ctx.getResources().getString(R.string.issueCreator) + singleIssue.getUser().getFull_name(), ctx));
}
else {
- assigneeAvatar.setOnClickListener(new ClickListener(ctx.getResources().getString(R.string.issueCreator) + singleIssue.getUser().getLogin(), ctx));
+ assigneeAvatar.setOnClickListener(
+ new ClickListener(ctx.getResources().getString(R.string.issueCreator) + singleIssue.getUser().getLogin(), ctx));
}
progressBar.setVisibility(View.GONE);
@@ -531,7 +555,10 @@ public class IssueDetailActivity extends BaseActivity {
else if(response.code() == 401) {
- AlertDialogs.authorizationTokenRevokedDialog(ctx, getResources().getString(R.string.alertDialogTokenRevokedTitle), getResources().getString(R.string.alertDialogTokenRevokedMessage), getResources().getString(R.string.alertDialogTokenRevokedCopyNegativeButton), getResources().getString(R.string.alertDialogTokenRevokedCopyPositiveButton));
+ AlertDialogs.authorizationTokenRevokedDialog(ctx, getResources().getString(R.string.alertDialogTokenRevokedTitle),
+ getResources().getString(R.string.alertDialogTokenRevokedMessage),
+ getResources().getString(R.string.alertDialogTokenRevokedCopyNegativeButton),
+ getResources().getString(R.string.alertDialogTokenRevokedCopyPositiveButton));
}
@@ -547,7 +574,8 @@ public class IssueDetailActivity extends BaseActivity {
if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) {
- Call call2 = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface().checkIssueWatchStatus(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
+ Call call2 = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface()
+ .checkIssueWatchStatus(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
call2.enqueue(new Callback() {
diff --git a/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java b/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java
index d0e510ec..27b495a6 100644
--- a/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java
@@ -291,7 +291,7 @@ public class LoginActivity extends BaseActivity {
try {
gitea_version = new Version(version.getVersion());
}
- catch(Error e) {
+ catch(Exception e) {
SnackBar.error(ctx, layoutView, getResources().getString(R.string.versionUnknown));
enableProcessButton();
diff --git a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java
index 2577026d..26fc163d 100644
--- a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java
@@ -35,6 +35,7 @@ import org.mian.gitnex.fragments.BottomSheetDraftsFragment;
import org.mian.gitnex.fragments.DraftsFragment;
import org.mian.gitnex.fragments.ExploreRepositoriesFragment;
import org.mian.gitnex.fragments.MyRepositoriesFragment;
+import org.mian.gitnex.fragments.NotificationsFragment;
import org.mian.gitnex.fragments.OrganizationsFragment;
import org.mian.gitnex.fragments.ProfileFragment;
import org.mian.gitnex.fragments.RepositoriesFragment;
@@ -49,10 +50,10 @@ import org.mian.gitnex.helpers.ColorInverter;
import org.mian.gitnex.helpers.RoundedTransformation;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.Toasty;
+import org.mian.gitnex.helpers.Version;
import org.mian.gitnex.models.GiteaVersion;
import org.mian.gitnex.models.UserInfo;
import java.util.Objects;
-import java.util.concurrent.ExecutionException;
import eightbitlab.com.blurview.BlurView;
import eightbitlab.com.blurview.RenderScriptBlur;
import retrofit2.Call;
@@ -90,7 +91,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
final TinyDB tinyDb = new TinyDB(appCtx);
tinyDb.putBoolean("noConnection", false);
- //userAvatar = findViewById(R.id.userAvatar);
Intent mainIntent = getIntent();
String launchFragment = mainIntent.getStringExtra("launchFragment");
@@ -124,12 +124,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
}
String accountName = loginUid + "@" + instanceUrl;
- try {
- getAccountData(accountName);
- }
- catch(ExecutionException | InterruptedException e) {
- Log.e("getAccountData", e.toString());
- }
+ getAccountData(accountName);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbarTitle = toolbar.findViewById(R.id.toolbar_title);
@@ -171,6 +166,9 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
else if(fragmentById instanceof ExploreRepositoriesFragment) {
toolbarTitle.setText(getResources().getString(R.string.pageTitleExplore));
}
+ else if(fragmentById instanceof NotificationsFragment) {
+ toolbarTitle.setText(R.string.pageTitleNotifications);
+ }
else if(fragmentById instanceof ProfileFragment) {
toolbarTitle.setText(getResources().getString(R.string.pageTitleProfile));
}
@@ -219,7 +217,10 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
userEmail.setTypeface(myTypeface);
userFullName.setTypeface(myTypeface);
+ String currentVersion = tinyDb.getString("giteaVersion");
+
navigationView.getMenu().findItem(R.id.nav_administration).setVisible(tinyDb.getBoolean("userIsAdmin"));
+ navigationView.getMenu().findItem(R.id.nav_notifications).setVisible(new Version(currentVersion).higherOrEqual("1.12.3"));
if(!userEmailNav.equals("")) {
userEmail.setText(userEmailNav);
@@ -297,13 +298,22 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
if(launchFragment != null) {
- if(launchFragment.equals("drafts")) {
+ mainIntent.removeExtra("launchFragment");
+
+ switch(launchFragment) {
+
+ case "drafts":
+ toolbarTitle.setText(getResources().getString(R.string.titleDrafts));
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new DraftsFragment()).commit();
+ navigationView.setCheckedItem(R.id.nav_comments_draft);
+ return;
+
+ case "notifications":
+ toolbarTitle.setText(getResources().getString(R.string.pageTitleNotifications));
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new NotificationsFragment()).commit();
+ navigationView.setCheckedItem(R.id.nav_notifications);
+ return;
- getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new DraftsFragment()).commit();
- toolbarTitle.setText(getResources().getString(R.string.titleDrafts));
- navigationView.setCheckedItem(R.id.nav_comments_draft);
- mainIntent.removeExtra("launchFragment");
- return;
}
}
@@ -354,7 +364,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
break;
}
-
}
if(!connToInternet) {
@@ -376,17 +385,22 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
// Changelog popup
int versionCode = 0;
+
try {
+
PackageInfo packageInfo = appCtx.getPackageManager().getPackageInfo(appCtx.getPackageName(), 0);
versionCode = packageInfo.versionCode;
}
catch(PackageManager.NameNotFoundException e) {
+
Log.e("changelogDialog", Objects.requireNonNull(e.getMessage()));
}
if(versionCode > tinyDb.getInt("versionCode")) {
+
tinyDb.putInt("versionCode", versionCode);
tinyDb.putBoolean("versionFlag", true);
+
ChangeLog changelogDialog = new ChangeLog(this);
changelogDialog.showDialog();
}
@@ -428,7 +442,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
}
- public void getAccountData(String accountName) throws ExecutionException, InterruptedException {
+ public void getAccountData(String accountName) {
UserAccountsApi accountData = new UserAccountsApi(ctx);
UserAccount data = accountData.getAccountData(accountName);
@@ -440,7 +454,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
else {
AlertDialogs.forceLogoutDialog(ctx, getResources().getString(R.string.forceLogoutDialogHeader), getResources().getString(R.string.forceLogoutDialogDescription), getResources().getString(R.string.alertDialogTokenRevokedCopyPositiveButton));
}
-
}
@Override
@@ -509,6 +522,11 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new ExploreRepositoriesFragment()).commit();
break;
+ case R.id.nav_notifications:
+ toolbarTitle.setText(R.string.pageTitleNotifications);
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new NotificationsFragment()).commit();
+ break;
+
case R.id.nav_comments_draft:
toolbarTitle.setText(getResources().getString(R.string.titleDrafts));
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new DraftsFragment()).commit();
@@ -583,14 +601,12 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
tinyDb.putString("giteaVersion", version.getVersion());
}
-
}
@Override
public void onFailure(@NonNull Call callVersion, @NonNull Throwable t) {
Log.e("onFailure-version", t.toString());
-
}
});
diff --git a/app/src/main/java/org/mian/gitnex/activities/SettingsSecurityActivity.java b/app/src/main/java/org/mian/gitnex/activities/SettingsSecurityActivity.java
index 4f5caf32..9e1b0096 100644
--- a/app/src/main/java/org/mian/gitnex/activities/SettingsSecurityActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/SettingsSecurityActivity.java
@@ -7,6 +7,7 @@ import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.NumberPicker;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import org.apache.commons.io.FileUtils;
@@ -15,7 +16,9 @@ import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.FilesData;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.Toasty;
+import org.mian.gitnex.helpers.Version;
import org.mian.gitnex.helpers.ssl.MemorizingTrustManager;
+import org.mian.gitnex.notifications.NotificationsMaster;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
@@ -27,6 +30,8 @@ import java.util.HashSet;
public class SettingsSecurityActivity extends BaseActivity {
private Context appCtx;
+ private Context ctx = this;
+
private View.OnClickListener onClickListener;
private static String[] cacheSizeDataList = {"50 MB", "100 MB", "250 MB", "500 MB", "1 GB"};
@@ -35,6 +40,10 @@ public class SettingsSecurityActivity extends BaseActivity {
private static String[] cacheSizeImagesList = {"50 MB", "100 MB", "250 MB", "500 MB", "1 GB"};
private static int cacheSizeImagesSelectedChoice = 0;
+ private static int MINIMUM_POLLING_DELAY = 1;
+ private static int DEFAULT_POLLING_DELAY = 20;
+ private static int MAXIMUM_POLLING_DELAY = 720;
+
@Override
protected int getLayoutResourceId() {
@@ -48,6 +57,7 @@ public class SettingsSecurityActivity extends BaseActivity {
appCtx = getApplicationContext();
TinyDB tinyDb = new TinyDB(appCtx);
+ String currentVersion = tinyDb.getString("giteaVersion");
ImageView closeActivity = findViewById(R.id.close);
@@ -57,8 +67,10 @@ public class SettingsSecurityActivity extends BaseActivity {
TextView cacheSizeDataSelected = findViewById(R.id.cacheSizeDataSelected); // setter for data cache size
TextView cacheSizeImagesSelected = findViewById(R.id.cacheSizeImagesSelected); // setter for images cache size
TextView clearCacheSelected = findViewById(R.id.clearCacheSelected); // setter for clear cache
+ TextView pollingDelaySelected = findViewById(R.id.pollingDelaySelected);
LinearLayout certsFrame = findViewById(R.id.certsFrame);
+ LinearLayout pollingDelayFrame = findViewById(R.id.pollingDelayFrame);
LinearLayout cacheSizeDataFrame = findViewById(R.id.cacheSizeDataSelectionFrame);
LinearLayout cacheSizeImagesFrame = findViewById(R.id.cacheSizeImagesSelectionFrame);
LinearLayout clearCacheFrame = findViewById(R.id.clearCacheSelectionFrame);
@@ -79,6 +91,12 @@ public class SettingsSecurityActivity extends BaseActivity {
cacheSizeImagesSelectedChoice = tinyDb.getInt("cacheSizeImagesId");
}
+ if(new Version(currentVersion).less("1.12.3")) {
+ pollingDelayFrame.setVisibility(View.GONE);
+ }
+
+ pollingDelaySelected.setText(String.format(getString(R.string.pollingDelaySelectedText), tinyDb.getInt("pollingDelayMinutes", DEFAULT_POLLING_DELAY)));
+
// clear cache setter
File cacheDir = appCtx.getCacheDir();
long size__ = FilesData.getFileSizeRecursively(new HashSet<>(), cacheDir);
@@ -190,7 +208,6 @@ public class SettingsSecurityActivity extends BaseActivity {
tinyDb.putBoolean("loggedInMode", false);
tinyDb.remove("basicAuthPassword");
tinyDb.putBoolean("basicAuthFlag", false);
- //tinyDb.clear();
Intent loginActivityIntent = new Intent().setClass(appCtx, LoginActivity.class);
loginActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -203,12 +220,42 @@ public class SettingsSecurityActivity extends BaseActivity {
});
+ // polling delay
+ pollingDelayFrame.setOnClickListener(v -> {
+
+ NumberPicker numberPicker = new NumberPicker(ctx);
+ numberPicker.setMinValue(MINIMUM_POLLING_DELAY);
+ numberPicker.setMaxValue(MAXIMUM_POLLING_DELAY);
+ numberPicker.setValue(tinyDb.getInt("pollingDelayMinutes", DEFAULT_POLLING_DELAY));
+ numberPicker.setWrapSelectorWheel(true);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+ builder.setTitle(getString(R.string.pollingDelayDialogHeaderText));
+ builder.setMessage(getString(R.string.pollingDelayDialogDescriptionText));
+
+ builder.setCancelable(true);
+ builder.setPositiveButton(getString(R.string.okButton), (dialog, which) -> {
+
+ tinyDb.putInt("pollingDelayMinutes", numberPicker.getValue());
+
+ NotificationsMaster.fireWorker(ctx);
+ NotificationsMaster.hireWorker(ctx);
+
+ pollingDelaySelected.setText(String.format(getString(R.string.pollingDelaySelectedText), numberPicker.getValue()));
+ Toasty.info(appCtx, getResources().getString(R.string.settingsSave));
+
+ });
+
+ builder.setNegativeButton(R.string.cancelButton, (dialog, which) -> dialog.dismiss());
+ builder.setView(numberPicker);
+ builder.create().show();
+
+ });
+
}
private void initCloseListener() {
- onClickListener = view -> {
- finish();
- };
- }
+ onClickListener = view -> finish();
+ }
}
diff --git a/app/src/main/java/org/mian/gitnex/adapters/IssuesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/IssuesAdapter.java
index 22d3a168..b1f56f5f 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/IssuesAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/IssuesAdapter.java
@@ -125,7 +125,7 @@ public class IssuesAdapter extends RecyclerView.Adapter
TinyDB tinyDb = new TinyDB(context);
tinyDb.putString("issueNumber", issueNumber.getText().toString());
- tinyDb.putString("issueType", "issue");
+ tinyDb.putString("issueType", "Issue");
context.startActivity(intent);
});
@@ -138,7 +138,7 @@ public class IssuesAdapter extends RecyclerView.Adapter
TinyDB tinyDb = new TinyDB(context);
tinyDb.putString("issueNumber", issueNumber.getText().toString());
- tinyDb.putString("issueType", "issue");
+ tinyDb.putString("issueType", "Issue");
context.startActivity(intent);
});
diff --git a/app/src/main/java/org/mian/gitnex/adapters/NotificationsAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/NotificationsAdapter.java
new file mode 100644
index 00000000..8097eb09
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/adapters/NotificationsAdapter.java
@@ -0,0 +1,125 @@
+package org.mian.gitnex.adapters;
+
+import android.content.Context;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import org.mian.gitnex.R;
+import org.mian.gitnex.models.NotificationThread;
+import java.util.List;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationsAdapter extends RecyclerView.Adapter {
+
+ private Context context;
+ private List notificationThreads;
+ private OnMoreClickedListener onMoreClickedListener;
+ private OnNotificationClickedListener onNotificationClickedListener;
+
+ public NotificationsAdapter(Context context, List notificationThreads, OnMoreClickedListener onMoreClickedListener, OnNotificationClickedListener onNotificationClickedListener) {
+
+ this.context = context;
+ this.notificationThreads = notificationThreads;
+ this.onMoreClickedListener = onMoreClickedListener;
+ this.onNotificationClickedListener = onNotificationClickedListener;
+
+ }
+
+ static class NotificationsViewHolder extends RecyclerView.ViewHolder {
+
+ private LinearLayout frame;
+ private TextView subject;
+ private TextView repository;
+ private ImageView type;
+ private ImageView pinned;
+ private ImageView more;
+
+ public NotificationsViewHolder(@NonNull View itemView) {
+
+ super(itemView);
+
+ frame = itemView.findViewById(R.id.frame);
+ subject = itemView.findViewById(R.id.subject);
+ repository = itemView.findViewById(R.id.repository);
+ type = itemView.findViewById(R.id.type);
+ pinned = itemView.findViewById(R.id.pinned);
+ more = itemView.findViewById(R.id.more);
+
+ }
+ }
+
+ @NonNull
+ @Override
+ public NotificationsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+
+ View v = LayoutInflater.from(context).inflate(R.layout.list_notifications, parent, false);
+ return new NotificationsAdapter.NotificationsViewHolder(v);
+
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull NotificationsViewHolder holder, int position) {
+
+ NotificationThread notificationThread = notificationThreads.get(position);
+
+ String url = notificationThread.getSubject().getUrl();
+ String subjectId = "" + context.getResources()
+ .getString(R.string.hash) + url.substring(url.lastIndexOf("/") + 1) + "";
+
+ holder.subject.setText(Html.fromHtml(subjectId + " " + notificationThread.getSubject().getTitle()));
+ holder.repository.setText(notificationThread.getRepository().getFullname());
+
+ if(notificationThread.isPinned()) {
+ holder.pinned.setVisibility(View.VISIBLE);
+ }
+ else {
+ holder.pinned.setVisibility(View.GONE);
+ }
+
+ switch(notificationThread.getSubject().getType()) {
+
+ case "Pull":
+ holder.type.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_pull_request, null));
+ break;
+
+ case "Issue":
+ holder.type.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_issue, null));
+ break;
+
+ default:
+ holder.type.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_question, null));
+ break;
+
+ }
+
+ holder.frame.setOnClickListener(v -> onNotificationClickedListener.onNotificationClicked(notificationThread));
+ holder.more.setOnClickListener(v -> onMoreClickedListener.onMoreClicked(notificationThread));
+
+ }
+
+ @Override
+ public int getItemCount() {
+
+ return notificationThreads.size();
+ }
+
+ public interface OnNotificationClickedListener {
+
+ void onNotificationClicked(NotificationThread notificationThread);
+ }
+
+ public interface OnMoreClickedListener {
+
+ void onMoreClicked(NotificationThread notificationThread);
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/adapters/PullRequestsAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/PullRequestsAdapter.java
index 64f20f80..dba63bf4 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/PullRequestsAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/PullRequestsAdapter.java
@@ -136,7 +136,7 @@ public class PullRequestsAdapter extends RecyclerView.Adapter call;
call = RetrofitClient.getInstance(instanceUrl, context).getApiInterface().checkRepoWatchStatus(token, repoOwner, repoName);
diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetNotificationsFilterFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetNotificationsFilterFragment.java
new file mode 100644
index 00000000..48b66560
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetNotificationsFilterFragment.java
@@ -0,0 +1,81 @@
+package org.mian.gitnex.fragments;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import org.mian.gitnex.R;
+import org.mian.gitnex.helpers.TinyDB;
+
+/**
+ * Author opyale
+ */
+
+public class BottomSheetNotificationsFilterFragment extends BottomSheetDialogFragment {
+
+ private TinyDB tinyDB;
+ private OnDismissedListener onDismissedListener;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+
+ this.tinyDB = new TinyDB(context);
+ super.onAttach(context);
+
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.bottom_sheet_notifications_filter, container, false);
+
+ TextView readNotifications = view.findViewById(R.id.readNotifications);
+ TextView unreadNotifications = view.findViewById(R.id.unreadNotifications);
+
+ readNotifications.setOnClickListener(v1 -> {
+
+ tinyDB.putString("notificationsFilterState", "read");
+ dismiss();
+
+ });
+
+ unreadNotifications.setOnClickListener(v12 -> {
+
+ tinyDB.putString("notificationsFilterState", "unread");
+ dismiss();
+
+ });
+
+ return view;
+
+ }
+
+ @Override
+ public void dismiss() {
+
+ if(onDismissedListener != null) {
+
+ onDismissedListener.onDismissed();
+ }
+
+ super.dismiss();
+
+ }
+
+ public void setOnDismissedListener(OnDismissedListener onDismissedListener) {
+
+ this.onDismissedListener = onDismissedListener;
+ }
+
+ public interface OnDismissedListener {
+
+ void onDismissed();
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetNotificationsFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetNotificationsFragment.java
new file mode 100644
index 00000000..a254d19c
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetNotificationsFragment.java
@@ -0,0 +1,147 @@
+package org.mian.gitnex.fragments;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import org.mian.gitnex.R;
+import org.mian.gitnex.actions.NotificationsActions;
+import org.mian.gitnex.helpers.AppUtil;
+import org.mian.gitnex.helpers.Toasty;
+import org.mian.gitnex.models.NotificationThread;
+import java.util.Objects;
+
+/**
+ * Author opyale
+ */
+
+public class BottomSheetNotificationsFragment extends BottomSheetDialogFragment {
+
+ private Context context;
+ private NotificationThread notificationThread;
+ private OnOptionSelectedListener onOptionSelectedListener;
+
+ public void onAttach(Context context, NotificationThread notificationThread, OnOptionSelectedListener onOptionSelectedListener) {
+
+ this.context = context;
+ this.notificationThread = notificationThread;
+ this.onOptionSelectedListener = onOptionSelectedListener;
+
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+
+ View v = inflater.inflate(R.layout.bottom_sheet_notifications, container, false);
+
+ TextView markRead = v.findViewById(R.id.markRead);
+ TextView markUnread = v.findViewById(R.id.markUnread);
+ TextView markPinned = v.findViewById(R.id.markPinned);
+
+ NotificationsActions notificationsActions = new NotificationsActions(context);
+ Activity activity = Objects.requireNonNull(getActivity());
+
+ if(notificationThread.isPinned()) {
+
+ AppUtil.setMultiVisibility(View.GONE, markUnread, markPinned);
+ } else if(notificationThread.isUnread()) {
+
+ markUnread.setVisibility(View.GONE);
+ } else {
+
+ markRead.setVisibility(View.GONE);
+ }
+
+ markPinned.setOnClickListener(v12 -> {
+
+ Thread thread = new Thread(() -> {
+
+ try {
+
+ notificationsActions.setNotificationStatus(notificationThread, NotificationsActions.NotificationStatus.PINNED);
+ activity.runOnUiThread(() -> onOptionSelectedListener.onSelected());
+
+ }
+ catch(Exception e) {
+
+ activity.runOnUiThread(() -> Toasty.error(context, getString(R.string.genericError)));
+ Log.e("onError", e.toString());
+
+ } finally {
+
+ dismiss();
+ }
+ });
+
+ thread.start();
+
+ });
+
+ markRead.setOnClickListener(v1 -> {
+
+ Thread thread = new Thread(() -> {
+
+ try {
+
+ notificationsActions.setNotificationStatus(notificationThread, NotificationsActions.NotificationStatus.READ);
+ activity.runOnUiThread(() -> onOptionSelectedListener.onSelected());
+
+ }
+ catch(Exception e) {
+
+ activity.runOnUiThread(() -> Toasty.error(context, getString(R.string.genericError)));
+ Log.e("onError", e.toString());
+
+ } finally {
+
+ dismiss();
+ }
+ });
+
+ thread.start();
+
+ });
+
+ markUnread.setOnClickListener(v13 -> {
+
+ Thread thread = new Thread(() -> {
+
+ try {
+
+ notificationsActions.setNotificationStatus(notificationThread, NotificationsActions.NotificationStatus.UNREAD);
+ activity.runOnUiThread(() -> onOptionSelectedListener.onSelected());
+
+ }
+ catch(Exception e) {
+
+ activity.runOnUiThread(() -> Toasty.error(context, getString(R.string.genericError)));
+ Log.e("onError", e.toString());
+
+ } finally {
+
+ dismiss();
+ }
+ });
+
+ thread.start();
+
+ });
+
+ return v;
+
+ }
+
+ public interface OnOptionSelectedListener {
+
+ void onSelected();
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java
index af8287c8..ece2c358 100644
--- a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java
+++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java
@@ -55,7 +55,7 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment {
TextView subscribeIssue = v.findViewById(R.id.subscribeIssue);
TextView unsubscribeIssue = v.findViewById(R.id.unsubscribeIssue);
- if(tinyDB.getString("issueType").equals("pr")) {
+ if(tinyDB.getString("issueType").equalsIgnoreCase("Pull")) {
editIssue.setText(R.string.editPrText);
copyIssueUrl.setText(R.string.copyPrUrlText);
@@ -199,7 +199,7 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment {
});
- if(tinyDB.getString("issueType").equals("issue")) {
+ if(tinyDB.getString("issueType").equalsIgnoreCase("Issue")) {
if(tinyDB.getString("issueState").equals("open")) { // close issue
diff --git a/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java b/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java
new file mode 100644
index 00000000..4fdd40bd
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java
@@ -0,0 +1,367 @@
+package org.mian.gitnex.fragments;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import org.apache.commons.lang3.StringUtils;
+import org.mian.gitnex.R;
+import org.mian.gitnex.actions.NotificationsActions;
+import org.mian.gitnex.activities.IssueDetailActivity;
+import org.mian.gitnex.adapters.NotificationsAdapter;
+import org.mian.gitnex.clients.RetrofitClient;
+import org.mian.gitnex.helpers.AppUtil;
+import org.mian.gitnex.helpers.InfiniteScrollListener;
+import org.mian.gitnex.helpers.SnackBar;
+import org.mian.gitnex.helpers.StaticGlobalVariables;
+import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.models.NotificationThread;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationsFragment extends Fragment implements NotificationsAdapter.OnNotificationClickedListener, NotificationsAdapter.OnMoreClickedListener, BottomSheetNotificationsFragment.OnOptionSelectedListener {
+
+ private List notificationThreads;
+ private NotificationsAdapter notificationsAdapter;
+ private NotificationsActions notificationsActions;
+
+ private ImageView markAllAsRead;
+ private ProgressBar progressBar;
+ private RelativeLayout mainLayout;
+ private ProgressBar loadingMoreView;
+ private TextView noDataNotifications;
+ private SwipeRefreshLayout pullToRefresh;
+
+ private Activity activity;
+ private Context context;
+ private TinyDB tinyDB;
+ private Menu menu;
+
+ private int pageCurrentIndex = 1;
+ private int pageResultLimit;
+ private String currentFilterMode = "unread";
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+
+ View v = inflater.inflate(R.layout.fragment_notifications, container, false);
+ setHasOptionsMenu(true);
+
+ activity = Objects.requireNonNull(getActivity());
+ context = getContext();
+ tinyDB = new TinyDB(context);
+
+ pageResultLimit = StaticGlobalVariables.getCurrentResultLimit(context);
+ tinyDB.putString("notificationsFilterState", currentFilterMode);
+
+ mainLayout = v.findViewById(R.id.mainLayout);
+ markAllAsRead = v.findViewById(R.id.markAllAsRead);
+ noDataNotifications = v.findViewById(R.id.noDataNotifications);
+ loadingMoreView = v.findViewById(R.id.loadingMoreView);
+ progressBar = v.findViewById(R.id.progressBar);
+
+ notificationThreads = new ArrayList<>();
+ notificationsActions = new NotificationsActions(context);
+ notificationsAdapter = new NotificationsAdapter(context, notificationThreads, this, this);
+
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context);
+
+ RecyclerView recyclerView = v.findViewById(R.id.notifications);
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setLayoutManager(linearLayoutManager);
+ recyclerView.setAdapter(notificationsAdapter);
+ recyclerView.addOnScrollListener(new InfiniteScrollListener(pageResultLimit, linearLayoutManager) {
+
+ @Override
+ public void onScrolledToEnd(int firstVisibleItemPosition) {
+
+ pageCurrentIndex++;
+ loadNotifications(true);
+
+ }
+ });
+
+ recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+
+ if(currentFilterMode.equalsIgnoreCase("unread")) {
+
+ if(dy > 0 && markAllAsRead.isShown()) {
+
+ markAllAsRead.setVisibility(View.GONE);
+ } else if(dy < 0) {
+
+ markAllAsRead.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+
+ super.onScrollStateChanged(recyclerView, newState);
+ }
+
+ });
+
+ markAllAsRead.setOnClickListener(v1 -> {
+
+ Thread thread = new Thread(() -> {
+
+ try {
+
+ if(notificationsActions.setAllNotificationsRead(new Date())) {
+
+ activity.runOnUiThread(() -> {
+
+ SnackBar.info(context, mainLayout, getString(R.string.markedNotificationsAsRead));
+ loadNotifications(true);
+
+ });
+ }
+ }
+ catch(IOException e) {
+
+ activity.runOnUiThread(() -> SnackBar.error(context, mainLayout, getString(R.string.genericError)));
+ Log.e("onError", e.toString());
+
+ }
+ });
+
+ thread.start();
+
+ });
+
+ pullToRefresh = v.findViewById(R.id.pullToRefresh);
+ pullToRefresh.setOnRefreshListener(() -> {
+
+ pageCurrentIndex = 1;
+ loadNotifications(false);
+
+ });
+
+ loadNotifications(false);
+ return v;
+
+ }
+
+ private void loadNotifications(boolean append) {
+
+ noDataNotifications.setVisibility(View.GONE);
+
+ if(pageCurrentIndex == 1 || !append) {
+
+ notificationThreads.clear();
+ notificationsAdapter.notifyDataSetChanged();
+ pullToRefresh.setRefreshing(false);
+ progressBar.setVisibility(View.VISIBLE);
+
+ } else {
+
+ loadingMoreView.setVisibility(View.VISIBLE);
+ }
+
+ String instanceUrl = tinyDB.getString("instanceUrl");
+ String loginUid = tinyDB.getString("loginUid");
+ String instanceToken = "token " + tinyDB.getString(loginUid + "-token");
+
+ String[] filter = tinyDB.getString("notificationsFilterState").equals("read") ?
+ new String[]{"pinned", "read"} :
+ new String[]{"pinned", "unread"};
+
+ Call> call = RetrofitClient.getInstance(instanceUrl, context)
+ .getApiInterface()
+ .getNotificationThreads(instanceToken, false, filter,
+ StaticGlobalVariables.defaultOldestTimestamp, "",
+ pageCurrentIndex, pageResultLimit);
+
+ call.enqueue(new Callback>() {
+
+ @Override
+ public void onResponse(@NonNull Call> call, @NonNull Response> response) {
+
+ if(response.code() == 200) {
+
+ assert response.body() != null;
+
+ if(!append) {
+
+ notificationThreads.clear();
+ }
+
+ notificationThreads.addAll(response.body());
+ notificationsAdapter.notifyDataSetChanged();
+
+ } else {
+
+ Log.e("onError", String.valueOf(response.code()));
+ }
+
+ onCleanup();
+
+ }
+
+ @Override
+ public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
+
+ Log.e("onError", t.toString());
+ onCleanup();
+
+ }
+
+ private void onCleanup() {
+
+ AppUtil.setMultiVisibility(View.GONE, loadingMoreView, progressBar);
+ pullToRefresh.setRefreshing(false);
+
+ if(notificationThreads.isEmpty()) {
+
+ noDataNotifications.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+ }
+
+ private void changeFilterMode() {
+
+ int filterIcon = currentFilterMode.equalsIgnoreCase("read") ?
+ R.drawable.ic_filter_closed :
+ R.drawable.ic_filter;
+
+ menu.getItem(0).setIcon(filterIcon);
+
+ if(currentFilterMode.equalsIgnoreCase("read")) {
+
+ markAllAsRead.setVisibility(View.GONE);
+ } else {
+
+ markAllAsRead.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+
+ this.menu = menu;
+
+ inflater.inflate(R.menu.filter_menu_notifications, menu);
+
+ currentFilterMode = tinyDB.getString("notificationsFilterState");
+ changeFilterMode();
+
+ super.onCreateOptionsMenu(menu, inflater);
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+
+ if(item.getItemId() == R.id.filterNotifications) {
+
+ BottomSheetNotificationsFilterFragment bottomSheetNotificationsFilterFragment = new BottomSheetNotificationsFilterFragment();
+ bottomSheetNotificationsFilterFragment.show(getChildFragmentManager(), "notificationsFilterBottomSheet");
+ bottomSheetNotificationsFilterFragment.setOnDismissedListener(() -> {
+
+ pageCurrentIndex = 1;
+ currentFilterMode = tinyDB.getString("notificationsFilterState");
+
+ changeFilterMode();
+ loadNotifications(false);
+
+ });
+
+ return true;
+
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ @Override
+ public void onNotificationClicked(NotificationThread notificationThread) {
+
+ Thread thread = new Thread(() -> {
+
+ try {
+
+ if(notificationThread.isUnread()) {
+
+ notificationsActions.setNotificationStatus(notificationThread, NotificationsActions.NotificationStatus.READ);
+ activity.runOnUiThread(() -> loadNotifications(false));
+
+ }
+ } catch(IOException ignored) {}
+
+ });
+
+ thread.start();
+
+ if(StringUtils.containsAny(notificationThread.getSubject().getType().toLowerCase(), "pull", "issue")) {
+
+ Intent intent = new Intent(context, IssueDetailActivity.class);
+ String issueUrl = notificationThread.getSubject().getUrl();
+
+ tinyDB.putString("issueNumber", issueUrl.substring(issueUrl.lastIndexOf("/") + 1));
+ tinyDB.putString("issueType", notificationThread.getSubject().getType());
+ tinyDB.putString("repoFullName", notificationThread.getRepository().getFullname());
+
+ startActivity(intent);
+
+ }
+ }
+
+ @Override
+ public void onMoreClicked(NotificationThread notificationThread) {
+
+ BottomSheetNotificationsFragment bottomSheetNotificationsFragment = new BottomSheetNotificationsFragment();
+ bottomSheetNotificationsFragment.onAttach(context, notificationThread, this);
+ bottomSheetNotificationsFragment.show(getChildFragmentManager(), "notificationsBottomSheet");
+
+ }
+
+ @Override
+ public void onSelected() {
+
+ pageCurrentIndex = 1;
+ loadNotifications(false);
+
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java b/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java
index f5705899..79b37417 100644
--- a/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java
+++ b/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java
@@ -14,8 +14,10 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
+import java.util.Date;
import java.util.Locale;
/**
@@ -144,6 +146,15 @@ public class AppUtil {
}
+ public static String getTimestampFromDate(Context context, Date date) {
+
+ TinyDB tinyDB = new TinyDB(context);
+ Locale locale = new Locale(tinyDB.getString("locale"));
+
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", locale).format(date);
+
+ }
+
public static String formatFileSizeInDetail(long size) {
String fileSize = null;
@@ -299,4 +310,14 @@ public class AppUtil {
}
}
+ public static int getPixelsFromDensity(Context context, int dp) {
+
+ return (int) (context.getResources().getDisplayMetrics().density * dp);
+ }
+
+ public static int getPixelsFromScaledDensity(Context context, int sp) {
+
+ return (int) (context.getResources().getDisplayMetrics().scaledDensity * sp);
+ }
+
}
diff --git a/app/src/main/java/org/mian/gitnex/helpers/InfiniteScrollListener.java b/app/src/main/java/org/mian/gitnex/helpers/InfiniteScrollListener.java
new file mode 100644
index 00000000..99005295
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/helpers/InfiniteScrollListener.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 Piotr Wittchen
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mian.gitnex.helpers;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * InfiniteScrollListener, which can be added to RecyclerView with addOnScrollListener
+ * to detect moment when RecyclerView was scrolled to the end.
+ */
+public abstract class InfiniteScrollListener extends RecyclerView.OnScrollListener {
+ private final int maxItemsPerRequest;
+ private final LinearLayoutManager layoutManager;
+
+ /**
+ * Initializes InfiniteScrollListener, which can be added
+ * to RecyclerView with addOnScrollListener method
+ *
+ * @param maxItemsPerRequest Max items to be loaded in a single request.
+ * @param layoutManager LinearLayoutManager created in the Activity.
+ */
+ public InfiniteScrollListener(int maxItemsPerRequest, LinearLayoutManager layoutManager) {
+ assert maxItemsPerRequest > 0;
+ assert layoutManager != null;
+
+ this.maxItemsPerRequest = maxItemsPerRequest;
+ this.layoutManager = layoutManager;
+ }
+
+ /**
+ * Callback method to be invoked when the RecyclerView has been scrolled
+ *
+ * @param recyclerView The RecyclerView which scrolled.
+ * @param dx The amount of horizontal scroll.
+ * @param dy The amount of vertical scroll.
+ */
+ @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+ if (canLoadMoreItems()) {
+ onScrolledToEnd(layoutManager.findFirstVisibleItemPosition());
+ }
+ }
+
+ /**
+ * Refreshes RecyclerView by setting new adapter,
+ * calling invalidate method and scrolling to given position
+ *
+ * @param view RecyclerView to be refreshed
+ * @param adapter adapter with new list of items to be loaded
+ * @param position position to which RecyclerView will be scrolled
+ */
+ protected void refreshView(RecyclerView view, RecyclerView.Adapter adapter, int position) {
+ view.setAdapter(adapter);
+ view.invalidate();
+ view.scrollToPosition(position);
+ }
+
+ /**
+ * Checks if more items can be loaded to the RecyclerView
+ *
+ * @return boolean Returns true if can load more items or false if not.
+ */
+ protected boolean canLoadMoreItems() {
+ final int visibleItemsCount = layoutManager.getChildCount();
+ final int totalItemsCount = layoutManager.getItemCount();
+ final int pastVisibleItemsCount = layoutManager.findFirstVisibleItemPosition();
+ final boolean lastItemShown = visibleItemsCount + pastVisibleItemsCount >= totalItemsCount;
+ return lastItemShown && totalItemsCount >= maxItemsPerRequest;
+ }
+
+ /**
+ * Callback method to be invoked when the RecyclerView has been scrolled to the end
+ *
+ * @param firstVisibleItemPosition Id of the first visible item on the list.
+ */
+ public abstract void onScrolledToEnd(final int firstVisibleItemPosition);
+}
diff --git a/app/src/main/java/org/mian/gitnex/helpers/StaticGlobalVariables.java b/app/src/main/java/org/mian/gitnex/helpers/StaticGlobalVariables.java
index 7f261578..1ba029a3 100644
--- a/app/src/main/java/org/mian/gitnex/helpers/StaticGlobalVariables.java
+++ b/app/src/main/java/org/mian/gitnex/helpers/StaticGlobalVariables.java
@@ -1,38 +1,48 @@
package org.mian.gitnex.helpers;
+import android.content.Context;
+
/**
* Author M M Arif
*/
-public interface StaticGlobalVariables {
+public abstract class StaticGlobalVariables {
// generic values
- int resultLimitNewGiteaInstances = 25; // Gitea 1.12 and above
- int resultLimitOldGiteaInstances = 10; // Gitea 1.11 and below
+ public static int resultLimitNewGiteaInstances = 25; // Gitea 1.12 and above
+ public static int resultLimitOldGiteaInstances = 10; // Gitea 1.11 and below
+ public static String defaultOldestTimestamp = "1970-01-01T00:00:00+00:00";
+
+ public static int getCurrentResultLimit(Context context) {
+
+ Version version = new Version(new TinyDB(context).getString("giteaVersion"));
+ return version.higherOrEqual("1.12") ? resultLimitNewGiteaInstances : resultLimitOldGiteaInstances;
+
+ }
// tags
- String tagMilestonesFragment = "MilestonesFragment";
- String tagPullRequestsList = "PullRequestsListFragment";
- String tagIssuesList = "IssuesListFragment";
- String tagMilestonesAdapter = "MilestonesAdapter";
- String draftsRepository = "DraftsRepository";
- String repositoriesRepository = "RepositoriesRepository";
- String replyToIssueActivity = "ReplyToIssueActivity";
- String tagDraftsBottomSheet = "BottomSheetDraftsFragment";
- String userAccountsRepository = "UserAccountsRepository";
+ public static String tagMilestonesFragment = "MilestonesFragment";
+ public static String tagPullRequestsList = "PullRequestsListFragment";
+ public static String tagIssuesList = "IssuesListFragment";
+ public static String tagMilestonesAdapter = "MilestonesAdapter";
+ public static String draftsRepository = "DraftsRepository";
+ public static String repositoriesRepository = "RepositoriesRepository";
+ public static String replyToIssueActivity = "ReplyToIssueActivity";
+ public static String tagDraftsBottomSheet = "BottomSheetDraftsFragment";
+ public static String userAccountsRepository = "UserAccountsRepository";
// issues variables
- int issuesPageInit = 1;
- String issuesRequestType = "issues";
+ public static int issuesPageInit = 1;
+ public static String issuesRequestType = "issues";
// pull request
- int prPageInit = 1;
+ public static int prPageInit = 1;
// milestone
- int milestonesPageInit = 1;
+ public static int milestonesPageInit = 1;
// drafts
- String draftTypeComment = "comment";
- String draftTypeIssue = "issue";
+ public static String draftTypeComment = "comment";
+ public static String draftTypeIssue = "issue";
}
diff --git a/app/src/main/java/org/mian/gitnex/helpers/TinyDB.java b/app/src/main/java/org/mian/gitnex/helpers/TinyDB.java
index 2059f17e..fd75f04e 100644
--- a/app/src/main/java/org/mian/gitnex/helpers/TinyDB.java
+++ b/app/src/main/java/org/mian/gitnex/helpers/TinyDB.java
@@ -211,6 +211,10 @@ public class TinyDB {
return preferences.getFloat(key, 0);
}
+ public float getFloat(String key, float defaultValue) {
+ return preferences.getFloat(key, defaultValue);
+ }
+
/**
* Get double value from SharedPreferences at 'key'. If exception thrown, return 'defaultValue'
* @param key SharedPreferences key
@@ -292,6 +296,10 @@ public class TinyDB {
return preferences.getBoolean(key, false);
}
+ public boolean getBoolean(String key, boolean defaultValue) {
+ return preferences.getBoolean(key, defaultValue);
+ }
+
/**
* Get parsed ArrayList of Boolean from SharedPreferences at 'key'
* @param key SharedPreferences key
@@ -357,7 +365,7 @@ public class TinyDB {
*/
public void putListInt(String key, ArrayList intList) {
checkForNullKey(key);
- Integer[] myIntList = intList.toArray(new Integer[intList.size()]);
+ Integer[] myIntList = intList.toArray(new Integer[0]);
preferences.edit().putString(key, TextUtils.join("‚‗‚", myIntList)).apply();
}
@@ -378,7 +386,7 @@ public class TinyDB {
*/
public void putListLong(String key, ArrayList longList) {
checkForNullKey(key);
- Long[] myLongList = longList.toArray(new Long[longList.size()]);
+ Long[] myLongList = longList.toArray(new Long[0]);
preferences.edit().putString(key, TextUtils.join("‚‗‚", myLongList)).apply();
}
diff --git a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java
index b110a898..a693e5b2 100644
--- a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java
+++ b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java
@@ -17,6 +17,7 @@ import org.mian.gitnex.models.Labels;
import org.mian.gitnex.models.MergePullRequest;
import org.mian.gitnex.models.Milestones;
import org.mian.gitnex.models.NewFile;
+import org.mian.gitnex.models.NotificationThread;
import org.mian.gitnex.models.OrgOwner;
import org.mian.gitnex.models.Organization;
import org.mian.gitnex.models.OrganizationRepository;
@@ -75,6 +76,27 @@ public interface ApiInterface {
@POST("users/{username}/tokens") // create new token with 2fa otp
Call createNewTokenWithOTP(@Header("Authorization") String authorization, @Header("X-Gitea-OTP") int loginOTP, @Path("username") String loginUid, @Body UserTokens jsonStr);
+ @GET("notifications") // List users's notification threads
+ Call> getNotificationThreads(@Header("Authorization") String token, @Query("all") Boolean all, @Query("status-types") String[] statusTypes, @Query("since") String since, @Query("before") String before, @Query("page") Integer page, @Query("limit") Integer limit);
+
+ @PUT("notifications") // Mark notification threads as read, pinned or unread
+ Call markNotificationThreadsAsRead(@Header("Authorization") String token, @Query("last_read_at") String last_read_at, @Query("all") Boolean all, @Query("status-types") String[] statusTypes, @Query("to-status") String toStatus);
+
+ @GET("notifications/new") // Check if unread notifications exist
+ Call checkUnreadNotifications(@Header("Authorization") String token);
+
+ @GET("notifications/threads/{id}") // Get notification thread by ID
+ Call getNotificationThread(@Header("Authorization") String token, @Path("id") Integer id);
+
+ @PATCH("notifications/threads/{id}") // Mark notification thread as read by ID
+ Call markNotificationThreadAsRead(@Header("Authorization") String token, @Path("id") Integer id, @Query("to-status") String toStatus);
+
+ @GET("repos/{owner}/{repo}/notifications") // List users's notification threads on a specific repo
+ Call> getRepoNotificationThreads(@Header("Authorization") String token, @Path("owner") String owner, @Path("repo") String repo, @Query("all") String all, @Query("status-types") String[] statusTypes, @Query("since") String since, @Query("before") String before, @Query("page") String page, @Query("limit") String limit);
+
+ @PUT("repos/{owner}/{repo}/notifications") // Mark notification threads as read, pinned or unread on a specific repo
+ Call markRepoNotificationThreadsAsRead(@Header("Authorization") String token, @Path("owner") String owner, @Path("repo") String repo, @Query("all") Boolean all, @Query("status-types") String[] statusTypes, @Query("to-status") String toStatus, @Query("last_read_at") String last_read_at);
+
@GET("user/orgs") // get user organizations
Call> getUserOrgs(@Header("Authorization") String token);
diff --git a/app/src/main/java/org/mian/gitnex/models/NotificationSubject.java b/app/src/main/java/org/mian/gitnex/models/NotificationSubject.java
new file mode 100644
index 00000000..3c05f69d
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/models/NotificationSubject.java
@@ -0,0 +1,37 @@
+package org.mian.gitnex.models;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationSubject {
+ private String latest_comment_url;
+ private String title;
+ private String type;
+ private String url;
+
+ public NotificationSubject(String latest_comment_url, String title, String type, String url) {
+
+ this.latest_comment_url = latest_comment_url;
+ this.title = title;
+ this.type = type;
+ this.url = url;
+ }
+
+ public String getLatest_comment_url() {
+ return latest_comment_url;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/models/NotificationThread.java b/app/src/main/java/org/mian/gitnex/models/NotificationThread.java
new file mode 100644
index 00000000..322400f5
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/models/NotificationThread.java
@@ -0,0 +1,55 @@
+package org.mian.gitnex.models;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationThread {
+ private int id;
+ private boolean pinned;
+ private UserRepositories repository;
+ private NotificationSubject subject;
+ private boolean unread;
+ private String updated_at;
+ private String url;
+
+ public NotificationThread(int id, boolean pinned, UserRepositories repository, NotificationSubject subject, boolean unread, String updated_at, String url) {
+
+ this.id = id;
+ this.pinned = pinned;
+ this.repository = repository;
+ this.subject = subject;
+ this.unread = unread;
+ this.updated_at = updated_at;
+ this.url = url;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public boolean isPinned() {
+ return pinned;
+ }
+
+ public UserRepositories getRepository() {
+ return repository;
+ }
+
+ public NotificationSubject getSubject() {
+ return subject;
+ }
+
+ public boolean isUnread() {
+ return unread;
+ }
+
+ public String getUpdated_at() {
+ return updated_at;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/notifications/NotificationsMaster.java b/app/src/main/java/org/mian/gitnex/notifications/NotificationsMaster.java
new file mode 100644
index 00000000..8449f75c
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/notifications/NotificationsMaster.java
@@ -0,0 +1,67 @@
+package org.mian.gitnex.notifications;
+
+import android.content.Context;
+import android.os.Build;
+import androidx.work.Constraints;
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.NetworkType;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
+import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.helpers.Version;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationsMaster {
+
+ private static int notificationsSupported = -1;
+
+ private static void checkVersion(TinyDB tinyDB) {
+
+ String currentVersion = tinyDB.getString("giteaVersion");
+
+ if(tinyDB.getBoolean("loggedInMode") && !currentVersion.isEmpty()) {
+
+ notificationsSupported = new Version(currentVersion).higherOrEqual("1.12.3") ? 1 : 0;
+ }
+ }
+
+ public static void fireWorker(Context context) {
+
+ WorkManager.getInstance(context).cancelAllWorkByTag(context.getPackageName());
+ }
+
+ public static void hireWorker(Context context) {
+
+ TinyDB tinyDB = new TinyDB(context);
+
+ if(notificationsSupported == -1) {
+ checkVersion(tinyDB);
+ }
+
+ if(notificationsSupported == 1) {
+
+ Constraints.Builder constraints = new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresBatteryNotLow(false)
+ .setRequiresStorageNotLow(false)
+ .setRequiresCharging(false);
+
+ if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+
+ constraints.setRequiresDeviceIdle(false);
+ }
+
+ PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(NotificationsWorker.class, tinyDB.getInt("pollingDelayMinutes"), TimeUnit.MINUTES)
+ .setConstraints(constraints.build())
+ .addTag(context.getPackageName())
+ .build();
+
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(context.getPackageName(), ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest);
+
+ }
+ }
+}
diff --git a/app/src/main/java/org/mian/gitnex/notifications/NotificationsWorker.java b/app/src/main/java/org/mian/gitnex/notifications/NotificationsWorker.java
new file mode 100644
index 00000000..c92284ba
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/notifications/NotificationsWorker.java
@@ -0,0 +1,160 @@
+package org.mian.gitnex.notifications;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.media.RingtoneManager;
+import android.os.Build;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import org.mian.gitnex.R;
+import org.mian.gitnex.activities.MainActivity;
+import org.mian.gitnex.clients.RetrofitClient;
+import org.mian.gitnex.helpers.AppUtil;
+import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.models.NotificationThread;
+import java.util.Date;
+import java.util.List;
+import retrofit2.Call;
+import retrofit2.Response;
+
+/**
+ * Author opyale
+ */
+
+public class NotificationsWorker extends Worker {
+
+ private static final int MAXIMUM_NOTIFICATIONS = 100;
+ private static final long[] VIBRATION_PATTERN = new long[]{ 1000, 1000 };
+
+ private Context context;
+ private TinyDB tinyDB;
+
+ public NotificationsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+
+ super(context, workerParams);
+
+ this.context = context;
+ this.tinyDB = new TinyDB(context);
+
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+
+ String instanceUrl = tinyDB.getString("instanceUrl");
+ String token = "token " + tinyDB.getString(tinyDB.getString("loginUid") + "-token");
+
+ int notificationLoops = tinyDB.getInt("pollingDelayMinutes") >= 15 ? 1 : Math.min(15 - tinyDB.getInt("pollingDelayMinutes"), 10);
+
+ for(int i=0; i> call = RetrofitClient.getInstance(instanceUrl, context)
+ .getApiInterface()
+ .getNotificationThreads(token, false, new String[]{"unread"}, previousRefreshTimestamp,
+ null, 1, MAXIMUM_NOTIFICATIONS);
+
+ Response> response = call.execute();
+
+ if(response.code() == 200) {
+
+ assert response.body() != null;
+
+ List notificationThreads = response.body();
+ Log.i("ReceivedNotifications", String.valueOf(notificationThreads.size()));
+
+ if(!notificationThreads.isEmpty()) {
+
+ for(NotificationThread notificationThread : notificationThreads) {
+
+ sendNotification(notificationThread);
+ }
+ }
+
+ tinyDB.putString("previousRefreshTimestamp", AppUtil.getTimestampFromDate(context, new Date()));
+
+ } else {
+
+ Log.e("onError", String.valueOf(response.code()));
+ }
+ } catch(Exception e) {
+
+ Log.e("onError", e.toString());
+ }
+
+ try {
+
+ if(notificationLoops > 1 && i < (notificationLoops - 1)) {
+
+ Thread.sleep(60000 - (System.currentTimeMillis() - startPollingTime));
+ }
+ } catch (InterruptedException ignored) {}
+ }
+
+ return Result.success();
+
+ }
+
+ private void sendNotification(NotificationThread notificationThread) {
+
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra("launchFragment", "notifications");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if(notificationManager != null) {
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+
+ NotificationChannel notificationChannel = new NotificationChannel(context.getPackageName(), context.getString(R.string.app_name),
+ NotificationManager.IMPORTANCE_HIGH);
+
+ notificationChannel.enableLights(true);
+ notificationChannel.setLightColor(Color.GREEN);
+ notificationChannel.enableVibration(true);
+ notificationChannel.setVibrationPattern(VIBRATION_PATTERN);
+
+ notificationManager.createNotificationChannel(notificationChannel);
+
+ }
+
+ String subjectUrl = notificationThread.getSubject().getUrl();
+ String issueId = context.getResources().getString(R.string.hash) + subjectUrl.substring(subjectUrl.lastIndexOf("/") + 1);
+
+ String notificationHeader = issueId + " " + notificationThread.getSubject().getTitle();
+ String notificationBody = String.format(context.getResources().getString(R.string.notificationBody),
+ notificationThread.getSubject().getType());
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, context.getPackageName())
+ .setSmallIcon(R.drawable.gitnex_transparent).setContentTitle(notificationHeader)
+ .setContentText(notificationBody)
+ .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setContentIntent(pendingIntent).setVibrate(VIBRATION_PATTERN).setAutoCancel(true);
+
+ int previousNotificationId = tinyDB.getInt("previousNotificationId", 0);
+ int newPreviousNotificationId = previousNotificationId > 71951418 ? 0 : previousNotificationId + 1;
+
+ tinyDB.putInt("previousNotificationId", newPreviousNotificationId);
+
+ notificationManager.notify(previousNotificationId, builder.build());
+
+ }
+ }
+
+}
diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml
new file mode 100644
index 00000000..7fbbb611
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml
new file mode 100644
index 00000000..c8d666e3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_settings_security.xml b/app/src/main/res/layout/activity_settings_security.xml
index aa4a938f..5b6b6aad 100644
--- a/app/src/main/res/layout/activity_settings_security.xml
+++ b/app/src/main/res/layout/activity_settings_security.xml
@@ -156,4 +156,34 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/bottom_sheet_notifications.xml b/app/src/main/res/layout/bottom_sheet_notifications.xml
new file mode 100644
index 00000000..18f38130
--- /dev/null
+++ b/app/src/main/res/layout/bottom_sheet_notifications.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/bottom_sheet_notifications_filter.xml b/app/src/main/res/layout/bottom_sheet_notifications_filter.xml
new file mode 100644
index 00000000..54969a3e
--- /dev/null
+++ b/app/src/main/res/layout/bottom_sheet_notifications_filter.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml
new file mode 100644
index 00000000..e84517a2
--- /dev/null
+++ b/app/src/main/res/layout/fragment_notifications.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_notifications.xml b/app/src/main/res/layout/list_notifications.xml
new file mode 100644
index 00000000..ddc12ce2
--- /dev/null
+++ b/app/src/main/res/layout/list_notifications.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml
index 5b233e61..5d517bbe 100644
--- a/app/src/main/res/menu/drawer_menu.xml
+++ b/app/src/main/res/menu/drawer_menu.xml
@@ -5,41 +5,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/app/src/main/res/menu/filter_menu_notifications.xml b/app/src/main/res/menu/filter_menu_notifications.xml
new file mode 100644
index 00000000..c1eb34ed
--- /dev/null
+++ b/app/src/main/res/menu/filter_menu_notifications.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d9e39298..b722aeed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -618,11 +618,31 @@
Themes, fonts, badges, code block theme
PDF mode, source code theme
- SSL certificates, cache
+ SSL certificates, cache, polling delay
Languages
Crash reports
Archived
Account deleted successfully
+
+
+ Notifications
+ No notifications found
+
+ You have received a new notification. (%s)
+
+ Notifications Polling Delay
+ %d Minutes
+ Select Polling Delay
+ Choose a minutely delay in which GitNex tries to poll new notifications.
+
+ Mark as Read
+ Mark as Unread
+ Pin Notification
+ Successfully marked all notifications as read.
+
+ Read
+ Unread
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index a35a7e4e..0bb7bf10 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -7,6 +7,7 @@
- monospace
- @color/colorAccent
- @color/colorWhite
+ - @color/colorWhite
- @color/diffAddedColor
- @color/diffRemovedColor
@@ -35,6 +36,7 @@
- monospace
- @color/colorAccent
- @color/lightThemeTextColor
+ - @color/lightThemeTextColor
- @color/lightThemeDiffAddedColor
- @color/lightThemeDiffRemovedColor