mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Message notifications working
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -18,6 +20,7 @@ import 'services/changelog_service.dart';
|
||||
import 'services/operator_profile_service.dart';
|
||||
import 'services/profile_service.dart';
|
||||
import 'widgets/proximity_warning_dialog.dart';
|
||||
import 'widgets/reauth_messages_dialog.dart';
|
||||
import 'dev_config.dart';
|
||||
import 'state/auth_state.dart';
|
||||
import 'state/messages_state.dart';
|
||||
@@ -208,6 +211,13 @@ class AppState extends ChangeNotifier {
|
||||
await _uploadQueueState.init();
|
||||
await _authState.init(_settingsState.uploadMode);
|
||||
|
||||
// Check for messages on app launch if user is already logged in
|
||||
if (isLoggedIn) {
|
||||
checkMessages();
|
||||
}
|
||||
|
||||
// Note: Re-auth check will be triggered from home screen after init
|
||||
|
||||
// Initialize OfflineAreaService to ensure offline areas are loaded
|
||||
await OfflineAreaService().ensureInitialized();
|
||||
|
||||
@@ -284,6 +294,76 @@ class AppState extends ChangeNotifier {
|
||||
void clearMessages() {
|
||||
_messagesState.clearMessages();
|
||||
}
|
||||
|
||||
/// Check if the current OAuth token has required scopes for message notifications
|
||||
/// Returns true if re-authentication is needed
|
||||
Future<bool> needsReauthForMessages() async {
|
||||
// Only check if logged in and not in simulate mode
|
||||
if (!isLoggedIn || uploadMode == UploadMode.simulate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final accessToken = await _authState.getAccessToken();
|
||||
if (accessToken == null) return false;
|
||||
|
||||
try {
|
||||
// Try to fetch user details - this should include message data if scope is correct
|
||||
final response = await http.get(
|
||||
Uri.parse('${_getApiHost()}/api/0.6/user/details.json'),
|
||||
headers: {'Authorization': 'Bearer $accessToken'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 403) {
|
||||
// Forbidden - likely missing scope
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final messages = data['user']?['messages'];
|
||||
// If messages field is missing, we might not have the right scope
|
||||
return messages == null;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
// On error, assume no re-auth needed to avoid annoying users
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Show re-authentication dialog if needed
|
||||
Future<void> checkAndPromptReauthForMessages(BuildContext context) async {
|
||||
if (await needsReauthForMessages()) {
|
||||
_showReauthDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _showReauthDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ReauthMessagesDialog(
|
||||
onReauth: () {
|
||||
// Navigate to OSM account page where user can re-authenticate
|
||||
Navigator.of(context).pushNamed('/settings/osm-account');
|
||||
},
|
||||
onDismiss: () {
|
||||
// Just dismiss - will show again on next app start or mode change
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getApiHost() {
|
||||
switch (uploadMode) {
|
||||
case UploadMode.production:
|
||||
return 'https://api.openstreetmap.org';
|
||||
case UploadMode.sandbox:
|
||||
return 'https://api06.dev.openstreetmap.org';
|
||||
case UploadMode.simulate:
|
||||
return 'https://api.openstreetmap.org';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Profile Methods ----------
|
||||
void toggleProfile(NodeProfile p, bool e) {
|
||||
@@ -503,6 +583,8 @@ class AppState extends ChangeNotifier {
|
||||
if (isLoggedIn) {
|
||||
// Don't await - let it run in background
|
||||
checkMessages();
|
||||
|
||||
// Note: Re-auth check will be triggered from the settings screen after mode change
|
||||
}
|
||||
|
||||
_startUploader(); // Restart uploader with new mode
|
||||
|
||||
@@ -183,7 +183,12 @@
|
||||
"currentDestinationSimulate": "Derzeit im: Simulationsmodus (kein echtes Konto)",
|
||||
"viewMessages": "Nachrichten auf OSM anzeigen",
|
||||
"unreadMessagesCount": "Sie haben {} ungelesene Nachrichten",
|
||||
"noUnreadMessages": "Keine ungelesenen Nachrichten"
|
||||
"noUnreadMessages": "Keine ungelesenen Nachrichten",
|
||||
"reauthRequired": "Authentifizierung aktualisieren",
|
||||
"reauthExplanation": "Sie müssen Ihre Authentifizierung aktualisieren, um OSM-Nachrichtenbenachrichtigungen über die App zu erhalten.",
|
||||
"reauthBenefit": "Dies ermöglicht Benachrichtigungspunkte, wenn Sie ungelesene Nachrichten auf OpenStreetMap haben.",
|
||||
"reauthNow": "Jetzt machen",
|
||||
"reauthLater": "Später"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Upload-Warteschlange",
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
"currentDestinationSimulate": "Currently in: Simulate mode (no real account)",
|
||||
"viewMessages": "View Messages on OSM",
|
||||
"unreadMessagesCount": "You have {} unread messages",
|
||||
"noUnreadMessages": "No unread messages"
|
||||
"noUnreadMessages": "No unread messages",
|
||||
"reauthRequired": "Refresh Authentication",
|
||||
"reauthExplanation": "You must refresh your authentication to receive OSM message notifications through the app.",
|
||||
"reauthBenefit": "This will enable notification dots when you have unread messages on OpenStreetMap.",
|
||||
"reauthNow": "Do That Now",
|
||||
"reauthLater": "Later"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Upload Queue",
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
"currentDestinationSimulate": "Actualmente en: Modo de simulación (sin cuenta real)",
|
||||
"viewMessages": "Ver Mensajes en OSM",
|
||||
"unreadMessagesCount": "Tienes {} mensajes sin leer",
|
||||
"noUnreadMessages": "No hay mensajes sin leer"
|
||||
"noUnreadMessages": "No hay mensajes sin leer",
|
||||
"reauthRequired": "Actualizar Autenticación",
|
||||
"reauthExplanation": "Debes actualizar tu autenticación para recibir notificaciones de mensajes OSM a través de la aplicación.",
|
||||
"reauthBenefit": "Esto habilitará puntos de notificación cuando tengas mensajes sin leer en OpenStreetMap.",
|
||||
"reauthNow": "Hazlo Ahora",
|
||||
"reauthLater": "Más Tarde"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Cola de Subida",
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
"currentDestinationSimulate": "Actuellement en : Mode simulation (pas de compte réel)",
|
||||
"viewMessages": "Voir les Messages sur OSM",
|
||||
"unreadMessagesCount": "Vous avez {} messages non lus",
|
||||
"noUnreadMessages": "Aucun message non lu"
|
||||
"noUnreadMessages": "Aucun message non lu",
|
||||
"reauthRequired": "Actualiser l'Authentification",
|
||||
"reauthExplanation": "Vous devez actualiser votre authentification pour recevoir des notifications de messages OSM via l'application.",
|
||||
"reauthBenefit": "Cela activera les points de notification lorsque vous avez des messages non lus sur OpenStreetMap.",
|
||||
"reauthNow": "Le Faire Maintenant",
|
||||
"reauthLater": "Plus Tard"
|
||||
},
|
||||
"queue": {
|
||||
"title": "File de Téléchargement",
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
"currentDestinationSimulate": "Attualmente in: Modalità simulazione (nessun account reale)",
|
||||
"viewMessages": "Visualizza Messaggi su OSM",
|
||||
"unreadMessagesCount": "Hai {} messaggi non letti",
|
||||
"noUnreadMessages": "Nessun messaggio non letto"
|
||||
"noUnreadMessages": "Nessun messaggio non letto",
|
||||
"reauthRequired": "Aggiorna Autenticazione",
|
||||
"reauthExplanation": "Devi aggiornare la tua autenticazione per ricevere notifiche di messaggi OSM tramite l'app.",
|
||||
"reauthBenefit": "Questo abiliterà i punti di notifica quando hai messaggi non letti su OpenStreetMap.",
|
||||
"reauthNow": "Fallo Ora",
|
||||
"reauthLater": "Più Tardi"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Coda di Upload",
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
"currentDestinationSimulate": "Atualmente em: Modo de simulação (sem conta real)",
|
||||
"viewMessages": "Ver Mensagens no OSM",
|
||||
"unreadMessagesCount": "Você tem {} mensagens não lidas",
|
||||
"noUnreadMessages": "Nenhuma mensagem não lida"
|
||||
"noUnreadMessages": "Nenhuma mensagem não lida",
|
||||
"reauthRequired": "Atualizar Autenticação",
|
||||
"reauthExplanation": "Você deve atualizar sua autenticação para receber notificações de mensagens OSM através do aplicativo.",
|
||||
"reauthBenefit": "Isso habilitará pontos de notificação quando você tiver mensagens não lidas no OpenStreetMap.",
|
||||
"reauthNow": "Fazer Agora",
|
||||
"reauthLater": "Mais Tarde"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Fila de Upload",
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
"currentDestinationSimulate": "当前处于:模拟模式(无真实账户)",
|
||||
"viewMessages": "在 OSM 上查看消息",
|
||||
"unreadMessagesCount": "您有 {} 条未读消息",
|
||||
"noUnreadMessages": "没有未读消息"
|
||||
"noUnreadMessages": "没有未读消息",
|
||||
"reauthRequired": "刷新身份验证",
|
||||
"reauthExplanation": "您必须刷新身份验证才能通过应用接收 OSM 消息通知。",
|
||||
"reauthBenefit": "这将在您在 OpenStreetMap 上有未读消息时启用通知点。",
|
||||
"reauthNow": "现在执行",
|
||||
"reauthLater": "稍后"
|
||||
},
|
||||
"queue": {
|
||||
"title": "上传队列",
|
||||
|
||||
@@ -673,7 +673,11 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
// Check for welcome/changelog popup after app is fully initialized
|
||||
if (appState.isInitialized && !_hasCheckedForPopup) {
|
||||
_hasCheckedForPopup = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _checkForPopup());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkForPopup();
|
||||
// Check if re-authentication is needed for message notifications
|
||||
appState.checkAndPromptReauthForMessages(context);
|
||||
});
|
||||
}
|
||||
|
||||
// Pass the active sheet height directly to the map
|
||||
@@ -720,7 +724,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
icon: Stack(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
if (appState.hasUnreadMessages && appState.uploadMode != UploadMode.simulate)
|
||||
if (appState.hasUnreadMessages)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
|
||||
@@ -239,8 +239,8 @@ class _OSMAccountScreenState extends State<OSMAccountScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Account deletion section - only show when logged in
|
||||
if (appState.isLoggedIn) ...[
|
||||
// Account deletion section - only show when logged in and not in simulate mode
|
||||
if (appState.isLoggedIn && appState.uploadMode != UploadMode.simulate) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildAccountDeletionSection(context, appState),
|
||||
],
|
||||
|
||||
@@ -38,7 +38,13 @@ class UploadModeSection extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
onChanged: (mode) {
|
||||
if (mode != null) appState.setUploadMode(mode);
|
||||
if (mode != null) {
|
||||
appState.setUploadMode(mode);
|
||||
// Check if re-authentication is needed after mode change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appState.checkAndPromptReauthForMessages(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -120,7 +120,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
leading: Stack(
|
||||
children: [
|
||||
const Icon(Icons.account_circle),
|
||||
if (appState.hasUnreadMessages && appState.uploadMode != UploadMode.simulate)
|
||||
if (appState.hasUnreadMessages)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
|
||||
@@ -51,7 +51,7 @@ class AuthService {
|
||||
_helper = OAuth2Helper(
|
||||
client,
|
||||
clientId: clientId,
|
||||
scopes: ['read_prefs', 'write_api'],
|
||||
scopes: ['read_prefs', 'write_api', 'consume_messages'],
|
||||
enablePKCE: true,
|
||||
// tokenStorageKey: _tokenKey, // not supported by this package version
|
||||
);
|
||||
|
||||
@@ -55,8 +55,11 @@ class OSMMessagesService {
|
||||
final messages = user['messages'];
|
||||
if (messages == null) return null;
|
||||
|
||||
// Get unread count
|
||||
final unreadCount = messages['unread']?['count'] ?? 0;
|
||||
// Get unread count from correct path: messages.received.unread
|
||||
final received = messages['received'];
|
||||
if (received == null) return null;
|
||||
|
||||
final unreadCount = received['unread'] ?? 0;
|
||||
|
||||
// Update cache
|
||||
_lastCheck = DateTime.now();
|
||||
|
||||
84
lib/widgets/reauth_messages_dialog.dart
Normal file
84
lib/widgets/reauth_messages_dialog.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
/// Dialog to prompt user to re-authenticate for message notifications
|
||||
class ReauthMessagesDialog extends StatelessWidget {
|
||||
final VoidCallback onReauth;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const ReauthMessagesDialog({
|
||||
super.key,
|
||||
required this.onReauth,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.message_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(locService.t('auth.reauthRequired')),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(locService.t('auth.reauthExplanation')),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
locService.t('auth.reauthBenefit'),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onDismiss();
|
||||
},
|
||||
child: Text(locService.t('auth.reauthLater')),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onReauth();
|
||||
},
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(locService.t('auth.reauthNow')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user