feat(chat): удаление сообщений (у себя/у всех)
This commit is contained in:
@@ -467,6 +467,28 @@ class MessagesModule {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteMessages(
|
||||||
|
int chatId,
|
||||||
|
List<String> messageIds, {
|
||||||
|
bool forEveryone = false,
|
||||||
|
}) async {
|
||||||
|
final ids = messageIds
|
||||||
|
.map((id) => int.tryParse(id))
|
||||||
|
.whereType<int>()
|
||||||
|
.toList();
|
||||||
|
if (ids.isEmpty) return false;
|
||||||
|
|
||||||
|
final payload = {
|
||||||
|
'messageIds': ids,
|
||||||
|
'chatId': chatId,
|
||||||
|
'forMe': !forEveryone,
|
||||||
|
'itemType': 'REGULAR',
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await _api.sendRequest(Opcode.msgDelete, payload);
|
||||||
|
return response.isOk;
|
||||||
|
}
|
||||||
|
|
||||||
Future<TranscriptionResult> requestTranscription(
|
Future<TranscriptionResult> requestTranscription(
|
||||||
int chatId,
|
int chatId,
|
||||||
int messageId,
|
int messageId,
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
bool _historyKickedOff = false;
|
bool _historyKickedOff = false;
|
||||||
List<CachedMessage> _messages = [];
|
List<CachedMessage> _messages = [];
|
||||||
final ValueNotifier<int> _messagesRev = ValueNotifier(0);
|
final ValueNotifier<int> _messagesRev = ValueNotifier(0);
|
||||||
|
final Set<String> _deletingIds = {};
|
||||||
List<Object>? _combinedItemsCache;
|
List<Object>? _combinedItemsCache;
|
||||||
int? _combinedItemsKey;
|
int? _combinedItemsKey;
|
||||||
bool _floatingDateScheduled = false;
|
bool _floatingDateScheduled = false;
|
||||||
@@ -622,6 +623,123 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
_messagesRev.value++;
|
_messagesRev.value++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmDeleteMessage(CachedMessage message, bool isMe) async {
|
||||||
|
final isLocalOnly = message.id.startsWith('temp_');
|
||||||
|
final canForEveryone = isMe && !isLocalOnly;
|
||||||
|
|
||||||
|
if (isLocalOnly) {
|
||||||
|
_startDeleteAnimation(message.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final forEveryone = await _showDeleteMessageDialog(canForEveryone);
|
||||||
|
if (forEveryone == null || !mounted) return;
|
||||||
|
|
||||||
|
final ok = await messagesModule.deleteMessages(
|
||||||
|
widget.chatId,
|
||||||
|
[message.id],
|
||||||
|
forEveryone: forEveryone,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!ok) {
|
||||||
|
Haptics.error();
|
||||||
|
showCustomNotification(context, 'Не удалось удалить сообщение');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_startDeleteAnimation(message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startDeleteAnimation(String messageId) {
|
||||||
|
if (!_deletingIds.add(messageId)) return;
|
||||||
|
Haptics.tap();
|
||||||
|
_bumpMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _finalizeDelete(String messageId) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
_deletingIds.remove(messageId);
|
||||||
|
final idx = _messages.indexWhere((m) => m.id == messageId);
|
||||||
|
if (idx != -1) {
|
||||||
|
_messages.removeAt(idx);
|
||||||
|
_reactionNotifiers.remove(messageId)?.dispose();
|
||||||
|
}
|
||||||
|
_bumpMessages();
|
||||||
|
try {
|
||||||
|
await AppDatabase.deleteMessage(_myId, widget.chatId, messageId);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _showDeleteMessageDialog(bool canForEveryone) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
var alsoForEveryone = canForEveryone;
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (ctx, setLocalState) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: cs.surfaceContainerHigh,
|
||||||
|
title: const Text('Удалить сообщение'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Вы точно хотите удалить это сообщение?',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 15),
|
||||||
|
),
|
||||||
|
if (canForEveryone) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => setLocalState(
|
||||||
|
() => alsoForEveryone = !alsoForEveryone,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: alsoForEveryone,
|
||||||
|
onChanged: (v) => setLocalState(
|
||||||
|
() => alsoForEveryone = v ?? false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Также удалить для ${widget.name}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Отмена'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(
|
||||||
|
ctx,
|
||||||
|
canForEveryone && alsoForEveryone,
|
||||||
|
),
|
||||||
|
child: Text('Удалить', style: TextStyle(color: cs.error)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onMessageEvent(MessageEvent event) {
|
void _onMessageEvent(MessageEvent event) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
switch (event) {
|
switch (event) {
|
||||||
@@ -1539,21 +1657,31 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
final pressable = _LongPressBubble(
|
final pressable = _LongPressBubble(
|
||||||
message: message,
|
message: message,
|
||||||
isMe: isMe,
|
isMe: isMe,
|
||||||
|
onDelete: () => _confirmDeleteMessage(message, isMe),
|
||||||
child: bubble,
|
child: bubble,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Widget child = message.id == _lastSentId
|
final Widget child;
|
||||||
? _SentMessageAnimation(
|
if (_deletingIds.contains(message.id)) {
|
||||||
key: ValueKey('anim_${message.id}'),
|
child = _DeletingMessageAnimation(
|
||||||
onComplete: () {
|
key: ValueKey('del_${message.id}'),
|
||||||
if (mounted) {
|
onComplete: () => _finalizeDelete(message.id),
|
||||||
_lastSentId = null;
|
child: IgnorePointer(child: pressable),
|
||||||
_bumpMessages();
|
);
|
||||||
}
|
} else if (message.id == _lastSentId) {
|
||||||
},
|
child = _SentMessageAnimation(
|
||||||
child: pressable,
|
key: ValueKey('anim_${message.id}'),
|
||||||
)
|
onComplete: () {
|
||||||
: pressable;
|
if (mounted) {
|
||||||
|
_lastSentId = null;
|
||||||
|
_bumpMessages();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: pressable,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
child = pressable;
|
||||||
|
}
|
||||||
|
|
||||||
final builtItem = RepaintBoundary(
|
final builtItem = RepaintBoundary(
|
||||||
key: ValueKey('msg_${message.id}'),
|
key: ValueKey('msg_${message.id}'),
|
||||||
@@ -2699,11 +2827,13 @@ class _LongPressBubble extends StatefulWidget {
|
|||||||
final Widget child;
|
final Widget child;
|
||||||
final CachedMessage message;
|
final CachedMessage message;
|
||||||
final bool isMe;
|
final bool isMe;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
const _LongPressBubble({
|
const _LongPressBubble({
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.isMe,
|
required this.isMe,
|
||||||
|
required this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2754,6 +2884,7 @@ class _LongPressBubbleState extends State<_LongPressBubble> {
|
|||||||
messageText: widget.message.text,
|
messageText: widget.message.text,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
style: AppMessageActionsStyle.current.value,
|
style: AppMessageActionsStyle.current.value,
|
||||||
|
onDelete: widget.onDelete,
|
||||||
onDispose: () {
|
onDispose: () {
|
||||||
if (identical(_controller, controller)) {
|
if (identical(_controller, controller)) {
|
||||||
_controller = null;
|
_controller = null;
|
||||||
@@ -2785,6 +2916,7 @@ class _LongPressBubbleState extends State<_LongPressBubble> {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
style: MessageActionsStyle.list,
|
style: MessageActionsStyle.list,
|
||||||
interaction: MessageActionsInteraction.click,
|
interaction: MessageActionsInteraction.click,
|
||||||
|
onDelete: widget.onDelete,
|
||||||
onDispose: () {
|
onDispose: () {
|
||||||
if (identical(_controller, controller)) {
|
if (identical(_controller, controller)) {
|
||||||
_controller = null;
|
_controller = null;
|
||||||
@@ -2814,6 +2946,75 @@ class _LongPressBubbleState extends State<_LongPressBubble> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DeletingMessageAnimation extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback onComplete;
|
||||||
|
|
||||||
|
const _DeletingMessageAnimation({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.onComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DeletingMessageAnimation> createState() =>
|
||||||
|
_DeletingMessageAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeletingMessageAnimationState extends State<_DeletingMessageAnimation>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _ctrl;
|
||||||
|
late final Animation<double> _opacity;
|
||||||
|
late final Animation<double> _scale;
|
||||||
|
late final Animation<double> _collapse;
|
||||||
|
bool _fired = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 280),
|
||||||
|
);
|
||||||
|
_opacity = Tween<double>(begin: 1, end: 0).animate(
|
||||||
|
CurvedAnimation(parent: _ctrl, curve: const Interval(0.0, 0.6)),
|
||||||
|
);
|
||||||
|
_scale = Tween<double>(begin: 1, end: 0.82).animate(
|
||||||
|
CurvedAnimation(parent: _ctrl, curve: const Interval(0.0, 0.6)),
|
||||||
|
);
|
||||||
|
_collapse = Tween<double>(begin: 1, end: 0).animate(
|
||||||
|
CurvedAnimation(parent: _ctrl, curve: const Interval(0.35, 1.0, curve: Curves.easeInOut)),
|
||||||
|
);
|
||||||
|
_ctrl.forward().whenComplete(() {
|
||||||
|
if (_fired) return;
|
||||||
|
_fired = true;
|
||||||
|
widget.onComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizeTransition(
|
||||||
|
sizeFactor: _collapse,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _opacity,
|
||||||
|
child: ScaleTransition(
|
||||||
|
scale: _scale,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SentMessageAnimation extends StatefulWidget {
|
class _SentMessageAnimation extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final VoidCallback onComplete;
|
final VoidCallback onComplete;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ void showMessageActions({
|
|||||||
required MessageActionsController controller,
|
required MessageActionsController controller,
|
||||||
required MessageActionsStyle style,
|
required MessageActionsStyle style,
|
||||||
required VoidCallback onDispose,
|
required VoidCallback onDispose,
|
||||||
|
VoidCallback? onDelete,
|
||||||
MessageActionsInteraction interaction = MessageActionsInteraction.dragAndRelease,
|
MessageActionsInteraction interaction = MessageActionsInteraction.dragAndRelease,
|
||||||
}) {
|
}) {
|
||||||
final overlay = Overlay.of(context, rootOverlay: true);
|
final overlay = Overlay.of(context, rootOverlay: true);
|
||||||
@@ -87,6 +88,7 @@ void showMessageActions({
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
style: style,
|
style: style,
|
||||||
interaction: interaction,
|
interaction: interaction,
|
||||||
|
onDelete: onDelete,
|
||||||
onDismiss: () {
|
onDismiss: () {
|
||||||
if (entry.mounted) entry.remove();
|
if (entry.mounted) entry.remove();
|
||||||
onDispose();
|
onDispose();
|
||||||
@@ -106,6 +108,7 @@ class _MessageActionsLayer extends StatefulWidget {
|
|||||||
final MessageActionsStyle style;
|
final MessageActionsStyle style;
|
||||||
final MessageActionsInteraction interaction;
|
final MessageActionsInteraction interaction;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
const _MessageActionsLayer({
|
const _MessageActionsLayer({
|
||||||
required this.snapshot,
|
required this.snapshot,
|
||||||
@@ -117,6 +120,7 @@ class _MessageActionsLayer extends StatefulWidget {
|
|||||||
required this.style,
|
required this.style,
|
||||||
required this.interaction,
|
required this.interaction,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
|
this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -286,7 +290,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
|
|||||||
_Action(
|
_Action(
|
||||||
Symbols.delete,
|
Symbols.delete,
|
||||||
'Удалить',
|
'Удалить',
|
||||||
() => _stub('Удаление'),
|
_delete,
|
||||||
destructive: true,
|
destructive: true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -346,6 +350,12 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
|
|||||||
await _close();
|
await _close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _delete() async {
|
||||||
|
final onDelete = widget.onDelete;
|
||||||
|
await _close();
|
||||||
|
onDelete?.call();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _stub(String name) async {
|
Future<void> _stub(String name) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showCustomNotification(context, '$name — пока в разработке');
|
showCustomNotification(context, '$name — пока в разработке');
|
||||||
|
|||||||
Reference in New Issue
Block a user