Skip to content

Commit dac5c7a

Browse files
jmattheisAlliotTech
authored andcommitted
feat: add delete message undo (#903)
Co-authored-by: AlliotTech <24980252+AlliotTech@users.noreply.github.com>
1 parent 6b1b1ba commit dac5c7a

3 files changed

Lines changed: 67 additions & 11 deletions

File tree

ui/src/message/Messages.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import LoadingSpinner from '../common/LoadingSpinner';
1212
import {useStores} from '../stores';
1313
import {Virtuoso} from 'react-virtuoso';
1414
import {PushMessageDialog} from './PushMessageDialog';
15+
import {enqueueSnackbar} from 'notistack';
16+
17+
const UndoAutoHideMs = 5000;
1518

1619
const Messages = observer(() => {
1720
const {id} = useParams<{id: string}>();
@@ -28,7 +31,25 @@ const Messages = observer(() => {
2831
const expandedState = React.useRef<Record<number, boolean>>({});
2932
const app = appId === -1 ? undefined : appStore.getByIDOrUndefined(appId);
3033

31-
const deleteMessage = (message: IMessage) => () => messagesStore.removeSingle(message);
34+
const deleteMessage = (message: IMessage) => {
35+
const key = enqueueSnackbar({
36+
message: 'Message deleted',
37+
variant: 'info',
38+
action: () => (
39+
<Button
40+
color="inherit"
41+
size="small"
42+
onClick={() => messagesStore.cancelPendingDelete(message)}>
43+
Undo
44+
</Button>
45+
),
46+
disableWindowBlurListener: true,
47+
transitionDuration: {enter: 0, exit: 0},
48+
autoHideDuration: UndoAutoHideMs,
49+
onExited: () => messagesStore.removeSingle(message),
50+
});
51+
messagesStore.addPendingDelete({message, key});
52+
};
3253

3354
React.useEffect(() => {
3455
if (!messagesStore.loaded(appId)) {
@@ -39,7 +60,7 @@ const Messages = observer(() => {
3960
const renderMessage = (_index: number, message: IMessage) => (
4061
<Message
4162
key={message.id}
42-
fDelete={deleteMessage(message)}
63+
fDelete={() => deleteMessage(message)}
4364
onExpand={(expanded) => (expandedState.current[message.id] = expanded)}
4465
title={message.title}
4566
date={message.date}

ui/src/message/MessagesStore.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as config from '../config';
55
import {createTransformer} from 'mobx-utils';
66
import {SnackReporter} from '../snack/SnackManager';
77
import {IApplication, IMessage, IPagedMessages} from '../types';
8+
import {closeSnackbar, SnackbarKey} from 'notistack';
89

910
const AllMessages = -1;
1011

@@ -15,17 +16,27 @@ interface MessagesState {
1516
loaded: boolean;
1617
}
1718

19+
interface PendingDelete {
20+
key: SnackbarKey;
21+
message: IMessage;
22+
}
23+
1824
export class MessagesStore {
1925
private state: Record<string, MessagesState> = {};
26+
private pendingDeletes: Map<number, PendingDelete> = observable.map();
2027

2128
private loading = false;
2229

2330
public constructor(
2431
private readonly appStore: BaseStore<IApplication>,
2532
private readonly snack: SnackReporter
2633
) {
27-
makeObservable<MessagesStore, 'state'>(this, {
34+
makeObservable<MessagesStore, 'state' | 'pendingDeletes'>(this, {
2835
state: observable,
36+
pendingDeletes: observable,
37+
addPendingDelete: action,
38+
executePendingDeletes: action,
39+
cancelPendingDelete: action,
2940
loadMore: action,
3041
publishSingleMessage: action,
3142
removeByApp: action,
@@ -95,15 +106,39 @@ export class MessagesStore {
95106
await this.loadMore(appId);
96107
};
97108

109+
public addPendingDelete = (pending: PendingDelete) =>
110+
this.pendingDeletes.set(pending.message.id, pending);
111+
112+
public cancelPendingDelete = (message: IMessage): boolean => {
113+
const pending = this.pendingDeletes.get(message.id);
114+
if (pending) {
115+
this.pendingDeletes.delete(message.id);
116+
closeSnackbar(pending.key);
117+
}
118+
return !!pending;
119+
};
120+
121+
public executePendingDeletes = () =>
122+
Array.from(this.pendingDeletes.values()).forEach(({message}) => this.removeSingle(message));
123+
124+
public visible = (message: number): boolean => !this.pendingDeletes.has(message);
125+
98126
public removeSingle = async (message: IMessage) => {
99-
await axios.delete(config.get('url') + 'message/' + message.id);
127+
if (!this.pendingDeletes.has(message.id)) {
128+
return;
129+
}
130+
131+
await axios.delete(config.get('url') + 'message/' + message.id, {
132+
adapter: 'fetch',
133+
fetchOptions: {keepalive: true},
134+
});
100135
if (this.exists(AllMessages)) {
101136
this.removeFromList(this.state[AllMessages].messages, message);
102137
}
103138
if (this.exists(message.appid)) {
104139
this.removeFromList(this.state[message.appid].messages, message);
105140
}
106-
this.snack('Message deleted');
141+
this.cancelPendingDelete(message);
107142
};
108143

109144
public sendMessage = async (
@@ -170,12 +205,9 @@ export class MessagesStore {
170205
.getItems()
171206
.reduce((all, app) => ({...all, [app.id]: app.image}), {});
172207

173-
return this.stateOf(appId, false).messages.map(
174-
(message: IMessage): IMessage => ({
175-
...message,
176-
image: appToImage[message.appid],
177-
})
178-
);
208+
return this.stateOf(appId, false)
209+
.messages.filter((message) => !this.pendingDeletes.has(message.id))
210+
.map((message: IMessage): IMessage => ({...message, image: appToImage[message.appid]}));
179211
};
180212

181213
public get = createTransformer(this.getUnCached);

ui/src/reactions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {StoreMapping} from './stores';
55
const AUDIO_REPEAT_DELAY = 1000;
66

77
export const registerReactions = (stores: StoreMapping) => {
8+
window.addEventListener('pagehide', stores.messagesStore.executePendingDeletes);
9+
window.addEventListener('beforeunload', stores.messagesStore.executePendingDeletes);
10+
811
const clearAll = () => {
912
stores.messagesStore.clearAll();
1013
stores.appStore.clear();

0 commit comments

Comments
 (0)