Implement undo snackbar when deleting messages by swiping.
When swiping away a message it is only removed from the local message lists and a snackbar appears. When the snackbar is dismissed or the user deletes another message the delete request is sent to the server. If the user presses "undo" on the snackbar the message is reinserted into the local lists at its previous position.
This commit is contained in:
committed by
Jannis Mattheis
parent
1d0ec1fe30
commit
c22c8c1417
@@ -48,11 +48,11 @@ public class ListMessageAdapter extends RecyclerView.Adapter<ListMessageAdapter.
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageWithImage getItem(int position) {
|
public List<MessageWithImage> getItems() {
|
||||||
return items.get(position);
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
void items(List<MessageWithImage> items) {
|
public void setItems(List<MessageWithImage> items) {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ import com.github.gotify.messages.provider.ApplicationHolder;
|
|||||||
import com.github.gotify.messages.provider.MessageFacade;
|
import com.github.gotify.messages.provider.MessageFacade;
|
||||||
import com.github.gotify.messages.provider.MessageState;
|
import com.github.gotify.messages.provider.MessageState;
|
||||||
import com.github.gotify.messages.provider.MessageWithImage;
|
import com.github.gotify.messages.provider.MessageWithImage;
|
||||||
|
import com.github.gotify.messages.provider.PositionPair;
|
||||||
import com.github.gotify.service.WebSocketService;
|
import com.github.gotify.service.WebSocketService;
|
||||||
import com.google.android.material.navigation.NavigationView;
|
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.OkHttp3Downloader;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.squareup.picasso.Target;
|
import com.squareup.picasso.Target;
|
||||||
@@ -212,8 +215,8 @@ public class MessagesActivity extends AppCompatActivity
|
|||||||
startActivity(browserIntent);
|
startActivity(browserIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(Message message) {
|
public void commitDelete() {
|
||||||
new DeleteMessage().execute(message);
|
new CommitDeleteMessage().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onUpdateApps(List<Application> applications) {
|
protected void onUpdateApps(List<Application> applications) {
|
||||||
@@ -360,6 +363,54 @@ public class MessagesActivity extends AppCompatActivity
|
|||||||
picasso.shutdown();
|
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<Snackbar> {
|
||||||
|
@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 class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
|
||||||
private ListMessageAdapter adapter;
|
private ListMessageAdapter adapter;
|
||||||
private Drawable icon;
|
private Drawable icon;
|
||||||
@@ -394,8 +445,8 @@ public class MessagesActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||||
int position = viewHolder.getAdapterPosition();
|
int position = viewHolder.getAdapterPosition();
|
||||||
Message message = adapter.getItem(position).message;
|
MessageWithImage message = adapter.getItems().get(position);
|
||||||
MessagesActivity.this.delete(message);
|
scheduleDeletion(position, message.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -561,11 +612,11 @@ public class MessagesActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DeleteMessage extends AsyncTask<Message, Void, Void> {
|
private class CommitDeleteMessage extends AsyncTask<Void, Void, Void> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(Message... messages) {
|
protected Void doInBackground(Void... messages) {
|
||||||
MessagesActivity.this.messages.delete(first(messages));
|
MessagesActivity.this.messages.commitDelete();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,7 +699,7 @@ public class MessagesActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
ListMessageAdapter adapter = (ListMessageAdapter) messagesView.getAdapter();
|
ListMessageAdapter adapter = (ListMessageAdapter) messagesView.getAdapter();
|
||||||
adapter.items(messageWithImages);
|
adapter.setItems(messageWithImages);
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class MessageFacade {
|
|||||||
private final MessageRequester requester;
|
private final MessageRequester requester;
|
||||||
private final MessageStateHolder state;
|
private final MessageStateHolder state;
|
||||||
private final MessageImageCombiner combiner;
|
private final MessageImageCombiner combiner;
|
||||||
|
private Message messagePendingDeletion;
|
||||||
|
|
||||||
public MessageFacade(MessageApi api, ApplicationHolder applicationHolder) {
|
public MessageFacade(MessageApi api, ApplicationHolder applicationHolder) {
|
||||||
this.applicationHolder = applicationHolder;
|
this.applicationHolder = applicationHolder;
|
||||||
@@ -33,6 +34,13 @@ public class MessageFacade {
|
|||||||
if (state.hasNext || !state.loaded) {
|
if (state.hasNext || !state.loaded) {
|
||||||
PagedMessages pagedMessages = requester.loadMore(state);
|
PagedMessages pagedMessages = requester.loadMore(state);
|
||||||
this.state.newMessages(appId, pagedMessages);
|
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);
|
return get(appId);
|
||||||
}
|
}
|
||||||
@@ -52,9 +60,24 @@ public class MessageFacade {
|
|||||||
return state.getLastReceivedMessage();
|
return state.getLastReceivedMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(Message message) {
|
public void deleteLocal(Message message) {
|
||||||
this.requester.asyncRemoveMessage(message);
|
|
||||||
this.state.removeMessage(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) {
|
public boolean deleteAll(Integer appId) {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ class MessageStateHolder {
|
|||||||
private int lastReceivedMessage = -1;
|
private int lastReceivedMessage = -1;
|
||||||
private Map<Integer, MessageState> states = new HashMap<>();
|
private Map<Integer, MessageState> states = new HashMap<>();
|
||||||
|
|
||||||
|
private Message lastRemovedMessage;
|
||||||
|
private int lastRemovedAllPosition;
|
||||||
|
private int lastRemovedAppPosition;
|
||||||
|
|
||||||
synchronized void clear() {
|
synchronized void clear() {
|
||||||
states = new HashMap<>();
|
states = new HashMap<>();
|
||||||
}
|
}
|
||||||
@@ -30,17 +34,7 @@ class MessageStateHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized void newMessage(Message message) {
|
synchronized void newMessage(Message message) {
|
||||||
MessageState allMessages = state(MessageState.ALL_MESSAGES);
|
addMessage(message, 0, 0);
|
||||||
MessageState appMessages = state(message.getAppid());
|
|
||||||
|
|
||||||
if (allMessages.loaded) {
|
|
||||||
allMessages.messages.add(0, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appMessages.loaded) {
|
|
||||||
appMessages.messages.add(0, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastReceivedMessage = message.getId();
|
lastReceivedMessage = message.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +71,45 @@ class MessageStateHolder {
|
|||||||
MessageState appMessages = state(message.getAppid());
|
MessageState appMessages = state(message.getAppid());
|
||||||
|
|
||||||
if (allMessages.loaded) {
|
if (allMessages.loaded) {
|
||||||
allMessages.messages.remove(message);
|
int allPosition = allMessages.messages.indexOf(message);
|
||||||
|
allMessages.messages.remove(allPosition);
|
||||||
|
lastRemovedAllPosition = allPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appMessages.loaded) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
<string name="grouped_message">Received %d messages while being disconnected</string>
|
<string name="grouped_message">Received %d messages while being disconnected</string>
|
||||||
<string name="delete_all">Delete all</string>
|
<string name="delete_all">Delete all</string>
|
||||||
<string name="delete_logs">Delete logs</string>
|
<string name="delete_logs">Delete logs</string>
|
||||||
|
<string name="snackbar_deleted">Message deleted</string>
|
||||||
|
<string name="snackbar_undo">Undo</string>
|
||||||
<string name="all_messages">All Messages</string>
|
<string name="all_messages">All Messages</string>
|
||||||
<string name="logs">Logs</string>
|
<string name="logs">Logs</string>
|
||||||
<string name="message_image_desc">The image of a message</string>
|
<string name="message_image_desc">The image of a message</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user