-
-
Notifications
You must be signed in to change notification settings - Fork 109
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
base: develop
Are you sure you want to change the base?
Changes from all commits
f361726
5704ee5
fad973f
a425c52
60839b8
f7a41a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} | ||
var avatar: UIImage { | ||
return MLImageManager.sharedInstance().getIconFor(MLContact.createContact(fromJid: self.jid, andAccountID: self.accountID)) ?? UIImage(named: "noicon")! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should use the |
||
} | ||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use the |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't be needed anymore, once the |
||
DispatchQueue.main.async { | ||
DDLogVerbose("Refreshing the account list in the Settings view") | ||
refreshAccountList() | ||
} | ||
} | ||
} | ||
} |
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") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a new class with |
||||||
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())) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
HelperTools.defaultsDB().set(true, forKey: "showLogInSettings") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need this, if you use the |
||||||
// Redraw the view | ||||||
showDebugEntry = true | ||||||
} | ||||||
#endif | ||||||
}, label: { | ||||||
HStack { | ||||||
Text("Version") | ||||||
Spacer() | ||||||
Text(HelperTools.appBuildVersionInfo(for: MLVersionType.IQ)) | ||||||
} | ||||||
}) | ||||||
} | ||||||
.tint(Color.primary) | ||||||
} | ||||||
.navigationTitle("Settings") | ||||||
.navigationBarTitleDisplayMode(.inline) | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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) { | ||
|
@@ -811,6 +810,15 @@ class SwiftuiInterface : NSObject { | |
return host | ||
} | ||
|
||
@objc | ||
func makeSettingsView() -> UIViewController { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since |
||
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() | ||
|
@@ -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) { | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use
connectedTime
property of ourxmpp
class, wrapped by aObservableKVOWrapper
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the
connectedTimeFor:
method ofMLXMPPManager
can even be removed afterwards (no other code is using it if I don't miss something).