Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite the Settings view in SwiftUI #1317

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions Monal/Classes/AccountList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// AccountList.swift
// Monal
//
// Created by lissine on 30/11/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//

private class Account: Identifiable {
let accountID: NSNumber
let username: String
let domain: String
var enabled: Bool
var jid: String {
return username+"@"+domain
}
var connected: Bool {
return MLXMPPManager.sharedInstance().isAccount(forIdConnected: self.accountID)
}
var connectedTime: Date {
return MLXMPPManager.sharedInstance().connectedTime(for: self.accountID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use connectedTime property of our xmpp class, wrapped by a ObservableKVOWrapper.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the connectedTimeFor: method of MLXMPPManager can even be removed afterwards (no other code is using it if I don't miss something).

}
var avatar: UIImage {
return MLImageManager.sharedInstance().getIconFor(MLContact.createContact(fromJid: self.jid, andAccountID: self.accountID)) ?? UIImage(named: "noicon")!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use the avatar property of MLContact to get auto-updates (wrap the MLContact in ObservableKVOWrapper.

}
// Conformance to the Identifiable protocol
var id: NSNumber {
return accountID
}

init(account: [String: Any]) {
self.accountID = account["account_id"] as! NSNumber
self.username = account["username"] as! String
self.domain = account["domain"] as! String
self.enabled = account["enabled"] as! Bool
}
}

private struct AccountEntry: View {
let account: Account
let uptimeFormatter: DateFormatter

init(account: Account) {
self.account = account
self.uptimeFormatter = DateFormatter()
uptimeFormatter.dateStyle = .short
uptimeFormatter.timeStyle = .short
uptimeFormatter.doesRelativeDateFormatting = true
}

var connectionStatusString: String {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the accountState of our xmpp object (in a ObservableKVOWrapper) and a switch statement to stringify the values of the xmppState enum.

if account.enabled && account.connected {
return String(format: NSLocalizedString("Connected since: %@", comment: ""), uptimeFormatter.string(from: account.connectedTime))
} else if account.enabled && !account.connected {
return NSLocalizedString("Connecting...", comment: "")
} else {
return NSLocalizedString("Account disabled", comment: "")
}
}

var body: some View {
HStack {
Image(uiImage: account.avatar)
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.padding(.leading, -3)
.padding(.trailing, 4)
VStack {
Text(account.jid)
.frame(maxWidth: .infinity, alignment: .leading)

Text(self.connectionStatusString)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: account.enabled ? (account.connected ? "checkmark.circle.fill" : "checkmark.circle") : "circle")
.foregroundStyle(Color.accentColor)
}
}
}

struct AccountList: View {
@State private var accounts: [Account] = getAccountList()

private static func getAccountList() -> [Account] {
return (DataLayer.sharedInstance().accountList() as! [[String: Any]]).map { Account(account: $0) }
}
private func refreshAccountList() {
self.accounts = AccountList.getAccountList()
}

var body: some View {
List {
ForEach(accounts) { account in
NavigationLink {
LazyClosureView(EmptyView())
} label: {
AccountEntry(account: account)
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalAccountStatusChanged)).receive(on: RunLoop.main)) { notification in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't be needed anymore, once the xmpp class is a proper data model class used in conjunction with the ObservableKVOWrapper.

DispatchQueue.main.async {
DDLogVerbose("Refreshing the account list in the Settings view")
refreshAccountList()
}
}
}
}
11 changes: 11 additions & 0 deletions Monal/Classes/ActiveChatsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ -(void) configureComposeButton
[self.composeButton setAccessibilityTraits:UIAccessibilityTraitButton];
}

-(void) configureSettingsButton
{
UIImageView* image = [[UIImageView alloc] initWithImage:[[UIImage systemImageNamed:@"gearshape.fill"] imageWithTintColor:UIColor.tintColor]];
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showSettings)];
[image addGestureRecognizer:tapRecognizer];
self.settingsButton.customView = image;
[self.settingsButton setIsAccessibilityElement:YES];
[self.settingsButton setAccessibilityLabel:NSLocalizedString(@"Open the settings", @"")];
[self.settingsButton setAccessibilityTraits:UIAccessibilityTraitButton];
}

-(void) viewDidLoad
{
DDLogDebug(@"active chats view did load");
Expand Down
1 change: 1 addition & 0 deletions Monal/Classes/Monal-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
#import "ActiveChatsViewController.h"
#import "MLMucProcessor.h"
#import "SCRAM.h"
#import "MLSoundsTableViewController.h"
42 changes: 4 additions & 38 deletions Monal/Classes/RegisterAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,8 @@
// Copyright © 2022 Monal.im. All rights reserved.
//

import SafariServices
import WebKit
import FrameUp

struct WebView: UIViewRepresentable {
var url: URL

func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}

func updateUIView(_ webView: WKWebView, context: Context) {
var request = URLRequest(url: url)
if HelperTools.defaultsDB().bool(forKey:"useDnssecForAllConnections") {
request.requiresDNSSECValidation = true;
}
webView.load(request)
}
}

struct RegisterAccount: View {
static private let xmppFaultyPattern = ".+\\..{2,}$"
static private let credFaultyPattern = ".*@.*"
Expand Down Expand Up @@ -57,7 +39,6 @@ struct RegisterAccount: View {
@StateObject private var overlay = LoadingOverlayState()
@State private var currentTimeout : DispatchTime? = nil

@State private var showWebView = false
@State private var errorObserverEnabled = false

var delegate: SheetDismisserProtocol
Expand Down Expand Up @@ -428,29 +409,14 @@ struct RegisterAccount: View {
.padding(.vertical, 8)

if(selectedServerIndex != 0) {
Button (action: {
showWebView.toggle()
}){
NavigationLink(destination: LazyClosureView(WebView(url: termsSiteForCurrentLanguage()))) {
Text("Terms of use for \(RegisterAccount.XMPPServer[$selectedServerIndex.wrappedValue]["XMPPServer"]!)")
.font(.system(size: 10))
}
.frame(maxWidth: .infinity)
.sheet(isPresented: $showWebView) {
NavigationStack {
WebView(url: termsSiteForCurrentLanguage())
.navigationBarTitle(Text("Terms of \(RegisterAccount.XMPPServer[$selectedServerIndex.wrappedValue]["XMPPServer"]!)"), displayMode: .inline)
.toolbar(content: {
ToolbarItem(placement: .bottomBar) {
Button (action: {
showWebView.toggle()
}){
Text("Close")
}
}
})
}
.foregroundStyle(Color.accentColor)
.frame(maxWidth: .infinity, alignment: .center)
}
}

}
.textFieldStyle(.roundedBorder)
}
Expand Down
108 changes: 108 additions & 0 deletions Monal/Classes/Settings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// Settings.swift
// Monal
//
// Created by lissine on 30/11/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//

struct Settings: View {
@State private var tappedVersionInfo = 0
@State private var showDebugEntry = HelperTools.defaultsDB().bool(forKey: "showLogInSettings")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a new class with @defaultsDB annotated properties to hold and autoupdate these settings, see the implementation of GeneralSettingsDefaultsDB in GeneralSettings.swift

var delegate: SheetDismisserProtocol
var body: some View {
Form {
Section(header: Text("")) {
AccountList()
#if !IS_QUICKSY
NavigationLink(destination: LazyClosureView(WelcomeLogIn(delegate: delegate))) {
Text("Add Account")
}
NavigationLink(destination: LazyClosureView(WelcomeLogIn(advancedMode: true, delegate: delegate))) {
Text("Add Account (advanced)")
}
#endif
}
Section(header: Text("App")) {
NavigationLink(destination: LazyClosureView(GeneralSettings())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general settings should now be embedded in this settings rather than being a submenu (I only used a submenu because the settings weren't yet ported to swiftui).

Text("General Settings")
}
NavigationLink(destination: LazyClosureView(SwiftuiInterface.SoundsSettings())) {
Text("Sounds")
}
}
Section(header: Text("Support")) {
Link("Email Support",
destination: URL(string: "mailto:info@monal-im.org")!)

NavigationLink(destination: LazyClosureView(WebView(url: URL(string: "https://github.com/monal-im/Monal/issues")!))) {
Text("Submit A Bug")
}

NavigationLink(destination: LazyClosureView(WebView(url: URL(string: "https://github.com/monal-im/Monal/wiki/FAQ---Frequently-Asked-Questions")!))) {
Text("Frequently Asked Questions")
}
}
.tint(Color.primary)
Section(header: Text("About")) {
#if TARGET_OS_MACCATALYST
Link("Rate Monal",
destination: URL(string: "itms-apps://itunes.apple.com/app/1637078500")!)
#elseif IS_QUICKSY
Link("Rate Quicksy",
destination: URL(string: "itms-apps://itunes.apple.com/app/6538727270")!)
#else
Link("Rate Monal",
destination: URL(string: "itms-apps://itunes.apple.com/app/317711500")!)
#endif

let path = Bundle.main.path(forResource: "opensource", ofType: "html")
NavigationLink(destination: LazyClosureView(WebView(url: URL(fileURLWithPath: path!)))) {
Text("Open Source")
}

NavigationLink(destination: LazyClosureView(WebView(url: URL(string: "https://monal-im.org/privacy")!))) {
Text("Privacy")
}

NavigationLink(destination: LazyClosureView(WebView(url: URL(string: "https://monal-im.org/about")!))) {
Text("About")
}
#if DEBUG
NavigationLink(destination: LazyClosureView(DebugView())) {
Text("Debug")
}
#else
if showDebugEntry {
NavigationLink(destination: LazyClosureView(DebugView())) {
Text("Debug")
}
}
#endif

// Version button
Button(action: {
// Copy the version string to the clipboard
UIPasteboard.general.setValue(HelperTools.appBuildVersionInfo(for: MLVersionType.IQ), forPasteboardType: UTType.utf8PlainText.identifier)
#if !DEBUG
tappedVersionInfo += 1
if tappedVersionInfo > 16 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if tappedVersionInfo > 16 {
if tappedVersionInfo >= 16 {

HelperTools.defaultsDB().set(true, forKey: "showLogInSettings")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this, if you use the @defaultsDB annotation (see comment above).

// Redraw the view
showDebugEntry = true
}
#endif
}, label: {
HStack {
Text("Version")
Spacer()
Text(HelperTools.appBuildVersionInfo(for: MLVersionType.IQ))
}
})
}
.tint(Color.primary)
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
}
}
28 changes: 23 additions & 5 deletions Monal/Classes/SwiftuiHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,16 +556,12 @@ struct LazyClosureView<Content: View>: View {
// 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 {
Expand All @@ -574,10 +570,13 @@ struct AddTopLevelNavigation<Content: View>: View {
.navigationBarTitleDisplayMode(.automatic)
.navigationBarBackButtonHidden(true) // will not be shown because swiftui does not know we navigated here from UIKit
.toolbar {
// The macCatalyst build is currently using the iPad UI idiom
// But we want to display the back button on mac
#if targetEnvironment(macCatalyst)
let shouldDisplayBackButton = true
#else
let shouldDisplayBackButton = UIUserInterfaceSizeClass(rawValue: sizeClass.horizontal) == .compact
// Only hide the back button on iPads
let shouldDisplayBackButton = UIDevice.current.userInterfaceIdiom != .pad
#endif
if shouldDisplayBackButton {
ToolbarItem(placement: .topBarLeading) {
Expand Down Expand Up @@ -811,6 +810,15 @@ class SwiftuiInterface : NSObject {
return host
}

@objc
func makeSettingsView() -> UIViewController {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since makeSettingsView does not need any special argument, you should add the settings view to our makeView function below.

let delegate = SheetDismisserProtocol()
let host = UIHostingController(rootView:AnyView(EmptyView()))
delegate.host = host
host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: Settings(delegate: delegate)))
return host
}

@objc
func makeView(name: String) -> UIViewController {
let delegate = SheetDismisserProtocol()
Expand Down Expand Up @@ -842,4 +850,14 @@ class SwiftuiInterface : NSObject {
delegate.host = host!
return host!
}

struct SoundsSettings: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MLSoundsTableViewController {
let viewController = MLSoundsTableViewController()
viewController.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "soundCell")
return viewController
}
func updateUIViewController(_ uiViewController: MLSoundsTableViewController, context: Context) {
}
}
}
Loading