Message notifications working

This commit is contained in:
stopflock
2025-12-02 14:04:04 -06:00
parent bc03dcbe89
commit c4d9cd7986
15 changed files with 230 additions and 16 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -215,7 +215,12 @@
"currentDestinationSimulate": "当前处于:模拟模式(无真实账户)",
"viewMessages": "在 OSM 上查看消息",
"unreadMessagesCount": "您有 {} 条未读消息",
"noUnreadMessages": "没有未读消息"
"noUnreadMessages": "没有未读消息",
"reauthRequired": "刷新身份验证",
"reauthExplanation": "您必须刷新身份验证才能通过应用接收 OSM 消息通知。",
"reauthBenefit": "这将在您在 OpenStreetMap 上有未读消息时启用通知点。",
"reauthNow": "现在执行",
"reauthLater": "稍后"
},
"queue": {
"title": "上传队列",

View File

@@ -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,

View File

@@ -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),
],

View File

@@ -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);
});
}
},
),
),

View File

@@ -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,

View File

@@ -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
);

View File

@@ -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();

View 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')),
),
],
);
}
}