Skip to content

Commit 94130e1

Browse files
authored
feat: add undo for memory deleted by swiping (#2808)
Closes #2687 Demo: https://github.com/user-attachments/assets/c4f3371a-3f74-4f31-9b60-4ed05067760e
2 parents a54e322 + 91e51de commit 94130e1

File tree

3 files changed

+148
-2
lines changed

3 files changed

+148
-2
lines changed

app/lib/pages/memories/page.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class MemoriesPageState extends State<MemoriesPage> with AutomaticKeepAliveClien
5959
MemoryCategory? _selectedCategory;
6060
final ScrollController _scrollController = ScrollController();
6161

62+
OverlayEntry? _deleteNotificationOverlay;
6263
// Filter options for the dropdown
6364
// Default will be set in initState based on current date
6465
late FilterOption _currentFilter;
@@ -67,9 +68,91 @@ class MemoriesPageState extends State<MemoriesPage> with AutomaticKeepAliveClien
6768
void dispose() {
6869
_searchController.dispose();
6970
_scrollController.dispose();
71+
_removeDeleteNotification();
7072
super.dispose();
7173
}
7274

75+
// Remove the delete notification overlay if it exists
76+
void _removeDeleteNotification() {
77+
_deleteNotificationOverlay?.remove();
78+
_deleteNotificationOverlay = null;
79+
}
80+
81+
void showDeleteNotification(String memoryContent, Memory? memory) {
82+
_removeDeleteNotification();
83+
84+
final provider = Provider.of<MemoriesProvider>(this.context, listen: false);
85+
86+
_deleteNotificationOverlay = OverlayEntry(
87+
builder: (_) => Positioned(
88+
bottom: 20,
89+
left: 0,
90+
right: 0,
91+
child: Center(
92+
child: Material(
93+
color: Colors.transparent,
94+
child: Container(
95+
width: MediaQuery.of(this.context).size.width * 0.9,
96+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
97+
decoration: BoxDecoration(
98+
color: Colors.black87,
99+
borderRadius: BorderRadius.circular(8),
100+
boxShadow: [
101+
BoxShadow(
102+
color: Colors.black.withOpacity(0.2),
103+
blurRadius: 4,
104+
offset: const Offset(0, 2),
105+
),
106+
],
107+
),
108+
child: Row(
109+
children: [
110+
Expanded(
111+
child: Text(
112+
'Memory Deleted.',
113+
style: const TextStyle(color: Colors.white, fontSize: 14),
114+
),
115+
),
116+
TextButton(
117+
onPressed: () async {
118+
final success = await provider.restoreLastDeletedMemory();
119+
if (success) {
120+
_removeDeleteNotification();
121+
}
122+
},
123+
style: TextButton.styleFrom(
124+
padding: EdgeInsets.symmetric(horizontal: 8),
125+
minimumSize: Size(0, 36),
126+
),
127+
child: Text(
128+
'Undo',
129+
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w500),
130+
),
131+
),
132+
IconButton(
133+
onPressed: () {
134+
_removeDeleteNotification();
135+
},
136+
icon: Icon(Icons.close, color: Colors.white70, size: 20),
137+
padding: EdgeInsets.zero,
138+
constraints: BoxConstraints(),
139+
splashRadius: 20,
140+
),
141+
],
142+
),
143+
),
144+
),
145+
),
146+
),
147+
);
148+
149+
Overlay.of(this.context).insert(_deleteNotificationOverlay!);
150+
151+
Future.delayed(const Duration(seconds: 10), () {
152+
_removeDeleteNotification();
153+
});
154+
}
155+
73156
@override
74157
void initState() {
75158
super.initState();

app/lib/pages/memories/widgets/memory_item.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:omi/providers/memories_provider.dart';
55
import 'package:omi/utils/analytics/mixpanel.dart';
66
import 'package:omi/utils/ui_guidelines.dart';
77
import 'package:omi/widgets/extensions/string.dart';
8+
import 'package:omi/pages/memories/page.dart';
89

910
import 'delete_confirmation.dart';
1011

@@ -61,8 +62,14 @@ class MemoryItem extends StatelessWidget {
6162
return shouldDelete;
6263
},
6364
onDismissed: (direction) {
65+
final memoryContent = memory.content.decodeString;
66+
6467
provider.deleteMemory(memory);
6568
MixpanelManager().memoriesPageDeletedMemory(memory);
69+
70+
if (context.findAncestorStateOfType<MemoriesPageState>() != null) {
71+
context.findAncestorStateOfType<MemoriesPageState>()!.showDeleteNotification(memoryContent, memory);
72+
}
6673
},
6774
background: Container(
6875
margin: const EdgeInsets.only(bottom: AppStyles.spacingM),

app/lib/providers/memories_provider.dart

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'package:omi/widgets/extensions/string.dart';
23
import 'package:omi/backend/http/api/memories.dart';
34
import 'package:omi/backend/preferences.dart';
@@ -96,11 +97,66 @@ class MemoriesProvider extends ChangeNotifier {
9697
_setCategories();
9798
}
9899

99-
void deleteMemory(Memory memory) async {
100-
await deleteMemoryServer(memory.id);
100+
Memory? _lastDeletedMemory;
101+
Timer? _deletionTimer;
102+
String? _pendingDeletionId;
103+
104+
Memory? get lastDeletedMemory => _lastDeletedMemory;
105+
106+
void deleteMemory(Memory memory) {
107+
_cancelDeletionTimer();
108+
109+
_lastDeletedMemory = memory;
110+
_pendingDeletionId = memory.id;
111+
101112
_memories.remove(memory);
102113
_unreviewed.remove(memory);
103114
_setCategories();
115+
notifyListeners();
116+
117+
_startDeletionTimer();
118+
}
119+
120+
void _cancelDeletionTimer() {
121+
if (_deletionTimer != null && _deletionTimer!.isActive) {
122+
_deletionTimer!.cancel();
123+
_deletionTimer = null;
124+
}
125+
}
126+
127+
void _startDeletionTimer() {
128+
_deletionTimer = Timer(const Duration(seconds: 10), () {
129+
_executeServerDeletion();
130+
});
131+
}
132+
133+
Future<void> _executeServerDeletion() async {
134+
if (_pendingDeletionId != null) {
135+
await deleteMemoryServer(_pendingDeletionId!);
136+
_pendingDeletionId = null;
137+
}
138+
}
139+
140+
// Restore the last deleted memory
141+
Future<bool> restoreLastDeletedMemory() async {
142+
if (_lastDeletedMemory == null) return false;
143+
144+
_cancelDeletionTimer();
145+
_pendingDeletionId = null;
146+
147+
_memories.add(_lastDeletedMemory!);
148+
if (!_lastDeletedMemory!.reviewed &&
149+
_lastDeletedMemory!.createdAt.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
150+
_unreviewed.add(_lastDeletedMemory!);
151+
}
152+
153+
_setCategories();
154+
notifyListeners();
155+
156+
final restoredMemory = _lastDeletedMemory;
157+
_lastDeletedMemory = null;
158+
159+
return true;
104160
}
105161

106162
void deleteAllMemories() async {

0 commit comments

Comments
 (0)