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

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