feat: integrate invite API and improve notification handling

- Add invite code display and binding functionality via API
- Fix notification unread count sync on auth state change
- Improve notification mark read with server state validation
- Add auth state listener to trigger notification refresh
- Add YaoCoinConverter for coin-to-yao type conversion
- Remove YaoLegend from divination screens (UI cleanup)
- Abbreviate relation labels in yao detail view
- Add re-register notice to account delete screen
- Update 'coins' terminology to 'points' in localization
- Fix backend points consumption to only run in CHAT mode
- Add HttpxAuthNoiseFilter to suppress auth endpoint logging
- Fix notification static_schema import path
- Add test coverage for notification bloc error handling
- Update AGENTS.md page header rules and image handling
- Delete deprecated run-dev.sh script
This commit is contained in:
qzl
2026-04-13 14:52:22 +08:00
parent da947f9f08
commit 1e22f27de2
52 changed files with 1419 additions and 307 deletions
@@ -185,58 +185,64 @@ class NotificationBloc extends ChangeNotifier {
}
Future<void> _markRead(String notificationId) async {
final previousItems = _state.items;
final previousCount = _state.unreadCount;
final idx = _state.items.indexWhere((item) => item.id == notificationId);
if (idx == -1) return;
if (_state.items[idx].isRead) return;
final wasUnread = !_state.items[idx].isRead;
_state = _state.copyWith(
items: [
..._state.items.sublist(0, idx),
_state.items[idx].copyWith(isRead: true),
..._state.items.sublist(idx + 1),
],
unreadCount: wasUnread
? (_state.unreadCount > 0 ? _state.unreadCount - 1 : 0)
: _state.unreadCount,
_logger.info(
message: 'Mark notification read started',
extra: {'notification_id': notificationId},
);
notifyListeners();
try {
await _repository.markRead(notificationId: notificationId);
final updated = await _repository.markRead(
notificationId: notificationId,
);
final targetIndex = _state.items.indexWhere(
(item) => item.id == updated.id,
);
if (targetIndex == -1) {
return;
}
_state = _state.copyWith(
items: [
..._state.items.sublist(0, targetIndex),
updated,
..._state.items.sublist(targetIndex + 1),
],
unreadCount: _state.unreadCount > 0 ? _state.unreadCount - 1 : 0,
);
notifyListeners();
_logger.info(
message: 'Mark notification read succeeded',
extra: {'notification_id': notificationId},
);
} catch (error, stackTrace) {
_logger.error(
message: 'Mark read failed: ${error.runtimeType}',
error: error,
stackTrace: stackTrace,
);
_state = _state.copyWith(
items: previousItems,
unreadCount: previousCount,
);
notifyListeners();
}
}
Future<void> _markAllRead() async {
final previousItems = _state.items;
_state = _state.copyWith(
items: _state.items.map((item) => item.copyWith(isRead: true)).toList(),
unreadCount: 0,
);
notifyListeners();
_logger.info(message: 'Mark all notifications read started');
try {
await _repository.markAllRead();
_state = _state.copyWith(
items: _state.items.map((item) => item.copyWith(isRead: true)).toList(),
unreadCount: 0,
);
notifyListeners();
_logger.info(message: 'Mark all notifications read succeeded');
} catch (error, stackTrace) {
_logger.error(
message: 'Mark all read failed: ${error.runtimeType}',
error: error,
stackTrace: stackTrace,
);
_state = _state.copyWith(items: previousItems);
notifyListeners();
}
}