💄 Flashing message background when jumped
This commit is contained in:
@@ -3,6 +3,7 @@ import "dart:convert";
|
|||||||
import "dart:developer" as developer;
|
import "dart:developer" as developer;
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
import "package:dio/dio.dart";
|
import "package:dio/dio.dart";
|
||||||
|
import "package:drift/drift.dart" show Variable;
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:file_picker/file_picker.dart";
|
import "package:file_picker/file_picker.dart";
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
@@ -46,6 +47,8 @@ part 'room.g.dart';
|
|||||||
|
|
||||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||||
|
|
||||||
|
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||||
|
|
||||||
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||||
final controller = StreamController<AppLifecycleState>();
|
final controller = StreamController<AppLifecycleState>();
|
||||||
|
|
||||||
@@ -292,6 +295,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
|
bool _isJumping = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||||
@@ -372,26 +376,35 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
final dbLocalMessages = filteredMessages;
|
final dbLocalMessages = filteredMessages;
|
||||||
|
|
||||||
|
// Always ensure unique messages to prevent duplicate keys
|
||||||
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final message in dbLocalMessages) {
|
||||||
|
if (seenIds.add(message.id)) {
|
||||||
|
uniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
final pendingForRoom =
|
final pendingForRoom =
|
||||||
_pendingMessages.values
|
_pendingMessages.values
|
||||||
.where((msg) => msg.roomId == _roomId)
|
.where((msg) => msg.roomId == _roomId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final allMessages = [...pendingForRoom, ...dbLocalMessages];
|
final allMessages = [...pendingForRoom, ...uniqueMessages];
|
||||||
_sortMessages(allMessages); // Use the helper function
|
_sortMessages(allMessages); // Use the helper function
|
||||||
|
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final finalUniqueMessages = <LocalChatMessage>[];
|
||||||
final seenIds = <String>{};
|
final finalSeenIds = <String>{};
|
||||||
for (final message in allMessages) {
|
for (final message in allMessages) {
|
||||||
if (seenIds.add(message.id)) {
|
if (finalSeenIds.add(message.id)) {
|
||||||
uniqueMessages.add(message);
|
finalUniqueMessages.add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return uniqueMessages;
|
return finalUniqueMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbLocalMessages;
|
return uniqueMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
||||||
@@ -982,6 +995,111 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> jumpToMessage(String messageId) async {
|
||||||
|
developer.log(
|
||||||
|
'Starting jump to message $messageId',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
if (_isJumping) {
|
||||||
|
developer.log(
|
||||||
|
'Jump already in progress, skipping',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
_isJumping = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
developer.log('Fetching message $messageId', name: 'MessagesNotifier');
|
||||||
|
final message = await fetchMessageById(messageId);
|
||||||
|
if (message == null) {
|
||||||
|
developer.log('Message $messageId not found', name: 'MessagesNotifier');
|
||||||
|
showSnackBar('messageNotFound'.tr());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if message is already in current state to avoid duplicate loading
|
||||||
|
final currentMessages = state.value ?? [];
|
||||||
|
final existingIndex = currentMessages.indexWhere(
|
||||||
|
(m) => m.id == messageId,
|
||||||
|
);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
developer.log(
|
||||||
|
'Message $messageId already in current state at index $existingIndex, jumping directly',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
return existingIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
developer.log(
|
||||||
|
'Message $messageId not in current state, loading messages around it',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Count messages newer than this one
|
||||||
|
final query = _database.customSelect(
|
||||||
|
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
|
||||||
|
variables: [
|
||||||
|
Variable.withString(_roomId),
|
||||||
|
Variable.withDateTime(message.createdAt),
|
||||||
|
],
|
||||||
|
readsFrom: {_database.chatMessages},
|
||||||
|
);
|
||||||
|
final result = await query.getSingle();
|
||||||
|
final newerCount = result.read<int>('count');
|
||||||
|
|
||||||
|
// Load messages around this position
|
||||||
|
final offset =
|
||||||
|
(newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt();
|
||||||
|
developer.log(
|
||||||
|
'Loading messages with offset $offset, take $_pageSize',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
final loadedMessages = await _getCachedMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: _pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if loaded messages are already in current state
|
||||||
|
final currentIds = currentMessages.map((m) => m.id).toSet();
|
||||||
|
final newMessages =
|
||||||
|
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
|
||||||
|
developer.log(
|
||||||
|
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newMessages.isNotEmpty) {
|
||||||
|
// Merge with current messages
|
||||||
|
final allMessages = [...currentMessages, ...newMessages];
|
||||||
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final message in allMessages) {
|
||||||
|
if (seenIds.add(message.id)) {
|
||||||
|
uniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_sortMessages(uniqueMessages);
|
||||||
|
state = AsyncValue.data(uniqueMessages);
|
||||||
|
developer.log(
|
||||||
|
'Updated state with ${uniqueMessages.length} total messages',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final finalIndex = (state.value ?? []).indexWhere(
|
||||||
|
(m) => m.id == messageId,
|
||||||
|
);
|
||||||
|
developer.log(
|
||||||
|
'Final index for message $messageId is $finalIndex',
|
||||||
|
name: 'MessagesNotifier',
|
||||||
|
);
|
||||||
|
return finalIndex;
|
||||||
|
} finally {
|
||||||
|
_isJumping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _hasLink(LocalChatMessage message) {
|
bool _hasLink(LocalChatMessage message) {
|
||||||
final content = message.toRemoteMessage().content;
|
final content = message.toRemoteMessage().content;
|
||||||
if (content == null) return false;
|
if (content == null) return false;
|
||||||
@@ -1454,17 +1572,40 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
(m) => m.id == messageId,
|
(m) => m.id == messageId,
|
||||||
);
|
);
|
||||||
if (messageIndex == -1) {
|
if (messageIndex == -1) {
|
||||||
showSnackBar('messageJumpNotLoaded'.tr());
|
messagesNotifier.jumpToMessage(messageId).then((index) {
|
||||||
|
if (index != -1) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
listController.animateToItem(
|
||||||
|
index: index,
|
||||||
|
scrollController: scrollController,
|
||||||
|
alignment: 0.5,
|
||||||
|
duration:
|
||||||
|
(estimatedDistance) =>
|
||||||
|
Duration(milliseconds: 250),
|
||||||
|
curve: (estimatedDistance) => Curves.easeInOut,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ref
|
||||||
|
.read(flashingMessagesProvider.notifier)
|
||||||
|
.update((set) => set.union({messageId}));
|
||||||
|
}
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
listController.animateToItem(
|
listController.animateToItem(
|
||||||
index: messageIndex,
|
index: messageIndex,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration:
|
duration:
|
||||||
(estimatedDistance) => Duration(milliseconds: 250),
|
(estimatedDistance) =>
|
||||||
|
Duration(milliseconds: 250),
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
curve: (estimatedDistance) => Curves.easeInOut,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
ref
|
||||||
|
.read(flashingMessagesProvider.notifier)
|
||||||
|
.update((set) => set.union({messageId}));
|
||||||
},
|
},
|
||||||
progress: attachmentProgress.value[message.id],
|
progress: attachmentProgress.value[message.id],
|
||||||
showAvatar: isLastInGroup,
|
showAvatar: isLastInGroup,
|
||||||
|
@@ -6,7 +6,7 @@ part of 'room.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'196fe42438c716b2f975f5f14733974174b3dde7';
|
String _$messagesNotifierHash() => r'5787fcac9f6c77062aaf854daf2365464f771c2f';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@@ -67,6 +68,46 @@ class MessageItem extends HookConsumerWidget {
|
|||||||
final hasBackground =
|
final hasBackground =
|
||||||
ref.watch(backgroundImageFileProvider).valueOrNull != null;
|
ref.watch(backgroundImageFileProvider).valueOrNull != null;
|
||||||
|
|
||||||
|
final flashing = ref.watch(
|
||||||
|
flashingMessagesProvider.select((set) => set.contains(message.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isFlashing = useState(false);
|
||||||
|
final flashTimer = useState<Timer?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (flashing) {
|
||||||
|
if (flashTimer.value != null) return null;
|
||||||
|
isFlashing.value = true;
|
||||||
|
flashTimer.value = Timer.periodic(const Duration(milliseconds: 200), (
|
||||||
|
timer,
|
||||||
|
) {
|
||||||
|
isFlashing.value = !isFlashing.value;
|
||||||
|
if (timer.tick >= 4) {
|
||||||
|
// 4 ticks: true, false, true, false
|
||||||
|
timer.cancel();
|
||||||
|
flashTimer.value = null;
|
||||||
|
isFlashing.value = false;
|
||||||
|
ref
|
||||||
|
.read(flashingMessagesProvider.notifier)
|
||||||
|
.update((set) => set.difference({message.id}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
flashTimer.value?.cancel();
|
||||||
|
flashTimer.value = null;
|
||||||
|
isFlashing.value = false;
|
||||||
|
}
|
||||||
|
return () {
|
||||||
|
flashTimer.value?.cancel();
|
||||||
|
};
|
||||||
|
}, [flashing]);
|
||||||
|
|
||||||
|
final flashColor =
|
||||||
|
isFlashing.value
|
||||||
|
? Theme.of(context).colorScheme.primary.withOpacity(0.8)
|
||||||
|
: containerColor;
|
||||||
|
|
||||||
final remoteMessage = message.toRemoteMessage();
|
final remoteMessage = message.toRemoteMessage();
|
||||||
final sender = remoteMessage.sender;
|
final sender = remoteMessage.sender;
|
||||||
|
|
||||||
@@ -253,9 +294,10 @@ class MessageItem extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: containerColor,
|
color: flashColor,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
Reference in New Issue
Block a user