839 lines
31 KiB
Swift
839 lines
31 KiB
Swift
|
//
|
||
|
// ContactDetailsInterface.swift
|
||
|
// Monal
|
||
|
//
|
||
|
// Created by Jan on 22.10.21.
|
||
|
// Copyright © 2021 Monal.im. All rights reserved.
|
||
|
//
|
||
|
|
||
|
//see https://davedelong.com/blog/2018/01/19/simplifying-swift-framework-development/ for explanation of @_exported
|
||
|
@_exported import Foundation
|
||
|
@_exported import CocoaLumberjack
|
||
|
//@_exported import CocoaLumberjackSwift
|
||
|
@_exported import Logging
|
||
|
@_exported import SwiftUI
|
||
|
@_exported import monalxmpp
|
||
|
@_exported import Combine
|
||
|
import PhotosUI
|
||
|
import FLAnimatedImage
|
||
|
import OrderedCollections
|
||
|
import CropViewController
|
||
|
|
||
|
//see https://stackoverflow.com/a/62207329/3528174
|
||
|
//and https://www.hackingwithswift.com/forums/100-days-of-swiftui/extending-shapestyle-for-adding-colors-instead-of-extending-color/12324
|
||
|
public extension ShapeStyle where Self == Color {
|
||
|
static var interpolatedWindowBackground: Color { Color(UIColor { $0.userInterfaceStyle == .dark ? UIColor.systemBackground : UIColor.secondarySystemBackground }) }
|
||
|
static var background: Color { Color(UIColor.systemBackground) }
|
||
|
static var secondaryBackground: Color { Color(UIColor.secondarySystemBackground) }
|
||
|
static var tertiaryBackground: Color { Color(UIColor.tertiarySystemBackground) }
|
||
|
}
|
||
|
|
||
|
extension Binding {
|
||
|
func optionalMappedToBool<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
|
||
|
Binding<Bool>(
|
||
|
get: { self.wrappedValue != nil },
|
||
|
set: { newValue in
|
||
|
MLAssert(!newValue, "New value should never be true when writing to a binding created by optionalMappedToBool()")
|
||
|
self.wrappedValue = nil
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
extension Binding {
|
||
|
func bytecount(mappedTo: Double) -> Binding<Double> where Value == UInt {
|
||
|
Binding<Double>(
|
||
|
get: { Double(self.wrappedValue) / mappedTo },
|
||
|
set: { newValue in self.wrappedValue = UInt(newValue * mappedTo) }
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class SheetDismisserProtocol: ObservableObject {
|
||
|
weak var host: UIHostingController<AnyView>? = nil
|
||
|
func dismiss() {
|
||
|
host?.dismiss(animated: true)
|
||
|
}
|
||
|
func dismissWithoutAnimation() {
|
||
|
host?.dismiss(animated: false)
|
||
|
}
|
||
|
func replace<V>(with view: V) where V: View {
|
||
|
host?.rootView = AnyView(view)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getContactList(viewContact: (ObservableKVOWrapper<MLContact>?)) -> OrderedSet<ObservableKVOWrapper<MLContact>> {
|
||
|
if let contact = viewContact {
|
||
|
if(contact.isMuc) {
|
||
|
//this uses the account the muc belongs to and treats every other account to be remote,
|
||
|
//even when multiple accounts of the same monal instance are in the same group
|
||
|
var contactList : OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
|
||
|
for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: contact.contactJid, forAccountID: contact.accountID)) {
|
||
|
//jid can be participant_jid (if currently joined to muc) or member_jid (if not joined but member of muc)
|
||
|
guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else {
|
||
|
continue
|
||
|
}
|
||
|
contactList.append(ObservableKVOWrapper<MLContact>(MLContact.createContact(fromJid: jid, andAccountID: contact.accountID)))
|
||
|
}
|
||
|
return contactList
|
||
|
} else {
|
||
|
return [contact]
|
||
|
}
|
||
|
} else {
|
||
|
return []
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func promisifyMucAction(account: xmpp, mucJid: String, action: @escaping () throws -> Void) -> Promise<monal_void_block_t?> {
|
||
|
return Promise<monal_void_block_t?> { seal in
|
||
|
DispatchQueue.global(qos: .background).async {
|
||
|
account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary
|
||
|
let success : Bool = data["success"] as! Bool;
|
||
|
if !success {
|
||
|
seal.reject(data["errorMessage"] as? String ?? "Unknown error!")
|
||
|
} else {
|
||
|
if let callback = data["callback"] {
|
||
|
seal.fulfill(objcCast(callback) as monal_void_block_t)
|
||
|
} else {
|
||
|
seal.fulfill(nil)
|
||
|
}
|
||
|
}
|
||
|
}, forMuc:mucJid)
|
||
|
do {
|
||
|
try action()
|
||
|
} catch {
|
||
|
seal.reject(error)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func mucAffiliationToString(_ affiliation: String?) -> String {
|
||
|
if let affiliation = affiliation {
|
||
|
if affiliation == kMucAffiliationOwner {
|
||
|
return NSLocalizedString("Owner", comment:"muc affiliation")
|
||
|
} else if affiliation == kMucAffiliationAdmin {
|
||
|
return NSLocalizedString("Admin", comment:"muc affiliation")
|
||
|
} else if affiliation == kMucAffiliationMember {
|
||
|
return NSLocalizedString("Member", comment:"muc affiliation")
|
||
|
} else if affiliation == kMucAffiliationNone {
|
||
|
return NSLocalizedString("Participant", comment:"muc affiliation")
|
||
|
} else if affiliation == kMucAffiliationOutcast {
|
||
|
return NSLocalizedString("Blocked", comment:"muc affiliation")
|
||
|
} else if affiliation == kMucActionShowProfile {
|
||
|
return NSLocalizedString("Open contact details", comment:"muc members list")
|
||
|
} else if affiliation == kMucActionReinvite {
|
||
|
return NSLocalizedString("Invite again", comment:"muc invite")
|
||
|
}
|
||
|
}
|
||
|
return NSLocalizedString("<unknown>", comment:"muc affiliation")
|
||
|
}
|
||
|
|
||
|
func mucAffiliationToInt(_ affiliation: String?) -> Int {
|
||
|
if let affiliation = affiliation {
|
||
|
if affiliation == kMucAffiliationOwner {
|
||
|
return 1
|
||
|
} else if affiliation == kMucAffiliationAdmin {
|
||
|
return 2
|
||
|
} else if affiliation == kMucAffiliationMember {
|
||
|
return 3
|
||
|
} else if affiliation == kMucAffiliationNone {
|
||
|
return 4
|
||
|
} else if affiliation == kMucAffiliationOutcast {
|
||
|
return 5
|
||
|
} else if affiliation == kMucActionShowProfile {
|
||
|
return 1000
|
||
|
} else if affiliation == kMucActionReinvite {
|
||
|
return 100
|
||
|
}
|
||
|
}
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
struct CollapsedPickerStyle: ViewModifier {
|
||
|
let accessibilityLabel: Text
|
||
|
func body(content: Content) -> some View {
|
||
|
Menu {
|
||
|
content
|
||
|
} label: {
|
||
|
Button(action: { }) {
|
||
|
HStack {
|
||
|
Spacer().frame(width:8)
|
||
|
Image(systemName: "ellipsis")
|
||
|
.rotationEffect(.degrees(90))
|
||
|
.foregroundColor(.primary)
|
||
|
Spacer().frame(width:8)
|
||
|
}
|
||
|
.contentShape(Rectangle())
|
||
|
}
|
||
|
.frame(width: 24, height: 20)
|
||
|
.accessibilityLabel(accessibilityLabel)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
extension View {
|
||
|
func collapsedPickerStyle(accessibilityLabel label: Text) -> some View {
|
||
|
self.modifier(CollapsedPickerStyle(accessibilityLabel:label))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct TopRight<T: View>: ViewModifier {
|
||
|
let overlay: T
|
||
|
public func body(content: Content) -> some View {
|
||
|
ZStack(alignment: .topLeading) {
|
||
|
content
|
||
|
VStack {
|
||
|
HStack {
|
||
|
Spacer()
|
||
|
overlay
|
||
|
}
|
||
|
Spacer()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
extension View {
|
||
|
func addTopRight<T: View>(view overlayClosure: @autoclosure @escaping () -> T) -> some View {
|
||
|
modifier(TopRight(overlay:overlayClosure()))
|
||
|
}
|
||
|
func addTopRight(@ViewBuilder _ overlayClosure: @escaping () -> some View) -> some View {
|
||
|
modifier(TopRight(overlay:overlayClosure()))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct MonalProminentButtonStyle: ButtonStyle {
|
||
|
@Environment(\.isEnabled) var isEnabled
|
||
|
func makeBody(configuration: Configuration) -> some View {
|
||
|
configuration.label
|
||
|
.padding(10)
|
||
|
.background(Color.accentColor)
|
||
|
.foregroundColor(Color(UIColor.systemBackground))
|
||
|
.fontWeight(isEnabled ? .bold : .regular)
|
||
|
.cornerRadius(10)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@ViewBuilder
|
||
|
func buildNotificationStateLabel(_ description: Text, isWorking: Bool) -> some View {
|
||
|
if(isWorking == true) {
|
||
|
Label(title: {
|
||
|
description
|
||
|
}, icon: {
|
||
|
Image(systemName: "checkmark.seal")
|
||
|
.foregroundColor(.green)
|
||
|
})
|
||
|
} else {
|
||
|
Label(title: {
|
||
|
description
|
||
|
}, icon: {
|
||
|
Image(systemName: "xmark.seal")
|
||
|
.foregroundColor(.red)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//see https://github.com/CH3COOH/TOCropViewController/blob/issue/421/Swift/CropViewControllerSwiftUIExample/ImageCropView.swift
|
||
|
public struct ImageCropView: UIViewControllerRepresentable {
|
||
|
private let configureBlock: (CropViewController) -> Void
|
||
|
private let originalImage: UIImage
|
||
|
private let onCanceled: () -> Void
|
||
|
private let onImageCropped: (UIImage,CGRect,Int) -> Void
|
||
|
|
||
|
@Environment(\.presentationMode) private var presentationMode
|
||
|
|
||
|
public init(originalImage: UIImage, configureBlock: @escaping (CropViewController) -> Void, onCanceled: @escaping () -> Void, success onImageCropped: @escaping (UIImage,CGRect,Int) -> Void) {
|
||
|
self.originalImage = originalImage
|
||
|
self.configureBlock = configureBlock
|
||
|
self.onCanceled = onCanceled
|
||
|
self.onImageCropped = onImageCropped
|
||
|
}
|
||
|
|
||
|
public func makeUIViewController(context: Context) -> CropViewController {
|
||
|
let cropController = CropViewController(image: originalImage)
|
||
|
cropController.delegate = context.coordinator
|
||
|
configureBlock(cropController)
|
||
|
return cropController
|
||
|
}
|
||
|
|
||
|
public func updateUIViewController(_ uiViewController: CropViewController, context: Context) {
|
||
|
}
|
||
|
|
||
|
public func makeCoordinator() -> Coordinator {
|
||
|
Coordinator(
|
||
|
onDismiss: { self.presentationMode.wrappedValue.dismiss() },
|
||
|
onCanceled: self.onCanceled,
|
||
|
onImageCropped: self.onImageCropped
|
||
|
)
|
||
|
}
|
||
|
|
||
|
final public class Coordinator: NSObject, CropViewControllerDelegate {
|
||
|
private let onDismiss: () -> Void
|
||
|
private let onImageCropped: (UIImage,CGRect,Int) -> Void
|
||
|
private let onCanceled: () -> Void
|
||
|
|
||
|
init(onDismiss: @escaping () -> Void, onCanceled: @escaping () -> Void, onImageCropped: @escaping (UIImage,CGRect,Int) -> Void) {
|
||
|
self.onDismiss = onDismiss
|
||
|
self.onImageCropped = onImageCropped
|
||
|
self.onCanceled = onCanceled
|
||
|
}
|
||
|
|
||
|
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
||
|
self.onImageCropped(image, cropRect, angle)
|
||
|
self.onDismiss()
|
||
|
}
|
||
|
|
||
|
public func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
||
|
self.onImageCropped(image, cropRect, angle)
|
||
|
self.onDismiss()
|
||
|
}
|
||
|
|
||
|
public func cropViewController(_ cropViewController: CropViewController, didFinishCancelled cancelled: Bool) {
|
||
|
self.onCanceled()
|
||
|
self.onDismiss()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift
|
||
|
struct GIFViewer: UIViewRepresentable {
|
||
|
typealias UIViewType = FLAnimatedImageView
|
||
|
@Binding var data: Data
|
||
|
|
||
|
func makeUIView(context: Context) -> FLAnimatedImageView {
|
||
|
let imageView = FLAnimatedImageView(frame:.zero)
|
||
|
let animatedImage = FLAnimatedImage(animatedGIFData:data)
|
||
|
imageView.animatedImage = animatedImage
|
||
|
return imageView
|
||
|
}
|
||
|
|
||
|
func updateUIView(_ imageView: FLAnimatedImageView, context: Context) {
|
||
|
let animatedImage = FLAnimatedImage(animatedGIFData:data)
|
||
|
imageView.animatedImage = animatedImage
|
||
|
}
|
||
|
|
||
|
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextField, context: Context) -> CGSize? {
|
||
|
guard
|
||
|
let width = proposal.width,
|
||
|
let height = proposal.height
|
||
|
else { return nil }
|
||
|
return CGSize(width: width, height: height)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//see https://www.hackingwithswift.com/books/ios-swiftui/importing-an-image-into-swiftui-using-phpickerviewcontroller
|
||
|
struct ImagePicker: UIViewControllerRepresentable {
|
||
|
@Binding var image: UIImage?
|
||
|
|
||
|
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||
|
var config = PHPickerConfiguration()
|
||
|
config.filter = .images
|
||
|
let picker = PHPickerViewController(configuration: config)
|
||
|
picker.delegate = context.coordinator
|
||
|
return picker
|
||
|
}
|
||
|
|
||
|
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
|
||
|
|
||
|
}
|
||
|
|
||
|
func makeCoordinator() -> Coordinator {
|
||
|
Coordinator(self)
|
||
|
}
|
||
|
|
||
|
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||
|
let parent: ImagePicker
|
||
|
|
||
|
init(_ parent: ImagePicker) {
|
||
|
self.parent = parent
|
||
|
}
|
||
|
|
||
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
|
picker.dismiss(animated: true)
|
||
|
|
||
|
guard let provider = results.first?.itemProvider else { return }
|
||
|
|
||
|
if provider.canLoadObject(ofClass: UIImage.self) {
|
||
|
provider.loadObject(ofClass: UIImage.self) { image, _ in
|
||
|
self.parent.image = image as? UIImage
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//see https://stackoverflow.com/a/60452526
|
||
|
class DocumentPickerViewController: UIDocumentPickerViewController {
|
||
|
private let onDismiss: () -> Void
|
||
|
private let onPick: (URL) -> ()
|
||
|
|
||
|
init(supportedTypes: [UTType], onPick: @escaping (URL) -> Void, onDismiss: @escaping () -> Void) {
|
||
|
self.onDismiss = onDismiss
|
||
|
self.onPick = onPick
|
||
|
|
||
|
super.init(forOpeningContentTypes:supportedTypes, asCopy:true)
|
||
|
|
||
|
allowsMultipleSelection = false
|
||
|
delegate = self
|
||
|
}
|
||
|
|
||
|
required init?(coder: NSCoder) {
|
||
|
unreachable("init(coder:) has not been implemented")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extension DocumentPickerViewController: UIDocumentPickerDelegate {
|
||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||
|
onPick(urls.first!)
|
||
|
}
|
||
|
|
||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||
|
onDismiss()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct ActivityViewController: UIViewControllerRepresentable {
|
||
|
var activityItems: [Any]
|
||
|
var applicationActivities: [UIActivity]? = nil
|
||
|
|
||
|
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
|
||
|
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
|
||
|
return controller
|
||
|
}
|
||
|
|
||
|
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clear button for text fields, see https://stackoverflow.com/a/58896723/3528174
|
||
|
struct ClearButton: ViewModifier {
|
||
|
let isEditing: Bool
|
||
|
@Binding var text: String
|
||
|
|
||
|
public func body(content: Content) -> some View {
|
||
|
HStack {
|
||
|
content
|
||
|
.accessibilitySortPriority(2)
|
||
|
|
||
|
if isEditing, !text.isEmpty {
|
||
|
Button {
|
||
|
self.text = ""
|
||
|
} label: {
|
||
|
Image(systemName: "xmark.circle.fill")
|
||
|
.foregroundColor(Color(UIColor.tertiaryLabel))
|
||
|
.accessibilityLabel(Text("Clear text"))
|
||
|
}
|
||
|
.padding(.trailing, 8)
|
||
|
.accessibilitySortPriority(1)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
//this extension contains the easy-access view modifier
|
||
|
extension View {
|
||
|
/// Puts the view in an HStack and adds a clear button to the right when the text is not empty.
|
||
|
func addClearButton(isEditing: Bool, text: Binding<String>) -> some View {
|
||
|
modifier(ClearButton(isEditing: isEditing, text:text))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//see https://exyte.com/blog/swiftui-tutorial-popupview-library
|
||
|
struct FrameGetterModifier: ViewModifier {
|
||
|
@Binding var frame: CGRect
|
||
|
func body(content: Content) -> some View {
|
||
|
content
|
||
|
.background(
|
||
|
GeometryReader { proxy -> AnyView in
|
||
|
let rect = proxy.frame(in: .global)
|
||
|
// This avoids an infinite layout loop
|
||
|
if rect.integral != self.frame.integral {
|
||
|
DispatchQueue.main.async {
|
||
|
self.frame = rect
|
||
|
}
|
||
|
}
|
||
|
return AnyView(EmptyView())
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
extension View {
|
||
|
func frameGetter(_ frame: Binding<CGRect>) -> some View {
|
||
|
modifier(FrameGetterModifier(frame: frame))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct NumberlessBadge: View {
|
||
|
@Binding var notificationCount: Int
|
||
|
private let size: Int
|
||
|
private let inset: Int
|
||
|
|
||
|
var badgeSize: CGFloat {
|
||
|
CGFloat(integerLiteral: size)
|
||
|
}
|
||
|
|
||
|
var edgeInset: CGFloat {
|
||
|
CGFloat(integerLiteral: inset)
|
||
|
}
|
||
|
|
||
|
init(_ notificationCount: Binding<Int>, size: Int = 7, inset: Int = 1) {
|
||
|
self._notificationCount = notificationCount
|
||
|
self.size = size
|
||
|
self.inset = inset
|
||
|
}
|
||
|
|
||
|
var body: some View {
|
||
|
HStack {
|
||
|
Spacer()
|
||
|
VStack {
|
||
|
if notificationCount > 0 {
|
||
|
Image(systemName: "circle.fill")
|
||
|
.resizable()
|
||
|
.frame(width: badgeSize, height: badgeSize)
|
||
|
.tint(.red)
|
||
|
.padding(.trailing, edgeInset)
|
||
|
.padding(.top, edgeInset)
|
||
|
}
|
||
|
Spacer()
|
||
|
}
|
||
|
}
|
||
|
.animation(.default, value: notificationCount)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// //see https://stackoverflow.com/a/68291983
|
||
|
// struct OverflowContentViewModifier: ViewModifier {
|
||
|
// @State private var contentOverflow: Bool = false
|
||
|
// func body(content: Content) -> some View {
|
||
|
// GeometryReader { geometry in
|
||
|
// content
|
||
|
// .background(
|
||
|
// GeometryReader { contentGeometry in
|
||
|
// Color.clear.onAppear {
|
||
|
// contentOverflow = contentGeometry.size.height > geometry.size.height
|
||
|
// }
|
||
|
// }
|
||
|
// )
|
||
|
// .wrappedInScrollView(when: contentOverflow)
|
||
|
// }
|
||
|
// }
|
||
|
// }
|
||
|
//
|
||
|
// extension View {
|
||
|
// @ViewBuilder
|
||
|
// func wrappedInScrollView(when condition: Bool) -> some View {
|
||
|
// if condition {
|
||
|
// ScrollView {
|
||
|
// self
|
||
|
// }
|
||
|
// } else {
|
||
|
// self
|
||
|
// }
|
||
|
// }
|
||
|
// }
|
||
|
//
|
||
|
// extension View {
|
||
|
// func scrollOnOverflow() -> some View {
|
||
|
// modifier(OverflowContentViewModifier())
|
||
|
// }
|
||
|
// }
|
||
|
|
||
|
// lazy loading of views (e.g. when used inside a NavigationLink) with the additional ability to use a closure to modify/wrap them
|
||
|
// see https://stackoverflow.com/a/61234030/3528174
|
||
|
struct LazyClosureView<Content: View>: View {
|
||
|
let build: () -> Content
|
||
|
init(_ build: @autoclosure @escaping () -> Content) {
|
||
|
self.build = build
|
||
|
}
|
||
|
init(withClosure build: @escaping () -> Content) {
|
||
|
self.build = build
|
||
|
}
|
||
|
var body: Content {
|
||
|
build()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// use this to wrap a view into NavigationStack, if it should be the outermost swiftui view of a new view stack
|
||
|
struct AddTopLevelNavigation<Content: View>: View {
|
||
|
@Environment(\.presentationMode) private var presentationMode
|
||
|
@StateObject private var sizeClass: ObservableKVOWrapper<SizeClassWrapper>
|
||
|
let build: () -> Content
|
||
|
let delegate: SheetDismisserProtocol?
|
||
|
|
||
|
init(withDelegate delegate: SheetDismisserProtocol?, to build: @autoclosure @escaping () -> Content) {
|
||
|
self.build = build
|
||
|
self.delegate = delegate
|
||
|
|
||
|
let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats!
|
||
|
self._sizeClass = StateObject(wrappedValue: ObservableKVOWrapper<SizeClassWrapper>(activeChats.sizeClass))
|
||
|
}
|
||
|
|
||
|
var body: some View {
|
||
|
NavigationStack {
|
||
|
build()
|
||
|
.navigationBarTitleDisplayMode(.automatic)
|
||
|
.navigationBarBackButtonHidden(true) // will not be shown because swiftui does not know we navigated here from UIKit
|
||
|
.toolbar {
|
||
|
#if targetEnvironment(macCatalyst)
|
||
|
let shouldDisplayBackButton = true
|
||
|
#else
|
||
|
let shouldDisplayBackButton = UIUserInterfaceSizeClass(rawValue: sizeClass.horizontal) == .compact
|
||
|
#endif
|
||
|
if shouldDisplayBackButton {
|
||
|
ToolbarItem(placement: .topBarLeading) {
|
||
|
Button(action : {
|
||
|
//NOTE: since we can get opened from objc, we still need to support our SheetDismisserProtocol
|
||
|
if let delegate = self.delegate {
|
||
|
delegate.dismiss()
|
||
|
} else {
|
||
|
self.presentationMode.wrappedValue.dismiss()
|
||
|
}
|
||
|
}) {
|
||
|
Image(systemName: "arrow.backward")
|
||
|
}
|
||
|
.keyboardShortcut(.escape, modifiers: [])
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO: fix those workarounds as soon as we have no storyboards anymore
|
||
|
struct UIKitWorkaround<Content: View>: View {
|
||
|
let build: () -> Content
|
||
|
init(_ build: @autoclosure @escaping () -> Content) {
|
||
|
self.build = build
|
||
|
}
|
||
|
init(withClosure build: @escaping () -> Content) {
|
||
|
self.build = build
|
||
|
}
|
||
|
var body: some View {
|
||
|
if(UIDevice.current.userInterfaceIdiom == .phone) {
|
||
|
build().navigationBarTitleDisplayMode(.inline)
|
||
|
} else {
|
||
|
#if targetEnvironment(macCatalyst)
|
||
|
build().navigationBarTitleDisplayMode(.inline)
|
||
|
#else
|
||
|
NavigationStack {
|
||
|
build()
|
||
|
.navigationBarTitleDisplayMode(.automatic)
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// properties for use in Alert
|
||
|
struct AlertPrompt {
|
||
|
var title: Text = Text("")
|
||
|
var message: Text = Text("")
|
||
|
var dismissLabel: Text = Text("Close")
|
||
|
var dismissCallback: monal_void_block_t? = nil
|
||
|
}
|
||
|
|
||
|
// properties for use in actionSheet
|
||
|
struct ConfirmationPrompt {
|
||
|
var title: Text = Text("")
|
||
|
var message: Text = Text("")
|
||
|
var buttons: [ActionSheet.Button] = []
|
||
|
}
|
||
|
|
||
|
extension View {
|
||
|
/// Applies the given transform.
|
||
|
///
|
||
|
/// Useful for availability branching on view modifiers. Do not branch with any properties that may change during runtime as this will cause errors.
|
||
|
/// - Parameters:
|
||
|
/// - transform: The transform to apply to the source `View`.
|
||
|
/// - Returns: The view transformed by the transform.
|
||
|
func applyClosure<Content: View>(@ViewBuilder _ transform: (Self) -> Content) -> some View {
|
||
|
transform(self)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public extension UIViewController {
|
||
|
private struct AssociatedKeys {
|
||
|
static var DisposeCallbackKey = "ml_disposeCallbackKey"
|
||
|
}
|
||
|
|
||
|
private class DisposeCallback : NSObject {
|
||
|
let callback: monal_void_block_t
|
||
|
|
||
|
init(withCallback callback: @escaping monal_void_block_t) {
|
||
|
self.callback = callback
|
||
|
}
|
||
|
|
||
|
deinit {
|
||
|
self.callback()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
var ml_disposeCallback: monal_void_block_t {
|
||
|
get {
|
||
|
return withUnsafePointer(to: &AssociatedKeys.DisposeCallbackKey) { pointer in
|
||
|
if let callback = (objc_getAssociatedObject(self, pointer) as? DisposeCallback)?.callback {
|
||
|
return callback
|
||
|
}
|
||
|
unreachable("You can't get what you did not set!")
|
||
|
}
|
||
|
}
|
||
|
set {
|
||
|
withUnsafePointer(to: &AssociatedKeys.DisposeCallbackKey) { pointer in
|
||
|
objc_setAssociatedObject(self, pointer, DisposeCallback(withCallback: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Interfaces between ObjectiveC/Storyboards and SwiftUI
|
||
|
@objc
|
||
|
class SwiftuiInterface : NSObject {
|
||
|
@objc(makeAccountPickerForContacts:andCallType:)
|
||
|
func makeAccountPicker(for contacts: [MLContact], and callType: UInt) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:AccountPicker(contacts:contacts, callType:MLCallType(rawValue: callType)!)))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc(makeCallScreenForCall:)
|
||
|
func makeCallScreen(for call: MLCall) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AVCallUI(delegate:delegate, call:call))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeContactDetails(_ contact: MLContact) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:ContactDetails(delegate:delegate, contact:ObservableKVOWrapper<MLContact>(contact))))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc(makeImageViewerForCurrentItem:allItems:)
|
||
|
func makeImageViewerFor(currentItem:[String:AnyObject], allItems: [[String:AnyObject]]) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(MediaItemSwipeView(currentItem: currentItem, allItems: allItems))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeOwnOmemoKeyView(_ ownContact: MLContact?) -> UIViewController {
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
if(ownContact == nil) {
|
||
|
host.rootView = AnyView(UIKitWorkaround(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: nil))))
|
||
|
} else {
|
||
|
host.rootView = AnyView(UIKitWorkaround(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: ObservableKVOWrapper<MLContact>(ownContact!)))))
|
||
|
}
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeAccountRegistration(_ registerData: [String:AnyObject]?) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
#if IS_QUICKSY
|
||
|
host.rootView = AnyView(Quicksy_RegisterAccount(delegate:delegate))
|
||
|
#else
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:RegisterAccount(delegate:delegate, registerData:registerData)))
|
||
|
#endif
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeServerDetailsView(for xmppAccount: xmpp) -> UIViewController {
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
host.rootView = AnyView(ServerDetails(xmppAccount: xmppAccount))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeBlockedUsersView(for xmppAccount: xmpp) -> UIViewController {
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
host.rootView = AnyView(BlockedUsers(xmppAccount: xmppAccount))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makePasswordMigration(_ needingMigration: [[String:NSObject]]) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:PasswordMigration(delegate:delegate, needingMigration:needingMigration)))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc(makeAddContactViewWithDismisser:)
|
||
|
func makeAddContactView(dismisser: @escaping (MLContact) -> ()) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: AddContactMenu(delegate: delegate, dismissWithNewContact: dismisser)))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeAddContactView(forJid jid:String, preauthToken: String?, prefillAccount: xmpp?, andOmemoFingerprints omemoFingerprints: [NSNumber:Data]?, withDismisser dismisser: @escaping (MLContact) -> ()) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: AddContactMenu(delegate: delegate, dismissWithNewContact: dismisser, prefillJid: jid, preauthToken: preauthToken, prefillAccount: prefillAccount, omemoFingerprints: omemoFingerprints)))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc(makeContactsViewWithDismisser:onButton:)
|
||
|
func makeContactsView(dismisser: @escaping (MLContact) -> (), button: UIBarButtonItem) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
let host = UIHostingController(rootView: AnyView(EmptyView()))
|
||
|
let contactsView = ContactsView(contacts: Contacts(), delegate: delegate, dismissWithContact: dismisser)
|
||
|
delegate.host = host
|
||
|
host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: contactsView))
|
||
|
host.modalPresentationStyle = .popover
|
||
|
host.popoverPresentationController?.sourceItem = button
|
||
|
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: 400, height: 600))
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
@objc
|
||
|
func makeView(name: String) -> UIViewController {
|
||
|
let delegate = SheetDismisserProtocol()
|
||
|
var host: UIHostingController<AnyView>? = nil
|
||
|
//let host = UIHostingController(rootView:AnyView(EmptyView()))
|
||
|
switch(name) { // TODO names are currently taken from the segue identifier, an enum would be nice once everything is ported to SwiftUI
|
||
|
case "DebugView":
|
||
|
host = UIHostingController(rootView:AnyView(UIKitWorkaround(DebugView())))
|
||
|
case "WelcomeLogIn":
|
||
|
host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate:delegate, to:WelcomeLogIn(delegate:delegate))))
|
||
|
case "LogIn":
|
||
|
host = UIHostingController(rootView:AnyView(UIKitWorkaround(WelcomeLogIn(delegate:delegate))))
|
||
|
case "AdvancedLogIn":
|
||
|
host = UIHostingController(rootView:AnyView(UIKitWorkaround(WelcomeLogIn(advancedMode: true, delegate: delegate))))
|
||
|
case "ChatPlaceholder":
|
||
|
host = UIHostingController(rootView:AnyView(ChatPlaceholder()))
|
||
|
case "GeneralSettings" :
|
||
|
host = UIHostingController(rootView:AnyView(UIKitWorkaround(GeneralSettings())))
|
||
|
case "ActiveChatsGeneralSettings":
|
||
|
host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: GeneralSettings())))
|
||
|
case "ActiveChatsNotificationSettings":
|
||
|
host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())))
|
||
|
case "OnboardingView":
|
||
|
host = UIHostingController(rootView:AnyView(createOnboardingView(delegate:delegate)))
|
||
|
|
||
|
default:
|
||
|
unreachable()
|
||
|
}
|
||
|
delegate.host = host!
|
||
|
return host!
|
||
|
}
|
||
|
}
|