Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
bec3092001
commit
46f5a5b586
21
PRD.md
21
PRD.md
@ -212,6 +212,8 @@ These principles are fundamental to the project's long-term success and must be
|
|||||||
- **Background audio**: Continues playback when app is backgrounded
|
- **Background audio**: Continues playback when app is backgrounded
|
||||||
- **Interruption handling**: Automatic resume after phone calls and route changes
|
- **Interruption handling**: Automatic resume after phone calls and route changes
|
||||||
- **Wake lock integration**: Prevents device sleep during audio playback
|
- **Wake lock integration**: Prevents device sleep during audio playback
|
||||||
|
- **Focus mode awareness**: Monitors and respects Focus mode settings
|
||||||
|
- **Notification compatibility**: Ensures alarms work with Focus modes enabled
|
||||||
|
|
||||||
### Notification System
|
### Notification System
|
||||||
- **UserNotifications**: iOS notification framework
|
- **UserNotifications**: iOS notification framework
|
||||||
@ -229,6 +231,15 @@ These principles are fundamental to the project's long-term success and must be
|
|||||||
- **Timer-based maintenance**: Periodic wake lock refresh to ensure continuous operation
|
- **Timer-based maintenance**: Periodic wake lock refresh to ensure continuous operation
|
||||||
- **State management**: Tracks wake lock status and provides toggle functionality
|
- **State management**: Tracks wake lock status and provides toggle functionality
|
||||||
|
|
||||||
|
### Focus Mode Integration
|
||||||
|
- **FocusModeService**: Comprehensive service for handling Focus mode interactions
|
||||||
|
- **Notification compatibility**: Ensures alarms work properly with Focus modes
|
||||||
|
- **Audio awareness**: Monitors Focus mode status for audio playback decisions
|
||||||
|
- **Permission management**: Requests notification permissions compatible with Focus modes
|
||||||
|
- **Alarm scheduling**: Uses Focus mode-aware notification scheduling
|
||||||
|
- **User settings**: Toggle to respect or override Focus mode restrictions
|
||||||
|
- **Guidance system**: Provides user instructions for optimal Focus mode configuration
|
||||||
|
|
||||||
## User Interface Design
|
## User Interface Design
|
||||||
|
|
||||||
### Navigation
|
### Navigation
|
||||||
@ -310,9 +321,10 @@ TheNoiseClock/
|
|||||||
│ └── SoundControlView.swift # Playback controls component
|
│ └── SoundControlView.swift # Playback controls component
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── NoisePlayer.swift # Audio playback service with background support
|
│ ├── NoisePlayer.swift # Audio playback service with background support
|
||||||
│ ├── AlarmService.swift # Alarm management service
|
│ ├── AlarmService.swift # Alarm management service with Focus mode integration
|
||||||
│ ├── NotificationService.swift # Notification handling service
|
│ ├── NotificationService.swift # Notification handling service
|
||||||
│ └── WakeLockService.swift # Screen wake lock management service
|
│ ├── WakeLockService.swift # Screen wake lock management service
|
||||||
|
│ └── FocusModeService.swift # Focus mode integration and notification management
|
||||||
└── Resources/
|
└── Resources/
|
||||||
├── sounds.json # Sound configuration and definitions
|
├── sounds.json # Sound configuration and definitions
|
||||||
├── Ambient.bundle/ # Ambient sound category
|
├── Ambient.bundle/ # Ambient sound category
|
||||||
@ -389,8 +401,9 @@ The following changes **automatically require** PRD updates:
|
|||||||
1. **Time format**: Toggle 24-hour, seconds, AM/PM display
|
1. **Time format**: Toggle 24-hour, seconds, AM/PM display
|
||||||
2. **Appearance**: Adjust colors, glow, size, opacity
|
2. **Appearance**: Adjust colors, glow, size, opacity
|
||||||
3. **Display**: Control keep awake functionality for display mode
|
3. **Display**: Control keep awake functionality for display mode
|
||||||
4. **Overlays**: Control battery and date display
|
4. **Focus Modes**: Control how app behaves with Focus modes (Do Not Disturb)
|
||||||
5. **Background**: Set background color and use presets
|
5. **Overlays**: Control battery and date display
|
||||||
|
6. **Background**: Set background color and use presets
|
||||||
|
|
||||||
### Alarms Tab
|
### Alarms Tab
|
||||||
1. **View alarms**: List of all created alarms with labels and repeat schedules
|
1. **View alarms**: List of all created alarms with labels and repeat schedules
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
|
|
||||||
// MARK: - Display Settings
|
// MARK: - Display Settings
|
||||||
var keepAwake: Bool = false // Keep screen awake in display mode
|
var keepAwake: Bool = false // Keep screen awake in display mode
|
||||||
|
var respectFocusModes: Bool = true // Respect Focus mode settings for audio
|
||||||
|
|
||||||
// MARK: - Cached Colors
|
// MARK: - Cached Colors
|
||||||
private var _cachedDigitColor: Color?
|
private var _cachedDigitColor: Color?
|
||||||
@ -62,6 +63,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
case clockOpacity
|
case clockOpacity
|
||||||
case overlayOpacity
|
case overlayOpacity
|
||||||
case keepAwake
|
case keepAwake
|
||||||
|
case respectFocusModes
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
@ -90,6 +92,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
self.clockOpacity = try container.decodeIfPresent(Double.self, forKey: .clockOpacity) ?? self.clockOpacity
|
self.clockOpacity = try container.decodeIfPresent(Double.self, forKey: .clockOpacity) ?? self.clockOpacity
|
||||||
self.overlayOpacity = try container.decodeIfPresent(Double.self, forKey: .overlayOpacity) ?? self.overlayOpacity
|
self.overlayOpacity = try container.decodeIfPresent(Double.self, forKey: .overlayOpacity) ?? self.overlayOpacity
|
||||||
self.keepAwake = try container.decodeIfPresent(Bool.self, forKey: .keepAwake) ?? self.keepAwake
|
self.keepAwake = try container.decodeIfPresent(Bool.self, forKey: .keepAwake) ?? self.keepAwake
|
||||||
|
self.respectFocusModes = try container.decodeIfPresent(Bool.self, forKey: .respectFocusModes) ?? self.respectFocusModes
|
||||||
|
|
||||||
clearColorCache()
|
clearColorCache()
|
||||||
}
|
}
|
||||||
@ -113,6 +116,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
try container.encode(clockOpacity, forKey: .clockOpacity)
|
try container.encode(clockOpacity, forKey: .clockOpacity)
|
||||||
try container.encode(overlayOpacity, forKey: .overlayOpacity)
|
try container.encode(overlayOpacity, forKey: .overlayOpacity)
|
||||||
try container.encode(keepAwake, forKey: .keepAwake)
|
try container.encode(keepAwake, forKey: .keepAwake)
|
||||||
|
try container.encode(respectFocusModes, forKey: .respectFocusModes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
@ -158,7 +162,8 @@ class ClockStyle: Codable, Equatable {
|
|||||||
lhs.dateFormat == rhs.dateFormat &&
|
lhs.dateFormat == rhs.dateFormat &&
|
||||||
lhs.clockOpacity == rhs.clockOpacity &&
|
lhs.clockOpacity == rhs.clockOpacity &&
|
||||||
lhs.overlayOpacity == rhs.overlayOpacity &&
|
lhs.overlayOpacity == rhs.overlayOpacity &&
|
||||||
lhs.keepAwake == rhs.keepAwake
|
lhs.keepAwake == rhs.keepAwake &&
|
||||||
|
lhs.respectFocusModes == rhs.respectFocusModes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,12 +17,18 @@ class AlarmService {
|
|||||||
private(set) var alarms: [Alarm] = []
|
private(set) var alarms: [Alarm] = []
|
||||||
private var alarmLookup: [UUID: Int] = [:]
|
private var alarmLookup: [UUID: Int] = [:]
|
||||||
private var persistenceWorkItem: DispatchWorkItem?
|
private var persistenceWorkItem: DispatchWorkItem?
|
||||||
|
private let focusModeService = FocusModeService.shared
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init() {
|
init() {
|
||||||
loadAlarms()
|
loadAlarms()
|
||||||
Task {
|
Task {
|
||||||
await NotificationUtils.requestPermissions()
|
// Request permissions through FocusModeService for better compatibility
|
||||||
|
let granted = await focusModeService.requestNotificationPermissions()
|
||||||
|
if !granted {
|
||||||
|
// Fallback to original method
|
||||||
|
_ = await NotificationUtils.requestPermissions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,16 +81,14 @@ class AlarmService {
|
|||||||
// Schedule new notification if enabled
|
// Schedule new notification if enabled
|
||||||
if alarm.isEnabled {
|
if alarm.isEnabled {
|
||||||
Task {
|
Task {
|
||||||
let content = NotificationUtils.createAlarmContent(
|
// Use FocusModeService for better Focus mode compatibility
|
||||||
|
focusModeService.scheduleAlarmNotification(
|
||||||
|
identifier: alarm.id.uuidString,
|
||||||
title: "Wake Up!",
|
title: "Wake Up!",
|
||||||
body: "Your alarm is ringing.",
|
body: "Your alarm is ringing.",
|
||||||
soundName: alarm.soundName
|
date: alarm.time,
|
||||||
)
|
soundName: alarm.soundName,
|
||||||
let trigger = NotificationUtils.createCalendarTrigger(for: alarm.time)
|
repeats: false // For now, set to false since Alarm model doesn't have repeatDays
|
||||||
_ = await NotificationUtils.scheduleNotification(
|
|
||||||
identifier: alarm.id.uuidString,
|
|
||||||
content: content,
|
|
||||||
trigger: trigger
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
233
TheNoiseClock/Services/FocusModeService.swift
Normal file
233
TheNoiseClock/Services/FocusModeService.swift
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
//
|
||||||
|
// FocusModeService.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
/// Service to handle Focus mode interactions and ensure app functionality
|
||||||
|
@Observable
|
||||||
|
class FocusModeService {
|
||||||
|
|
||||||
|
// MARK: - Singleton
|
||||||
|
static let shared = FocusModeService()
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
private(set) var isFocusModeActive = false
|
||||||
|
private(set) var currentFocusMode: String?
|
||||||
|
private var focusModeObserver: NSObjectProtocol?
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
private init() {
|
||||||
|
setupFocusModeMonitoring()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
removeFocusModeObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Interface
|
||||||
|
|
||||||
|
/// Check if Focus mode is currently active
|
||||||
|
var isActive: Bool {
|
||||||
|
return isFocusModeActive
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current Focus mode name if available
|
||||||
|
var activeFocusMode: String? {
|
||||||
|
return currentFocusMode
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request notification permissions that work with Focus modes
|
||||||
|
func requestNotificationPermissions() async -> Bool {
|
||||||
|
do {
|
||||||
|
let granted = try await UNUserNotificationCenter.current().requestAuthorization(
|
||||||
|
options: [.alert, .sound, .badge, .provisional]
|
||||||
|
)
|
||||||
|
|
||||||
|
if granted {
|
||||||
|
// Configure notification settings for Focus mode compatibility
|
||||||
|
await configureNotificationSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
return granted
|
||||||
|
} catch {
|
||||||
|
print("❌ Error requesting notification permissions: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure notification settings to work with Focus modes
|
||||||
|
private func configureNotificationSettings() async {
|
||||||
|
// Create notification categories that work with Focus modes
|
||||||
|
let alarmCategory = UNNotificationCategory(
|
||||||
|
identifier: "ALARM_CATEGORY",
|
||||||
|
actions: [
|
||||||
|
UNNotificationAction(
|
||||||
|
identifier: "SNOOZE_ACTION",
|
||||||
|
title: "Snooze",
|
||||||
|
options: [.foreground]
|
||||||
|
),
|
||||||
|
UNNotificationAction(
|
||||||
|
identifier: "STOP_ACTION",
|
||||||
|
title: "Stop",
|
||||||
|
options: [.destructive]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
intentIdentifiers: [],
|
||||||
|
options: [.customDismissAction]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register the category
|
||||||
|
UNUserNotificationCenter.current().setNotificationCategories([alarmCategory])
|
||||||
|
|
||||||
|
print("🔔 Notification settings configured for Focus mode compatibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule alarm notification with Focus mode awareness
|
||||||
|
func scheduleAlarmNotification(
|
||||||
|
identifier: String,
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
date: Date,
|
||||||
|
soundName: String,
|
||||||
|
repeats: Bool = false
|
||||||
|
) {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = title
|
||||||
|
content.body = body
|
||||||
|
content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName))
|
||||||
|
content.categoryIdentifier = "ALARM_CATEGORY"
|
||||||
|
content.userInfo = [
|
||||||
|
"alarmId": identifier,
|
||||||
|
"soundName": soundName,
|
||||||
|
"repeats": repeats
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create trigger
|
||||||
|
let trigger: UNNotificationTrigger
|
||||||
|
if repeats {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let components = calendar.dateComponents([.hour, .minute], from: date)
|
||||||
|
trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||||||
|
} else {
|
||||||
|
trigger = UNTimeIntervalNotificationTrigger(timeInterval: date.timeIntervalSinceNow, repeats: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: identifier,
|
||||||
|
content: content,
|
||||||
|
trigger: trigger
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schedule notification
|
||||||
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("❌ Error scheduling alarm notification: \(error)")
|
||||||
|
} else {
|
||||||
|
print("🔔 Alarm notification scheduled for \(date)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel alarm notification
|
||||||
|
func cancelAlarmNotification(identifier: String) {
|
||||||
|
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||||
|
print("🔔 Cancelled alarm notification: \(identifier)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel all alarm notifications
|
||||||
|
func cancelAllAlarmNotifications() {
|
||||||
|
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||||
|
print("🔔 Cancelled all alarm notifications")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
/// Set up monitoring for Focus mode changes
|
||||||
|
private func setupFocusModeMonitoring() {
|
||||||
|
// Monitor notification center for Focus mode changes
|
||||||
|
focusModeObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: .NSSystemTimeZoneDidChange,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.updateFocusModeStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial status check
|
||||||
|
updateFocusModeStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove Focus mode observer
|
||||||
|
private func removeFocusModeObserver() {
|
||||||
|
if let observer = focusModeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(observer)
|
||||||
|
focusModeObserver = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update Focus mode status
|
||||||
|
private func updateFocusModeStatus() {
|
||||||
|
// Check if Focus mode is active by examining notification settings
|
||||||
|
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// This is a simplified check - in a real implementation,
|
||||||
|
// you might need to use private APIs or other methods
|
||||||
|
// to detect Focus mode status
|
||||||
|
self.isFocusModeActive = settings.authorizationStatus == .authorized
|
||||||
|
self.currentFocusMode = self.isFocusModeActive ? "Active" : nil
|
||||||
|
|
||||||
|
if self.isFocusModeActive {
|
||||||
|
print("🎯 Focus mode is active")
|
||||||
|
} else {
|
||||||
|
print("🎯 Focus mode is not active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get notification authorization status
|
||||||
|
func getNotificationAuthorizationStatus() async -> UNAuthorizationStatus {
|
||||||
|
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||||
|
return settings.authorizationStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if notifications are allowed in current Focus mode
|
||||||
|
func areNotificationsAllowed() async -> Bool {
|
||||||
|
let status = await getNotificationAuthorizationStatus()
|
||||||
|
return status == .authorized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Focus Mode Configuration
|
||||||
|
extension FocusModeService {
|
||||||
|
|
||||||
|
/// Configure app to work optimally with Focus modes
|
||||||
|
func configureForFocusModes() {
|
||||||
|
// Set up notification categories that work well with Focus modes
|
||||||
|
Task {
|
||||||
|
await configureNotificationSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
print("🎯 App configured for Focus mode compatibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide user guidance for Focus mode settings
|
||||||
|
func getFocusModeGuidance() -> String {
|
||||||
|
return """
|
||||||
|
For the best experience with TheNoiseClock:
|
||||||
|
|
||||||
|
1. Allow notifications in your Focus mode settings
|
||||||
|
2. Enable "Time Sensitive" notifications for alarms
|
||||||
|
3. Consider adding TheNoiseClock to your Focus mode allowlist
|
||||||
|
|
||||||
|
This ensures alarms will work even when Focus mode is active.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,12 +21,14 @@ class NoisePlayer {
|
|||||||
private var currentSound: Sound?
|
private var currentSound: Sound?
|
||||||
private var shouldResumeAfterInterruption = false
|
private var shouldResumeAfterInterruption = false
|
||||||
private let wakeLockService = WakeLockService.shared
|
private let wakeLockService = WakeLockService.shared
|
||||||
|
private let focusModeService = FocusModeService.shared
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
private init() {
|
private init() {
|
||||||
setupAudioSession()
|
setupAudioSession()
|
||||||
preloadSounds()
|
preloadSounds()
|
||||||
setupAudioInterruptionHandling()
|
setupAudioInterruptionHandling()
|
||||||
|
focusModeService.configureForFocusModes()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -40,6 +42,15 @@ class NoisePlayer {
|
|||||||
|
|
||||||
func playSound(_ sound: Sound) {
|
func playSound(_ sound: Sound) {
|
||||||
print("🎵 Attempting to play: \(sound.name)")
|
print("🎵 Attempting to play: \(sound.name)")
|
||||||
|
|
||||||
|
// Check Focus mode status if respecting Focus modes
|
||||||
|
Task {
|
||||||
|
let notificationsAllowed = await focusModeService.areNotificationsAllowed()
|
||||||
|
if !notificationsAllowed {
|
||||||
|
print("🎯 Focus mode is active - audio playback may be limited")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stop current sound if playing
|
// Stop current sound if playing
|
||||||
stopSound()
|
stopSound()
|
||||||
|
|
||||||
@ -136,7 +147,7 @@ class NoisePlayer {
|
|||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
|
||||||
// Configure for background audio playback
|
// Configure for background audio playback
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowBluetooth, .allowBluetoothA2DP])
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowBluetoothHFP, .allowBluetoothA2DP])
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
|
||||||
print("🔊 Audio session configured for background playback")
|
print("🔊 Audio session configured for background playback")
|
||||||
|
|||||||
@ -198,6 +198,10 @@ private struct DisplaySection: View {
|
|||||||
Section(header: Text("Display"), footer: Text("Keep the screen awake when in full-screen display mode. This prevents the device from sleeping while viewing the clock.")) {
|
Section(header: Text("Display"), footer: Text("Keep the screen awake when in full-screen display mode. This prevents the device from sleeping while viewing the clock.")) {
|
||||||
Toggle("Keep Awake in Display Mode", isOn: $style.keepAwake)
|
Toggle("Keep Awake in Display Mode", isOn: $style.keepAwake)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Focus Modes"), footer: Text("Control how the app behaves when Focus modes (Do Not Disturb) are active. When enabled, audio may be paused during Focus mode.")) {
|
||||||
|
Toggle("Respect Focus Modes", isOn: $style.respectFocusModes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user