mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-10-12 10:47:12 +02:00
Message notifications working
This commit is contained in:
1 parent
bc03dcbe89
commit
c4d9cd7986
15 files changed
+230
-16
No files matched your search
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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/operator_profile_service.dart';
|
||||||
import 'services/profile_service.dart';
|
import 'services/profile_service.dart';
|
||||||
import 'widgets/proximity_warning_dialog.dart';
|
import 'widgets/proximity_warning_dialog.dart';
|
||||||
|
import 'widgets/reauth_messages_dialog.dart';
|
||||||
import 'dev_config.dart';
|
import 'dev_config.dart';
|
||||||
import 'state/auth_state.dart';
|
import 'state/auth_state.dart';
|
||||||
import 'state/messages_state.dart';
|
import 'state/messages_state.dart';
|
||||||
@@ -208,6 +211,13 @@ class AppState extends ChangeNotifier {
|
|||||||
await _uploadQueueState.init();
|
await _uploadQueueState.init();
|
||||||
await _authState.init(_settingsState.uploadMode);
|
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
|
// Initialize OfflineAreaService to ensure offline areas are loaded
|
||||||
await OfflineAreaService().ensureInitialized();
|
await OfflineAreaService().ensureInitialized();
|
||||||
|
|
||||||
@@ -284,6 +294,76 @@ class AppState extends ChangeNotifier {
|
|||||||
void clearMessages() {
|
void clearMessages() {
|
||||||
_messagesState.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 ----------
|
// ---------- Profile Methods ----------
|
||||||
void toggleProfile(NodeProfile p, bool e) {
|
void toggleProfile(NodeProfile p, bool e) {
|
||||||
@@ -503,6 +583,8 @@ class AppState extends ChangeNotifier {
|
|||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
// Don't await - let it run in background
|
// Don't await - let it run in background
|
||||||
checkMessages();
|
checkMessages();
|
||||||
|
|
||||||
|
// Note: Re-auth check will be triggered from the settings screen after mode change
|
||||||
}
|
}
|
||||||
|
|
||||||
_startUploader(); // Restart uploader with new mode
|
_startUploader(); // Restart uploader with new mode
|
||||||
|
|||||||
@@ -183,7 +183,12 @@
|
|||||||
"currentDestinationSimulate": "Derzeit im: Simulationsmodus (kein echtes Konto)",
|
"currentDestinationSimulate": "Derzeit im: Simulationsmodus (kein echtes Konto)",
|
||||||
"viewMessages": "Nachrichten auf OSM anzeigen",
|
"viewMessages": "Nachrichten auf OSM anzeigen",
|
||||||
"unreadMessagesCount": "Sie haben {} ungelesene Nachrichten",
|
"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": {
|
"queue": {
|
||||||
"title": "Upload-Warteschlange",
|
"title": "Upload-Warteschlange",
|
||||||
|
|||||||
@@ -215,7 +215,12 @@
|
|||||||
"currentDestinationSimulate": "Currently in: Simulate mode (no real account)",
|
"currentDestinationSimulate": "Currently in: Simulate mode (no real account)",
|
||||||
"viewMessages": "View Messages on OSM",
|
"viewMessages": "View Messages on OSM",
|
||||||
"unreadMessagesCount": "You have {} unread messages",
|
"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": {
|
"queue": {
|
||||||
"title": "Upload Queue",
|
"title": "Upload Queue",
|
||||||
|
|||||||
@@ -215,7 +215,12 @@
|
|||||||
"currentDestinationSimulate": "Actualmente en: Modo de simulación (sin cuenta real)",
|
"currentDestinationSimulate": "Actualmente en: Modo de simulación (sin cuenta real)",
|
||||||
"viewMessages": "Ver Mensajes en OSM",
|
"viewMessages": "Ver Mensajes en OSM",
|
||||||
"unreadMessagesCount": "Tienes {} mensajes sin leer",
|
"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": {
|
"queue": {
|
||||||
"title": "Cola de Subida",
|
"title": "Cola de Subida",
|
||||||
|
|||||||
@@ -215,7 +215,12 @@
|
|||||||
"currentDestinationSimulate": "Actuellement en : Mode simulation (pas de compte réel)",
|
"currentDestinationSimulate": "Actuellement en : Mode simulation (pas de compte réel)",
|
||||||
"viewMessages": "Voir les Messages sur OSM",
|
"viewMessages": "Voir les Messages sur OSM",
|
||||||
"unreadMessagesCount": "Vous avez {} messages non lus",
|
"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": {
|
"queue": {
|
||||||
"title": "File de Téléchargement",
|
"title": "File de Téléchargement",
|
||||||
|
|||||||
@@ -215,7 +215,12 @@
|
|||||||
"currentDestinationSimulate": "Attualmente in: Modalità simulazione (nessun account reale)",
|
"currentDestinationSimulate": "Attualmente in: Modalità simulazione (nessun account reale)",
|
||||||
"viewMessages": "Visualizza Messaggi su OSM",
|
"viewMessages": "Visualizza Messaggi su OSM",
|
||||||
"unreadMessagesCount": "Hai {} messaggi non letti",
|
"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": {
|
"queue": {
|
||||||
"title": "Coda di Upload",
|
"title": "Coda di Upload",
|
||||||
|
|||||||
@@ -215,7 +215,12 @@
|
|||||||
"currentDestinationSimulate": "Atualmente em: Modo de simulação (sem conta real)",
|
"currentDestinationSimulate": "Atualmente em: Modo de simulação (sem conta real)",
|
||||||
"viewMessages": "Ver Mensagens no OSM",
|
"viewMessages": "Ver Mensagens no OSM",
|
||||||
"unreadMessagesCount": "Você tem {} mensagens não lidas",
|
"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": {
|
"queue": {
|
||||||
"title": "Fila de Upload",
|
"title": "Fila de Upload",
|
||||||
|
|||||||
@@ -215,7 +215,12 @@
|
|||||||
"currentDestinationSimulate": "当前处于:模拟模式(无真实账户)",
|
"currentDestinationSimulate": "当前处于:模拟模式(无真实账户)",
|
||||||
"viewMessages": "在 OSM 上查看消息",
|
"viewMessages": "在 OSM 上查看消息",
|
||||||
"unreadMessagesCount": "您有 {} 条未读消息",
|
"unreadMessagesCount": "您有 {} 条未读消息",
|
||||||
"noUnreadMessages": "没有未读消息"
|
"noUnreadMessages": "没有未读消息",
|
||||||
|
"reauthRequired": "刷新身份验证",
|
||||||
|
"reauthExplanation": "您必须刷新身份验证才能通过应用接收 OSM 消息通知。",
|
||||||
|
"reauthBenefit": "这将在您在 OpenStreetMap 上有未读消息时启用通知点。",
|
||||||
|
"reauthNow": "现在执行",
|
||||||
|
"reauthLater": "稍后"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"title": "上传队列",
|
"title": "上传队列",
|
||||||
|
|||||||
@@ -673,7 +673,11 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
// Check for welcome/changelog popup after app is fully initialized
|
// Check for welcome/changelog popup after app is fully initialized
|
||||||
if (appState.isInitialized && !_hasCheckedForPopup) {
|
if (appState.isInitialized && !_hasCheckedForPopup) {
|
||||||
_hasCheckedForPopup = true;
|
_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
|
// Pass the active sheet height directly to the map
|
||||||
@@ -720,7 +724,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
icon: Stack(
|
icon: Stack(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.settings),
|
const Icon(Icons.settings),
|
||||||
if (appState.hasUnreadMessages && appState.uploadMode != UploadMode.simulate)
|
if (appState.hasUnreadMessages)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@@ -239,8 +239,8 @@ class _OSMAccountScreenState extends State<OSMAccountScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Account deletion section - only show when logged in
|
// Account deletion section - only show when logged in and not in simulate mode
|
||||||
if (appState.isLoggedIn) ...[
|
if (appState.isLoggedIn && appState.uploadMode != UploadMode.simulate) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildAccountDeletionSection(context, appState),
|
_buildAccountDeletionSection(context, appState),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ class UploadModeSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
onChanged: (mode) {
|
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(
|
leading: Stack(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.account_circle),
|
const Icon(Icons.account_circle),
|
||||||
if (appState.hasUnreadMessages && appState.uploadMode != UploadMode.simulate)
|
if (appState.hasUnreadMessages)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class AuthService {
|
|||||||
_helper = OAuth2Helper(
|
_helper = OAuth2Helper(
|
||||||
client,
|
client,
|
||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
scopes: ['read_prefs', 'write_api'],
|
scopes: ['read_prefs', 'write_api', 'consume_messages'],
|
||||||
enablePKCE: true,
|
enablePKCE: true,
|
||||||
// tokenStorageKey: _tokenKey, // not supported by this package version
|
// tokenStorageKey: _tokenKey, // not supported by this package version
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,8 +55,11 @@ class OSMMessagesService {
|
|||||||
final messages = user['messages'];
|
final messages = user['messages'];
|
||||||
if (messages == null) return null;
|
if (messages == null) return null;
|
||||||
|
|
||||||
// Get unread count
|
// Get unread count from correct path: messages.received.unread
|
||||||
final unreadCount = messages['unread']?['count'] ?? 0;
|
final received = messages['received'];
|
||||||
|
if (received == null) return null;
|
||||||
|
|
||||||
|
final unreadCount = received['unread'] ?? 0;
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
_lastCheck = DateTime.now();
|
_lastCheck = DateTime.now();
|
||||||
|
|||||||
@@ -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