diff --git a/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java b/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java index ce30792..8aa4823 100644 --- a/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java +++ b/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java @@ -48,11 +48,11 @@ public class ListMessageAdapter extends RecyclerView.Adapter getItems() { + return items; } - void items(List items) { + public void setItems(List items) { this.items = items; } diff --git a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java index 9e16370..c66ae4d 100644 --- a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java +++ b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java @@ -58,8 +58,11 @@ import com.github.gotify.messages.provider.ApplicationHolder; import com.github.gotify.messages.provider.MessageFacade; import com.github.gotify.messages.provider.MessageState; import com.github.gotify.messages.provider.MessageWithImage; +import com.github.gotify.messages.provider.PositionPair; import com.github.gotify.service.WebSocketService; import com.google.android.material.navigation.NavigationView; +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; import com.squareup.picasso.OkHttp3Downloader; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; @@ -212,8 +215,8 @@ public class MessagesActivity extends AppCompatActivity startActivity(browserIntent); } - public void delete(Message message) { - new DeleteMessage().execute(message); + public void commitDelete() { + new CommitDeleteMessage().execute(); } protected void onUpdateApps(List applications) { @@ -360,6 +363,54 @@ public class MessagesActivity extends AppCompatActivity picasso.shutdown(); } + private void scheduleDeletion(int position, Message message) { + ListMessageAdapter adapter = (ListMessageAdapter) messagesView.getAdapter(); + + messages.deleteLocal(message); + adapter.setItems(messages.get(appId)); + adapter.notifyItemRemoved(position); + + showDeletionSnackbar(); + } + + private void undoDelete() { + PositionPair positionPair = messages.undoDeleteLocal(); + + if (positionPair != null) { + ListMessageAdapter adapter = (ListMessageAdapter) messagesView.getAdapter(); + adapter.setItems(messages.get(appId)); + int insertPosition = + appId == MessageState.ALL_MESSAGES + ? positionPair.getAllPosition() + : positionPair.getAppPosition(); + adapter.notifyItemInserted(insertPosition); + messagesView.smoothScrollToPosition(insertPosition); + } + } + + private void showDeletionSnackbar() { + View view = swipeRefreshLayout; + Snackbar snackbar = Snackbar.make(view, R.string.snackbar_deleted, Snackbar.LENGTH_LONG); + snackbar.setAction(R.string.snackbar_undo, v -> undoDelete()); + snackbar.addCallback(new SnackbarCallback()); + snackbar.show(); + } + + private class SnackbarCallback extends BaseTransientBottomBar.BaseCallback { + @Override + public void onDismissed(Snackbar transientBottomBar, int event) { + super.onDismissed(transientBottomBar, event); + if (event != DISMISS_EVENT_ACTION && event != DISMISS_EVENT_CONSECUTIVE) { + // Execute deletion when the snackbar disappeared without pressing the undo button + // DISMISS_EVENT_CONSECUTIVE should be excluded as well, because it would cause the + // deletion to be sent to the server twice, since the deletion is sent to the server + // in MessageFacade if a message is deleted while another message was already + // waiting for deletion. + MessagesActivity.this.commitDelete(); + } + } + } + private class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback { private ListMessageAdapter adapter; private Drawable icon; @@ -394,8 +445,8 @@ public class MessagesActivity extends AppCompatActivity @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition(); - Message message = adapter.getItem(position).message; - MessagesActivity.this.delete(message); + MessageWithImage message = adapter.getItems().get(position); + scheduleDeletion(position, message.message); } @Override @@ -561,11 +612,11 @@ public class MessagesActivity extends AppCompatActivity } } - private class DeleteMessage extends AsyncTask { + private class CommitDeleteMessage extends AsyncTask { @Override - protected Void doInBackground(Message... messages) { - MessagesActivity.this.messages.delete(first(messages)); + protected Void doInBackground(Void... messages) { + MessagesActivity.this.messages.commitDelete(); return null; } @@ -648,7 +699,7 @@ public class MessagesActivity extends AppCompatActivity } ListMessageAdapter adapter = (ListMessageAdapter) messagesView.getAdapter(); - adapter.items(messageWithImages); + adapter.setItems(messageWithImages); adapter.notifyDataSetChanged(); } diff --git a/app/src/main/java/com/github/gotify/messages/provider/MessageFacade.java b/app/src/main/java/com/github/gotify/messages/provider/MessageFacade.java index f3eaa2d..664e79b 100644 --- a/app/src/main/java/com/github/gotify/messages/provider/MessageFacade.java +++ b/app/src/main/java/com/github/gotify/messages/provider/MessageFacade.java @@ -10,6 +10,7 @@ public class MessageFacade { private final MessageRequester requester; private final MessageStateHolder state; private final MessageImageCombiner combiner; + private Message messagePendingDeletion; public MessageFacade(MessageApi api, ApplicationHolder applicationHolder) { this.applicationHolder = applicationHolder; @@ -33,6 +34,13 @@ public class MessageFacade { if (state.hasNext || !state.loaded) { PagedMessages pagedMessages = requester.loadMore(state); this.state.newMessages(appId, pagedMessages); + + // If there is a message with pending removal, it should not reappear in the list when + // reloading. Thus, it needs to be removed from the local list again after loading new + // messages. + if (messagePendingDeletion != null) { + this.state.removeMessage(messagePendingDeletion); + } } return get(appId); } @@ -52,9 +60,24 @@ public class MessageFacade { return state.getLastReceivedMessage(); } - public void delete(Message message) { - this.requester.asyncRemoveMessage(message); + public void deleteLocal(Message message) { this.state.removeMessage(message); + // If there is already a deletion pending, that one should be executed before scheduling the + // next deletion. + if (messagePendingDeletion != null) { + commitDelete(); + } + messagePendingDeletion = message; + } + + public void commitDelete() { + this.requester.asyncRemoveMessage(messagePendingDeletion); + messagePendingDeletion = null; + } + + public PositionPair undoDeleteLocal() { + messagePendingDeletion = null; + return this.state.undoLastRemoveMessage(); } public boolean deleteAll(Integer appId) { diff --git a/app/src/main/java/com/github/gotify/messages/provider/MessageStateHolder.java b/app/src/main/java/com/github/gotify/messages/provider/MessageStateHolder.java index 0f55164..08c8ccb 100644 --- a/app/src/main/java/com/github/gotify/messages/provider/MessageStateHolder.java +++ b/app/src/main/java/com/github/gotify/messages/provider/MessageStateHolder.java @@ -9,6 +9,10 @@ class MessageStateHolder { private int lastReceivedMessage = -1; private Map states = new HashMap<>(); + private Message lastRemovedMessage; + private int lastRemovedAllPosition; + private int lastRemovedAppPosition; + synchronized void clear() { states = new HashMap<>(); } @@ -30,17 +34,7 @@ class MessageStateHolder { } synchronized void newMessage(Message message) { - MessageState allMessages = state(MessageState.ALL_MESSAGES); - MessageState appMessages = state(message.getAppid()); - - if (allMessages.loaded) { - allMessages.messages.add(0, message); - } - - if (appMessages.loaded) { - appMessages.messages.add(0, message); - } - + addMessage(message, 0, 0); lastReceivedMessage = message.getId(); } @@ -77,11 +71,45 @@ class MessageStateHolder { MessageState appMessages = state(message.getAppid()); if (allMessages.loaded) { - allMessages.messages.remove(message); + int allPosition = allMessages.messages.indexOf(message); + allMessages.messages.remove(allPosition); + lastRemovedAllPosition = allPosition; } if (appMessages.loaded) { - appMessages.messages.remove(message); + int appPosition = appMessages.messages.indexOf(message); + appMessages.messages.remove(appPosition); + lastRemovedAppPosition = appPosition; + } + + lastRemovedMessage = message; + } + + PositionPair undoLastRemoveMessage() { + PositionPair result = null; + + if (lastRemovedMessage != null) { + addMessage(lastRemovedMessage, lastRemovedAllPosition, lastRemovedAppPosition); + result = new PositionPair(lastRemovedAllPosition, lastRemovedAppPosition); + + lastRemovedMessage = null; + lastRemovedAllPosition = -1; + lastRemovedAppPosition = -1; + } + + return result; + } + + private void addMessage(Message message, int allPosition, int appPosition) { + MessageState allMessages = state(MessageState.ALL_MESSAGES); + MessageState appMessages = state(message.getAppid()); + + if (allMessages.loaded && allPosition != -1) { + allMessages.messages.add(allPosition, message); + } + + if (appMessages.loaded && appPosition != -1) { + appMessages.messages.add(appPosition, message); } } } diff --git a/app/src/main/java/com/github/gotify/messages/provider/PositionPair.java b/app/src/main/java/com/github/gotify/messages/provider/PositionPair.java new file mode 100644 index 0000000..2f9fe03 --- /dev/null +++ b/app/src/main/java/com/github/gotify/messages/provider/PositionPair.java @@ -0,0 +1,19 @@ +package com.github.gotify.messages.provider; + +public final class PositionPair { + private final int allPosition; + private final int appPosition; + + public PositionPair(int allPosition, int appPosition) { + this.allPosition = allPosition; + this.appPosition = appPosition; + } + + public int getAllPosition() { + return allPosition; + } + + public int getAppPosition() { + return appPosition; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf3ed82..249604a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,8 @@ Received %d messages while being disconnected Delete all Delete logs + Message deleted + Undo All Messages Logs The image of a message