add global notification
This commit is contained in:
		
							parent
							
								
									a6f399bb08
								
							
						
					
					
						commit
						3658d5a963
					
				
							
								
								
									
										51
									
								
								yobble/Components/NewMessageBannerView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								yobble/Components/NewMessageBannerView.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct IncomingMessageBanner: Identifiable {
 | 
			
		||||
    let id = UUID()
 | 
			
		||||
    let message: MessageItem
 | 
			
		||||
    let senderName: String
 | 
			
		||||
    let messagePreview: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct NewMessageBannerView: View {
 | 
			
		||||
    let banner: IncomingMessageBanner
 | 
			
		||||
    let onOpen: () -> Void
 | 
			
		||||
    let onDismiss: () -> Void
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack(alignment: .center, spacing: 12) {
 | 
			
		||||
            Image(systemName: "bubble.left.and.bubble.right.fill")
 | 
			
		||||
                .foregroundColor(.accentColor)
 | 
			
		||||
                .imageScale(.medium)
 | 
			
		||||
 | 
			
		||||
            VStack(alignment: .leading, spacing: 4) {
 | 
			
		||||
                Text(banner.senderName)
 | 
			
		||||
                    .font(.headline)
 | 
			
		||||
                    .foregroundColor(.primary)
 | 
			
		||||
                    .lineLimit(1)
 | 
			
		||||
 | 
			
		||||
                Text(banner.messagePreview)
 | 
			
		||||
                    .font(.subheadline)
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
                    .lineLimit(2)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Spacer(minLength: 8)
 | 
			
		||||
 | 
			
		||||
            Button(action: onDismiss) {
 | 
			
		||||
                Image(systemName: "xmark")
 | 
			
		||||
                    .font(.footnote.weight(.semibold))
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
                    .padding(6)
 | 
			
		||||
                    .background(Color.secondary.opacity(0.15), in: Circle())
 | 
			
		||||
            }
 | 
			
		||||
            .buttonStyle(.plain)
 | 
			
		||||
        }
 | 
			
		||||
        .padding(.horizontal, 16)
 | 
			
		||||
        .padding(.vertical, 14)
 | 
			
		||||
        .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
 | 
			
		||||
        .shadow(color: Color.black.opacity(0.12), radius: 12, y: 6)
 | 
			
		||||
        .contentShape(Rectangle())
 | 
			
		||||
        .onTapGesture(perform: onOpen)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								yobble/Models/ChatDeepLink.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								yobble/Models/ChatDeepLink.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
struct ChatDeepLink: Identifiable {
 | 
			
		||||
    let id = UUID()
 | 
			
		||||
    let chatId: String
 | 
			
		||||
    let chatProfile: ChatProfile?
 | 
			
		||||
    let message: MessageItem?
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										97
									
								
								yobble/Services/IncomingMessageCenter.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								yobble/Services/IncomingMessageCenter.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import Combine
 | 
			
		||||
 | 
			
		||||
final class IncomingMessageCenter: ObservableObject {
 | 
			
		||||
    @Published private(set) var banner: IncomingMessageBanner?
 | 
			
		||||
    @Published var pendingDeepLink: ChatDeepLink?
 | 
			
		||||
    var currentUserId: String?
 | 
			
		||||
 | 
			
		||||
    private var dismissWorkItem: DispatchWorkItem?
 | 
			
		||||
    private var cancellables = Set<AnyCancellable>()
 | 
			
		||||
    private let notificationCenter: NotificationCenter
 | 
			
		||||
 | 
			
		||||
    init(notificationCenter: NotificationCenter = .default) {
 | 
			
		||||
        self.notificationCenter = notificationCenter
 | 
			
		||||
        notificationCenter.publisher(for: .socketDidReceivePrivateMessage)
 | 
			
		||||
            .compactMap { $0.object as? MessageItem }
 | 
			
		||||
            .receive(on: RunLoop.main)
 | 
			
		||||
            .sink { [weak self] message in
 | 
			
		||||
                self?.handleIncoming(message)
 | 
			
		||||
            }
 | 
			
		||||
            .store(in: &cancellables)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func dismissBanner() {
 | 
			
		||||
        dismissWorkItem?.cancel()
 | 
			
		||||
        dismissWorkItem = nil
 | 
			
		||||
        banner = nil
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func openCurrentChat() {
 | 
			
		||||
        guard let banner else { return }
 | 
			
		||||
        pendingDeepLink = ChatDeepLink(
 | 
			
		||||
            chatId: banner.message.chatId,
 | 
			
		||||
            chatProfile: banner.message.senderData,
 | 
			
		||||
            message: banner.message
 | 
			
		||||
        )
 | 
			
		||||
        dismissBanner()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func handleIncoming(_ message: MessageItem) {
 | 
			
		||||
        if let currentUserId, message.senderId == currentUserId {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        banner = buildBanner(from: message)
 | 
			
		||||
        scheduleDismiss()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func buildBanner(from message: MessageItem) -> IncomingMessageBanner {
 | 
			
		||||
        let sender = senderName(for: message)
 | 
			
		||||
        let preview = messagePreview(for: message)
 | 
			
		||||
        return IncomingMessageBanner(message: message, senderName: sender, messagePreview: preview)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func senderName(for message: MessageItem) -> String {
 | 
			
		||||
        let candidates: [String?] = [
 | 
			
		||||
            message.senderData?.customName,
 | 
			
		||||
            message.senderData?.fullName,
 | 
			
		||||
            message.senderData?.login.map { "@\($0)" }
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for candidate in candidates {
 | 
			
		||||
            if let value = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {
 | 
			
		||||
                return value
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return message.senderId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func messagePreview(for message: MessageItem) -> String {
 | 
			
		||||
        if let content = message.content?.trimmingCharacters(in: .whitespacesAndNewlines), !content.isEmpty {
 | 
			
		||||
            return content
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch message.messageType.lowercased() {
 | 
			
		||||
        case "image":
 | 
			
		||||
            return NSLocalizedString("Изображение", comment: "Image message placeholder")
 | 
			
		||||
        case "audio":
 | 
			
		||||
            return NSLocalizedString("Аудио", comment: "Audio message placeholder")
 | 
			
		||||
        case "video":
 | 
			
		||||
            return NSLocalizedString("Видео", comment: "Video message placeholder")
 | 
			
		||||
        default:
 | 
			
		||||
            return NSLocalizedString("Новое сообщение", comment: "Default banner subtitle")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func scheduleDismiss(after delay: TimeInterval = 5) {
 | 
			
		||||
        dismissWorkItem?.cancel()
 | 
			
		||||
        let workItem = DispatchWorkItem { [weak self] in
 | 
			
		||||
            self?.banner = nil
 | 
			
		||||
            self?.dismissWorkItem = nil
 | 
			
		||||
        }
 | 
			
		||||
        dismissWorkItem = workItem
 | 
			
		||||
        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -10,13 +10,6 @@ import SwiftUI
 | 
			
		||||
import UIKit
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
struct ChatDeepLink: Identifiable {
 | 
			
		||||
    let id = UUID()
 | 
			
		||||
    let chatId: String
 | 
			
		||||
    let chatProfile: ChatProfile?
 | 
			
		||||
    let message: MessageItem?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ChatsTab: View {
 | 
			
		||||
    @ObservedObject private var loginViewModel: LoginViewModel
 | 
			
		||||
    @Binding private var pendingDeepLink: ChatDeepLink?
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct MainView: View {
 | 
			
		||||
    @ObservedObject var viewModel: LoginViewModel
 | 
			
		||||
    @EnvironmentObject private var messageCenter: IncomingMessageCenter
 | 
			
		||||
    @State private var selectedTab: Int = 0
 | 
			
		||||
//    @StateObject private var newHomeTabViewModel = NewHomeTabViewModel()
 | 
			
		||||
    
 | 
			
		||||
@ -15,10 +16,7 @@ struct MainView: View {
 | 
			
		||||
    @State private var menuOffset: CGFloat = 0
 | 
			
		||||
    @State private var chatSearchRevealProgress: CGFloat = 0
 | 
			
		||||
    @State private var chatSearchText: String = ""
 | 
			
		||||
    @State private var pendingChatDeepLink: ChatDeepLink?
 | 
			
		||||
    @State private var incomingMessageBanner: IncomingMessageBanner?
 | 
			
		||||
    @State private var bannerDismissWorkItem: DispatchWorkItem?
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private var tabTitle: String {
 | 
			
		||||
        switch selectedTab {
 | 
			
		||||
        case 0: return "Home"
 | 
			
		||||
@ -58,7 +56,10 @@ struct MainView: View {
 | 
			
		||||
 | 
			
		||||
                            ChatsTab(
 | 
			
		||||
                                loginViewModel: viewModel,
 | 
			
		||||
                                pendingDeepLink: $pendingChatDeepLink,
 | 
			
		||||
                                pendingDeepLink: Binding(
 | 
			
		||||
                                    get: { messageCenter.pendingDeepLink },
 | 
			
		||||
                                    set: { messageCenter.pendingDeepLink = $0 }
 | 
			
		||||
                                ),
 | 
			
		||||
                                searchRevealProgress: $chatSearchRevealProgress,
 | 
			
		||||
                                searchText: $chatSearchText
 | 
			
		||||
                            )
 | 
			
		||||
@ -98,19 +99,6 @@ struct MainView: View {
 | 
			
		||||
                        .offset(x: -menuWidth + menuOffset) // Новая логика смещения
 | 
			
		||||
                        .ignoresSafeArea(edges: .vertical)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if let banner = incomingMessageBanner {
 | 
			
		||||
                    NewMessageBannerView(
 | 
			
		||||
                        senderName: banner.senderName,
 | 
			
		||||
                        messagePreview: banner.messagePreview,
 | 
			
		||||
                        onOpen: { openChat(from: banner) },
 | 
			
		||||
                        onDismiss: { dismissIncomingBanner() }
 | 
			
		||||
                    )
 | 
			
		||||
                    .padding(.horizontal, 16)
 | 
			
		||||
                    .padding(.top, 12)
 | 
			
		||||
                    .transition(.move(edge: .top).combined(with: .opacity))
 | 
			
		||||
                    .zIndex(1)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .gesture(
 | 
			
		||||
                DragGesture()
 | 
			
		||||
@ -152,9 +140,22 @@ struct MainView: View {
 | 
			
		||||
                menuOffset = presented ? menuWidth : 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .onReceive(NotificationCenter.default.publisher(for: .socketDidReceivePrivateMessage)) { notification in
 | 
			
		||||
            guard let message = notification.object as? MessageItem else { return }
 | 
			
		||||
            handleIncomingMessage(message)
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            messageCenter.currentUserId = viewModel.userId.isEmpty ? nil : viewModel.userId
 | 
			
		||||
        }
 | 
			
		||||
        .onChange(of: viewModel.userId) { newValue in
 | 
			
		||||
            messageCenter.currentUserId = newValue.isEmpty ? nil : newValue
 | 
			
		||||
        }
 | 
			
		||||
        .onChange(of: messageCenter.pendingDeepLink?.id) { _ in
 | 
			
		||||
            guard messageCenter.pendingDeepLink != nil else { return }
 | 
			
		||||
            withAnimation(.easeInOut) {
 | 
			
		||||
                selectedTab = 2
 | 
			
		||||
                isSideMenuPresented = false
 | 
			
		||||
                menuOffset = 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .onDisappear {
 | 
			
		||||
            messageCenter.currentUserId = nil
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -163,138 +164,7 @@ struct MainView_Previews: PreviewProvider {
 | 
			
		||||
    static var previews: some View {
 | 
			
		||||
        let mockViewModel = LoginViewModel()
 | 
			
		||||
        MainView(viewModel: mockViewModel)
 | 
			
		||||
            .environmentObject(IncomingMessageCenter())
 | 
			
		||||
            .environmentObject(ThemeManager())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private extension MainView {
 | 
			
		||||
    func handleIncomingMessage(_ message: MessageItem) {
 | 
			
		||||
        guard message.senderId != viewModel.userId else { return }
 | 
			
		||||
 | 
			
		||||
        let banner = IncomingMessageBanner(
 | 
			
		||||
            message: message,
 | 
			
		||||
            senderName: senderDisplayName(for: message),
 | 
			
		||||
            messagePreview: messagePreview(for: message)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        bannerDismissWorkItem?.cancel()
 | 
			
		||||
        withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
 | 
			
		||||
            incomingMessageBanner = banner
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let workItem = DispatchWorkItem {
 | 
			
		||||
            withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
 | 
			
		||||
                incomingMessageBanner = nil
 | 
			
		||||
            }
 | 
			
		||||
            bannerDismissWorkItem = nil
 | 
			
		||||
        }
 | 
			
		||||
        bannerDismissWorkItem = workItem
 | 
			
		||||
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func dismissIncomingBanner() {
 | 
			
		||||
        bannerDismissWorkItem?.cancel()
 | 
			
		||||
        bannerDismissWorkItem = nil
 | 
			
		||||
        withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
 | 
			
		||||
            incomingMessageBanner = nil
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func openChat(from banner: IncomingMessageBanner) {
 | 
			
		||||
        dismissIncomingBanner()
 | 
			
		||||
        pendingChatDeepLink = ChatDeepLink(
 | 
			
		||||
            chatId: banner.message.chatId,
 | 
			
		||||
            chatProfile: banner.message.senderData,
 | 
			
		||||
            message: banner.message
 | 
			
		||||
        )
 | 
			
		||||
        withAnimation(.easeInOut) {
 | 
			
		||||
            selectedTab = 2
 | 
			
		||||
            isSideMenuPresented = false
 | 
			
		||||
            menuOffset = 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func senderDisplayName(for message: MessageItem) -> String {
 | 
			
		||||
        let candidates: [String?] = [
 | 
			
		||||
            message.senderData?.customName,
 | 
			
		||||
            message.senderData?.fullName,
 | 
			
		||||
            message.senderData?.login.map { "@\($0)" }
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for candidate in candidates {
 | 
			
		||||
            if let value = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {
 | 
			
		||||
                return value
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return message.senderId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func messagePreview(for message: MessageItem) -> String {
 | 
			
		||||
        if let content = message.content?.trimmingCharacters(in: .whitespacesAndNewlines), !content.isEmpty {
 | 
			
		||||
            return content
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch message.messageType.lowercased() {
 | 
			
		||||
        case "image":
 | 
			
		||||
            return NSLocalizedString("Изображение", comment: "Image message placeholder")
 | 
			
		||||
        case "audio":
 | 
			
		||||
            return NSLocalizedString("Аудио", comment: "Audio message placeholder")
 | 
			
		||||
        case "video":
 | 
			
		||||
            return NSLocalizedString("Видео", comment: "Video message placeholder")
 | 
			
		||||
        default:
 | 
			
		||||
            return NSLocalizedString("Новое сообщение", comment: "Default banner subtitle")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct IncomingMessageBanner: Identifiable {
 | 
			
		||||
    let id = UUID()
 | 
			
		||||
    let message: MessageItem
 | 
			
		||||
    let senderName: String
 | 
			
		||||
    let messagePreview: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct NewMessageBannerView: View {
 | 
			
		||||
    let senderName: String
 | 
			
		||||
    let messagePreview: String
 | 
			
		||||
    let onOpen: () -> Void
 | 
			
		||||
    let onDismiss: () -> Void
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack(alignment: .center, spacing: 12) {
 | 
			
		||||
            Image(systemName: "bubble.left.and.bubble.right.fill")
 | 
			
		||||
                .foregroundColor(.accentColor)
 | 
			
		||||
                .imageScale(.medium)
 | 
			
		||||
 | 
			
		||||
            VStack(alignment: .leading, spacing: 4) {
 | 
			
		||||
                Text(senderName)
 | 
			
		||||
                    .font(.headline)
 | 
			
		||||
                    .foregroundColor(.primary)
 | 
			
		||||
                    .lineLimit(1)
 | 
			
		||||
 | 
			
		||||
                Text(messagePreview)
 | 
			
		||||
                    .font(.subheadline)
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
                    .lineLimit(2)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Spacer(minLength: 8)
 | 
			
		||||
 | 
			
		||||
            Button(action: onDismiss) {
 | 
			
		||||
                Image(systemName: "xmark")
 | 
			
		||||
                    .font(.footnote.weight(.semibold))
 | 
			
		||||
                    .foregroundColor(.secondary)
 | 
			
		||||
                    .padding(6)
 | 
			
		||||
                    .background(Color.secondary.opacity(0.15), in: Circle())
 | 
			
		||||
            }
 | 
			
		||||
            .buttonStyle(.plain)
 | 
			
		||||
        }
 | 
			
		||||
        .padding(.horizontal, 16)
 | 
			
		||||
        .padding(.vertical, 14)
 | 
			
		||||
        .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
 | 
			
		||||
        .shadow(color: Color.black.opacity(0.12), radius: 12, y: 6)
 | 
			
		||||
        .contentShape(Rectangle())
 | 
			
		||||
        .onTapGesture(perform: onOpen)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,19 +12,36 @@ import CoreData
 | 
			
		||||
struct yobbleApp: App {
 | 
			
		||||
    @StateObject private var themeManager = ThemeManager()
 | 
			
		||||
    @StateObject private var viewModel = LoginViewModel()
 | 
			
		||||
    @StateObject private var messageCenter = IncomingMessageCenter()
 | 
			
		||||
    private let persistenceController = PersistenceController.shared
 | 
			
		||||
    
 | 
			
		||||
    var body: some Scene {
 | 
			
		||||
        WindowGroup {
 | 
			
		||||
            Group {
 | 
			
		||||
                if viewModel.isLoading {
 | 
			
		||||
                    SplashScreenView()
 | 
			
		||||
                } else if viewModel.isLoggedIn {
 | 
			
		||||
                    MainView(viewModel: viewModel)
 | 
			
		||||
                } else {
 | 
			
		||||
                    LoginView(viewModel: viewModel)
 | 
			
		||||
            ZStack(alignment: .top) {
 | 
			
		||||
                Group {
 | 
			
		||||
                    if viewModel.isLoading {
 | 
			
		||||
                        SplashScreenView()
 | 
			
		||||
                    } else if viewModel.isLoggedIn {
 | 
			
		||||
                        MainView(viewModel: viewModel)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        LoginView(viewModel: viewModel)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if let banner = messageCenter.banner {
 | 
			
		||||
                    NewMessageBannerView(
 | 
			
		||||
                        banner: banner,
 | 
			
		||||
                        onOpen: { messageCenter.openCurrentChat() },
 | 
			
		||||
                        onDismiss: { messageCenter.dismissBanner() }
 | 
			
		||||
                    )
 | 
			
		||||
                    .padding(.horizontal, 16)
 | 
			
		||||
                    .padding(.top, 12)
 | 
			
		||||
                    .transition(.move(edge: .top).combined(with: .opacity))
 | 
			
		||||
                    .zIndex(1)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .animation(.spring(response: 0.35, dampingFraction: 0.8), value: messageCenter.banner != nil)
 | 
			
		||||
            .environmentObject(messageCenter)
 | 
			
		||||
            .environmentObject(themeManager)
 | 
			
		||||
            .preferredColorScheme(themeManager.theme.colorScheme)
 | 
			
		||||
            .environment(\.managedObjectContext, persistenceController.viewContext)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user