Notifications (#554)

Cleanup

Extending and improving notifications

Using new icons instead

Lowering polling delay to one minute and other improvements

Fixing minor issues

Simplifying progress layout

Fixing bugs and other improvements

Adding translations

Notifications

Co-authored-by: opyale <opyale@noreply.gitea.io>
Co-authored-by: 6543 <6543@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/554
Reviewed-by: 6543 <6543@noreply.codeberg.org>
Reviewed-by: M M Arif <mmarif@noreply.codeberg.org>
This commit is contained in:
opyale 2020-07-22 21:32:42 +02:00 committed by 6543
parent cd55f946f0
commit 39ac49b258
39 changed files with 1856 additions and 129 deletions

View File

@ -9,7 +9,6 @@ max_line_length = 150
[*.java]
indent_style = tab
max_line_length = 220
line_comment = //
block_comment_start = /*
block_comment = *

View File

@ -29,6 +29,7 @@
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="CATCH_ON_NEW_LINE" value="true" />
<option name="FINALLY_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="SPACE_BEFORE_IF_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_WHILE_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_FOR_PARENTHESES" value="false" />
@ -36,9 +37,13 @@
<option name="SPACE_BEFORE_CATCH_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_SWITCH_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_SYNCHRONIZED_PARENTHESES" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="THROWS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="WRAP_ON_TYPING" value="1" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />

View File

@ -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"
}

View File

@ -4,6 +4,7 @@
package="org.mian.gitnex">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
@ -14,6 +15,7 @@
android:roundIcon="@mipmap/app_logo_round"
android:supportsRtl="true"
tools:targetApi="n">
<activity android:name=".activities.MergePullRequestActivity" />
<activity
android:name=".activities.FileViewActivity"
@ -81,6 +83,7 @@
<activity android:name=".activities.SettingsReportsActivity" />
<activity android:name=".activities.AddNewTeamMemberActivity" />
<activity android:name=".activities.SettingsDraftsActivity" />
</application>
</manifest>

View File

@ -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<ResponseBody> 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<ResponseBody> call = RetrofitClient.getInstance(instanceUrl, context).getApiInterface()
.markNotificationThreadsAsRead(instanceToken, AppUtil.getTimestampFromDate(context, date), true,
new String[]{"unread", "pinned"}, "read");
return call.execute().isSuccessful();
}
}

View File

@ -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,6 +88,12 @@ public abstract class BaseActivity extends AppCompatActivity {
}
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);

View File

@ -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 {

View File

@ -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<List<IssueComments>>() {
@Override
public void onChanged(@Nullable List<IssueComments> 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,7 +330,6 @@ public class IssueDetailActivity extends BaseActivity {
adapter = new IssueCommentsAdapter(ctx, issueCommentsMain);
mRecyclerView.setAdapter(adapter);
}
});
}
@ -336,7 +337,8 @@ public class IssueDetailActivity extends BaseActivity {
private void getSingleIssue(String instanceUrl, String instanceToken, String repoOwner, String repoName, int issueIndex, String loginUid) {
final TinyDB tinyDb = new TinyDB(appCtx);
Call<Issues> call = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface().getIssueByIndex(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
Call<Issues> call = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface()
.getIssueByIndex(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
call.enqueue(new Callback<Issues>() {
@ -348,14 +350,16 @@ 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 -> {
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 int resourceId = ctx.getResources()
.getIdentifier(raw.substring("drawable://".length()), "drawable", ctx.getPackageName());
final Drawable drawable = ctx.getDrawable(resourceId);
@ -382,9 +386,11 @@ public class IssueDetailActivity extends BaseActivity {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.codeTextColor(tinyDb.getInt("codeBlockColor")).codeBackgroundColor(tinyDb.getInt("codeBlockBackground")).linkColor(getResources().getColor(R.color.lightBlue));
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_ = "<font color='" + appCtx.getResources().getColor(R.color.lightGray) + "'>" + appCtx.getResources().getString(R.string.hash) + singleIssue.getNumber() + "</font>";
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_ = "<font color='" + appCtx.getResources().getColor(R.color.lightGray) + "'>" + appCtx.getResources()
.getString(R.string.hash) + singleIssue.getNumber() + "</font>";
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<WatchInfo> call2 = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface().checkIssueWatchStatus(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
Call<WatchInfo> call2 = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface()
.checkIssueWatchStatus(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, issueIndex);
call2.enqueue(new Callback<WatchInfo>() {

View File

@ -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();

View File

@ -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());
}
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")) {
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");
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;
}
}
@ -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<GiteaVersion> callVersion, @NonNull Throwable t) {
Log.e("onFailure-version", t.toString());
}
});

View File

@ -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();
}
}

View File

@ -125,7 +125,7 @@ public class IssuesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
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<RecyclerView.ViewHolder>
TinyDB tinyDb = new TinyDB(context);
tinyDb.putString("issueNumber", issueNumber.getText().toString());
tinyDb.putString("issueType", "issue");
tinyDb.putString("issueType", "Issue");
context.startActivity(intent);
});

View File

@ -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<NotificationsAdapter.NotificationsViewHolder> {
private Context context;
private List<NotificationThread> notificationThreads;
private OnMoreClickedListener onMoreClickedListener;
private OnNotificationClickedListener onNotificationClickedListener;
public NotificationsAdapter(Context context, List<NotificationThread> 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 = "<font color='" + context.getResources().getColor(R.color.lightGray) + "'>" + context.getResources()
.getString(R.string.hash) + url.substring(url.lastIndexOf("/") + 1) + "</font>";
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);
}
}

View File

@ -136,7 +136,7 @@ public class PullRequestsAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
tinyDb.putString("prHeadBranch", prHeadBranch.getText().toString());
tinyDb.putString("prIsFork", prIsFork.getText().toString());
tinyDb.putString("prForkFullName", prForkFullName.getText().toString());
tinyDb.putString("issueType", "pr");
tinyDb.putString("issueType", "Pull");
context.startActivity(intent);
});
@ -155,7 +155,7 @@ public class PullRequestsAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
tinyDb.putString("prHeadBranch", prHeadBranch.getText().toString());
tinyDb.putString("prIsFork", prIsFork.getText().toString());
tinyDb.putString("prForkFullName", prForkFullName.getText().toString());
tinyDb.putString("issueType", "pr");
tinyDb.putString("issueType", "Pull");
context.startActivity(intent);
});

View File

@ -121,8 +121,6 @@ public class RepositoriesByOrgAdapter extends RecyclerView.Adapter<RepositoriesB
final String instanceUrl = tinyDb.getString("instanceUrl");
final String token = "token " + tinyDb.getString(tinyDb.getString("loginUid") + "-token");
WatchInfo watch = new WatchInfo();
Call<WatchInfo> call;
call = RetrofitClient.getInstance(instanceUrl, context).getApiInterface().checkRepoWatchStatus(token, repoOwner, repoName);

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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<NotificationThread> 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<List<NotificationThread>> call = RetrofitClient.getInstance(instanceUrl, context)
.getApiInterface()
.getNotificationThreads(instanceToken, false, filter,
StaticGlobalVariables.defaultOldestTimestamp, "",
pageCurrentIndex, pageResultLimit);
call.enqueue(new Callback<List<NotificationThread>>() {
@Override
public void onResponse(@NonNull Call<List<NotificationThread>> call, @NonNull Response<List<NotificationThread>> 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<List<NotificationThread>> 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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";
}

View File

@ -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<Integer> 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<Long> 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();
}

View File

@ -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<UserTokens> 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<List<NotificationThread>> 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<ResponseBody> 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<JsonElement> checkUnreadNotifications(@Header("Authorization") String token);
@GET("notifications/threads/{id}") // Get notification thread by ID
Call<NotificationThread> getNotificationThread(@Header("Authorization") String token, @Path("id") Integer id);
@PATCH("notifications/threads/{id}") // Mark notification thread as read by ID
Call<ResponseBody> 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<List<NotificationThread>> 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<ResponseBody> 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<List<UserOrganizations>> getUserOrgs(@Header("Authorization") String token);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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<notificationLoops; i++) {
long startPollingTime = System.currentTimeMillis();
try {
String previousRefreshTimestamp = tinyDB.getString("previousRefreshTimestamp", AppUtil.getTimestampFromDate(context, new Date()));
Call<List<NotificationThread>> call = RetrofitClient.getInstance(instanceUrl, context)
.getApiInterface()
.getNotificationThreads(token, false, new String[]{"unread"}, previousRefreshTimestamp,
null, 1, MAXIMUM_NOTIFICATIONS);
Response<List<NotificationThread>> response = call.execute();
if(response.code() == 200) {
assert response.body() != null;
List<NotificationThread> 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());
}
}
}

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,8A6,6 0,0 0,6 8c0,7 -3,9 -3,9h18s-3,-2 -3,-9"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#368f73"
android:strokeLineCap="round"/>
<path
android:pathData="M13.73,21a2,2 0,0 1,-3.46 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#368f73"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#368f73"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7.886,1.553a1.75,1.75 0,0 1,2.869 0.604l0.633,1.629a5.666,5.666 0,0 0,3.725 3.395l3.959,1.131a1.75,1.75 0,0 1,0.757 2.92L16.06,15l5.594,5.595a0.75,0.75 0,1 1,-1.06 1.06L15,16.061l-3.768,3.768a1.75,1.75 0,0 1,-2.92 -0.757l-1.131,-3.96a5.667,5.667 0,0 0,-3.395 -3.724l-1.63,-0.633a1.75,1.75 0,0 1,-0.603 -2.869l6.333,-6.333zM14.475,14.465l-0.005,0.005 -0.005,0.005 -4.294,4.293a0.25,0.25 0,0 1,-0.417 -0.108l-1.13,-3.96A7.166,7.166 0,0 0,4.33 9.99L2.7,9.356a0.25,0.25 0,0 1,-0.086 -0.41l6.333,-6.332a0.25,0.25 0,0 1,0.41 0.086l0.633,1.63a7.167,7.167 0,0 0,4.71 4.293l3.96,1.131a0.25,0.25 0,0 1,0.108 0.417l-4.293,4.294z"
android:fillType="evenOdd"/>
</vector>

View File

@ -156,4 +156,34 @@
</LinearLayout>
<LinearLayout
android:id="@+id/pollingDelayFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/pollingDelayHeaderSelector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginTop="10dp"
android:layout_marginStart="44dp"
android:layout_marginEnd="4dp"
android:text="@string/notificationsPollingHeaderText"
android:textColor="?attr/primaryTextColor"/>
<TextView
android:id="@+id/pollingDelaySelected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginStart="44dp"
android:layout_marginEnd="4dp"
android:text="@string/pollingDelaySelectedText"
android:textColor="?attr/selectedTextColor"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="6dp"
android:paddingBottom="12dp"
android:background="?attr/primaryBackgroundColor">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:id="@+id/markPinned"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/pinNotification"
android:drawableStart="@drawable/ic_pin"
android:drawablePadding="24dp"
android:textColor="?attr/primaryTextColor"
android:textSize="16sp"
android:padding="12dp" />
<TextView
android:id="@+id/markRead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/markAsRead"
android:drawableStart="@drawable/ic_unwatch"
android:drawablePadding="24dp"
android:textColor="?attr/primaryTextColor"
android:textSize="16sp"
android:padding="12dp" />
<TextView
android:id="@+id/markUnread"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/markAsUnread"
android:drawableStart="@drawable/ic_watchers"
android:drawablePadding="24dp"
android:textColor="?attr/primaryTextColor"
android:textSize="16sp"
android:padding="12dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="6dp"
android:paddingBottom="12dp"
android:background="?attr/primaryBackgroundColor">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:id="@+id/unreadNotifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/isUnread"
android:drawableStart="@drawable/ic_watchers"
android:drawablePadding="24dp"
android:textColor="?attr/primaryTextColor"
android:textSize="16sp"
android:padding="12dp" />
<TextView
android:id="@+id/readNotifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/isRead"
android:drawableStart="@drawable/ic_unwatch"
android:drawablePadding="24dp"
android:textColor="?attr/primaryTextColor"
android:textSize="16sp"
android:padding="12dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBackgroundColor"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
<ProgressBar
android:id="@+id/loadingMoreView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"
android:padding="5dp" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/pullToRefresh"
android:layout_above="@id/loadingMoreView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notifications"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/noDataNotifications"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="15dp"
android:gravity="center"
android:text="@string/noDataNotifications"
android:textColor="?attr/primaryTextColor"
android:textSize="20sp"
android:visibility="gone" />
<ImageView
android:id="@+id/markAllAsRead"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="15dp"
android:background="@drawable/shape_circle"
android:contentDescription="@string/markAsRead"
android:padding="@dimen/fab_padding"
android:src="@drawable/ic_done"
android:tint="@color/colorWhite" />
</RelativeLayout>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryBackgroundColor"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginRight="20dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_pull_request" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/pinned"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_pin" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp" />
<TextView
android:id="@+id/repository"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="?attr/primaryTextColor" />
</LinearLayout>
<ImageView
android:id="@+id/more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginLeft="10dp"
android:background="@android:color/transparent"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_dotted_menu_horizontal" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/dividerColor" />
</LinearLayout>

View File

@ -5,41 +5,59 @@
<group android:checkableBehavior="single"
android:id="@+id/nav_main">
<item android:id="@+id/nav_home"
android:icon="@drawable/ic_repo"
android:title="@string/navMyRepos" />
<item android:id="@+id/nav_starred_repos"
android:icon="@drawable/ic_star_unfilled"
android:title="@string/navStarredRepos" />
<item android:id="@+id/nav_organizations"
android:icon="@drawable/ic_organization"
android:title="@string/navOrgs" />
<item android:id="@+id/nav_repositories"
android:icon="@drawable/ic_repo"
android:title="@string/navRepos" />
<item
android:id="@+id/nav_notifications"
android:icon="@drawable/ic_notifications"
android:title="@string/pageTitleNotifications"
android:visible="false" />
<item android:id="@+id/nav_explore"
android:icon="@drawable/ic_search"
android:title="@string/navExplore" />
<item android:id="@+id/nav_comments_draft"
android:icon="@drawable/ic_drafts"
android:title="@string/titleDrafts" />
<item android:id="@+id/nav_profile"
android:icon="@drawable/ic_person"
android:title="@string/navProfile" />
<item android:id="@+id/nav_administration"
android:icon="@drawable/ic_tool"
android:title="@string/navAdministration"
android:visible="false" />
</group>
<group android:checkableBehavior="single"
android:id="@+id/nav_menu_settings">
<item android:id="@+id/nav_settings"
android:icon="@drawable/ic_settings"
android:title="@string/navSettings" />
<item android:id="@+id/nav_about"
android:icon="@drawable/ic_info"
android:title="@string/navAbout" />
</group>
<item android:id="@+id/nav_rate_app"
@ -48,9 +66,11 @@
<group android:checkableBehavior="single"
android:id="@+id/nav_extra">
<item android:id="@+id/nav_logout"
android:icon="@drawable/ic_logout"
android:title="@string/navLogout" />
</group>
</menu>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/filterNotifications"
android:icon="@drawable/ic_filter"
android:title="@string/strFilter"
android:orderInCategory="0"
app:showAsAction="ifRoom" />
</menu>

View File

@ -618,11 +618,31 @@
<string name="appearanceHintText">Themes, fonts, badges, code block theme</string>
<string name="fileViewerHintText">PDF mode, source code theme</string>
<string name="securityHintText">SSL certificates, cache</string>
<string name="securityHintText">SSL certificates, cache, polling delay</string>
<string name="languagesHintText">Languages</string>
<string name="reportsHintText">Crash reports</string>
<string name="archivedRepository">Archived</string>
<string name="accountDeletedMessage">Account deleted successfully</string>
<!-- Notifications -->
<string name="pageTitleNotifications">Notifications</string>
<string name="noDataNotifications">No notifications found</string>
<string name="notificationBody">You have received a new notification. (%s)</string>
<string name="notificationsPollingHeaderText">Notifications Polling Delay</string>
<string name="pollingDelaySelectedText">%d Minutes</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 as Read</string>
<string name="markAsUnread">Mark as Unread</string>
<string name="pinNotification">Pin Notification</string>
<string name="markedNotificationsAsRead">Successfully marked all notifications as read.</string>
<string name="isRead">Read</string>
<string name="isUnread">Unread</string>
</resources>

View File

@ -7,6 +7,7 @@
<item name="android:typeface">monospace</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColorSecondary">@color/colorWhite</item>
<item name="android:textColorPrimary">@color/colorWhite</item>
<item name="diffAddedColor">@color/diffAddedColor</item>
<item name="diffRemovedColor">@color/diffRemovedColor</item>
@ -35,6 +36,7 @@
<item name="android:typeface">monospace</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColorSecondary">@color/lightThemeTextColor</item>
<item name="android:textColorPrimary">@color/lightThemeTextColor</item>
<item name="diffAddedColor">@color/lightThemeDiffAddedColor</item>
<item name="diffRemovedColor">@color/lightThemeDiffRemovedColor</item>