You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

342 lines
14 KiB

// Copyright © 2015 Abhishek Banthia
import Cocoa
import CoreLoggerKit
import CoreModelKit
import FirebaseDatabase
protocol AppFeedbackWindowControllerDelegate: AnyObject {
func appFeedbackWindowWillClose()
func appFeedbackWindoEntryPoint() -> String
}
extension NSNib.Name {
static let appFeedbackWindowIdentifier = NSNib.Name("AppFeedbackWindow")
static let onboardingWindowIdentifier = NSNib.Name("OnboardingWindow")
static let welcomeViewIdentifier = NSNib.Name("WelcomeView")
static let startAtLoginViewIdentifier = NSNib.Name("StartAtLoginView")
}
enum AppFeedbackConstants {
static let CLAppFeedbackNoResponseString = "Not Provided"
static let CLAppFeedbackNameProperty = "name"
static let CLAppFeedbackEmailProperty = "email"
static let CLAppFeedbackFeedbackProperty = "feedback"
static let CLOperatingSystemVersion = "OS"
static let CLClockerVersion = "Clocker version"
static let CLFeedbackAlertTitle = "Thank you for helping make Clocker even better!"
static let CLFeedbackAlertInformativeText = "We owe you a candy. 😇"
static let CLFeedbackAlertButtonTitle = "Close"
static let CLFeedbackNotEnteredErrorMessage = "Please enter some feedback."
static let CLAppFeedbackDateProperty = "date"
static let CLAppFeedbackUserPreferences = "Prefs"
static let CLCaliforniaTimezoneIdentifier = "America/Los_Angeles"
static let CLFeedbackEntryPoint = "entry_point"
}
class AppFeedbackWindowController: NSWindowController {
@IBOutlet var nameField: NSTextField!
@IBOutlet var emailField: NSTextField!
@IBOutlet var feedbackTextView: NSTextView!
@IBOutlet var progressIndicator: NSProgressIndicator!
@IBOutlet var quickCommentsLabel: PointingHandCursorButton!
public weak var appFeedbackWindowDelegate: AppFeedbackWindowControllerDelegate?
private var themeDidChangeNotification: NSObjectProtocol?
private var serialNumber: String? {
let platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
guard platformExpert > 0 else {
return nil
}
guard let serialNumber = (IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String) else {
return nil
}
IOObjectRelease(platformExpert)
return serialNumber
}
private var isActivityInProgress = false {
didSet {
progressIndicator.isHidden = !isActivityInProgress
isActivityInProgress ? progressIndicator.startAnimation(nil) : progressIndicator.stopAnimation(nil)
}
}
static var sharedWindow = AppFeedbackWindowController(windowNibName: NSNib.Name.appFeedbackWindowIdentifier)
override func windowDidLoad() {
super.windowDidLoad()
window?.backgroundColor = Themer.shared().mainBackgroundColor()
window?.titleVisibility = .hidden
window?.titlebarAppearsTransparent = true
progressIndicator.isHidden = true
feedbackTextView.setAccessibilityIdentifier("FeedbackTextView")
nameField.setAccessibilityIdentifier("NameField")
emailField.setAccessibilityIdentifier("EmailField")
progressIndicator.setAccessibilityIdentifier("ProgressIndicator")
quickCommentsLabel.setAccessibility("QuickCommentLabel")
setup()
themeDidChangeNotification = NotificationCenter.default.addObserver(forName: .themeDidChangeNotification,
object: nil,
queue: OperationQueue.main)
{ _ in
self.window?.backgroundColor = Themer.shared().mainBackgroundColor()
self.setup()
}
}
class func shared() -> AppFeedbackWindowController {
return sharedWindow
}
override init(window: NSWindow!) {
super.init(window: window)
}
deinit {
if let themeDidChangeNotif = themeDidChangeNotification {
NotificationCenter.default.removeObserver(themeDidChangeNotif)
}
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@IBAction func sendFeedback(_: Any) {
isActivityInProgress = true
if didUserEnterFeedback() == false {
return
}
let feedbackInfo = retrieveDataForSending()
Logger.info("About to send \(feedbackInfo)")
sendDataToFirebase(feedbackInfo: feedbackInfo)
showSucccessOnSendingInfo()
}
@IBAction func cancel(_: Any) {
window?.close()
}
private func didUserEnterFeedback() -> Bool {
let cleanedUpString = feedbackTextView.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if cleanedUpString.isEmpty {
window?.contentView?.makeToast(AppFeedbackConstants.CLFeedbackNotEnteredErrorMessage)
isActivityInProgress = false
return false
}
return true
}
private func generateUserPreferences() -> String {
let preferences = DataStore.shared().timezones()
guard let theme = DataStore.shared().retrieve(key: UserDefaultKeys.themeKey) as? NSNumber,
let displayFutureSliderKey = DataStore.shared().retrieve(key: UserDefaultKeys.themeKey) as? NSNumber,
let relativeDateKey = DataStore.shared().retrieve(key: UserDefaultKeys.relativeDateKey) as? NSNumber,
let country = Locale.autoupdatingCurrent.regionCode
else {
return "Error"
}
let selectedTimezones = preferences.compactMap { data -> String? in
guard let timezoneObject = TimezoneData.customObject(from: data) else {
return nil
}
let customString = """
Timezone: \(timezoneObject.timezone())
Name: \(timezoneObject.formattedAddress ?? "No")
Favourited: \((timezoneObject.isFavourite == 1) ? "Yes" : "No")
Format: \(timezoneObject.overrideFormat)
System: \(timezoneObject.isSystemTimezone ? "Yes" : "No")"
"""
return customString
}
var relativeDate = "Relative"
if relativeDateKey.isEqual(to: NSNumber(value: 1)) {
relativeDate = "Actual Day"
} else if relativeDateKey.isEqual(to: NSNumber(value: 2)) {
relativeDate = "Date"
}
var futureSlider = "Modern"
if displayFutureSliderKey.isEqual(to: NSNumber(value: 1)) {
futureSlider = "Legacy"
} else if theme.isEqual(to: NSNumber(value: 2)) {
futureSlider = "Hidden"
}
return """
"Global Timezone Format: \(DataStore.shared().timezoneFormat()), "Display Future Slider": \(futureSlider), "Relative Date": \(relativeDate), "Country": \(country), "Timezones": \(selectedTimezones)
"""
}
private func retrieveDataForSending() -> [String: String] {
guard let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String,
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
else {
return [:]
}
let name = nameField.stringValue.isEmpty ? AppFeedbackConstants.CLAppFeedbackNoResponseString : nameField.stringValue
let email = emailField.stringValue.isEmpty ? AppFeedbackConstants.CLAppFeedbackNoResponseString : emailField.stringValue
let appFeedbackProperty = feedbackTextView.string
let operatingSystem = ProcessInfo.processInfo.operatingSystemVersion
let osVersion = "\(operatingSystem.majorVersion).\(operatingSystem.minorVersion).\(operatingSystem.patchVersion)"
let versionInfo = "Clocker \(shortVersion) (\(appVersion))"
return [
AppFeedbackConstants.CLAppFeedbackNameProperty: name,
AppFeedbackConstants.CLAppFeedbackEmailProperty: email,
AppFeedbackConstants.CLAppFeedbackFeedbackProperty: appFeedbackProperty,
AppFeedbackConstants.CLOperatingSystemVersion: osVersion,
AppFeedbackConstants.CLClockerVersion: versionInfo,
AppFeedbackConstants.CLAppFeedbackDateProperty: todaysDate(),
AppFeedbackConstants.CLAppFeedbackUserPreferences: generateUserPreferences(),
AppFeedbackConstants.CLFeedbackEntryPoint: appFeedbackWindowDelegate?.appFeedbackWindoEntryPoint() ?? "Error",
]
}
private func todaysDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
dateFormatter.timeZone = TimeZone(identifier: AppFeedbackConstants.CLCaliforniaTimezoneIdentifier)
return dateFormatter.string(from: Date())
}
var firebaseDBReference: DatabaseReference!
private func sendDataToFirebase(feedbackInfo info: [String: String]) {
#if DEBUG
Logger.info("Sending a feedback in Debug builds will lead to a no-op")
#endif
guard let identifier = serialNumber else {
assertionFailure("Serial Identifier was unexpectedly nil")
return
}
#if RELEASE
firebaseDBReference = Database.database().reference()
firebaseDBReference.child("Feedback").child(identifier).setValue(info)
#endif
}
private func showSucccessOnSendingInfo() {
guard let feedbackWindow = window else {
assertionFailure("Window property was unexpectedly nil")
return
}
isActivityInProgress = false
let alert = NSAlert()
alert.messageText = AppFeedbackConstants.CLFeedbackAlertTitle
alert.informativeText = AppFeedbackConstants.CLFeedbackAlertInformativeText
alert.addButton(withTitle: AppFeedbackConstants.CLFeedbackAlertButtonTitle)
alert.beginSheetModal(for: feedbackWindow) { _ in
self.window?.close()
}
}
@IBOutlet var contactBox: NSBox!
@IBOutlet var accessoryInfo: NSTextField!
private func setup() {
contactBox.title = "Contact Information (Optional)".localized()
accessoryInfo.stringValue = "Contact fields are optional! Your contact information will let us contact you in case we need more information or can help!".localized()
let versionUpdateInstance = iVersion.sharedInstance()
let string = versionUpdateInstance?.versionDetails(since: versionUpdateInstance?.applicationVersion,
inDict: versionUpdateInstance?.remoteVersionsDict)
if string != nil {
let range = NSRange(location: 37, length: 13)
quickCommentsLabel.title = "📣 An improved Clocker experience is now available!"
quickCommentsLabel.tag = 0
setUnderline(for: quickCommentsLabel, range: range)
} else {
let range = NSRange(location: 9, length: 16)
quickCommentsLabel.title = "Tweet to @Clocker_Support if you have a quick comment!"
setUnderline(for: quickCommentsLabel, range: range)
quickCommentsLabel.tag = 100
}
[accessoryInfo].forEach { $0?.textColor = Themer.shared().mainTextColor() }
contactBox.borderColor = Themer.shared().mainTextColor()
feedbackTextView.backgroundColor = Themer.shared().mainBackgroundColor()
nameField.backgroundColor = Themer.shared().mainBackgroundColor()
emailField.backgroundColor = Themer.shared().mainBackgroundColor()
}
private func setUnderline(for button: PointingHandCursorButton?, range: NSRange) {
guard let underlinedButton = button else { return }
let mutableParaghStyle = NSMutableParagraphStyle()
mutableParaghStyle.alignment = .center
let originalText = NSMutableAttributedString(string: underlinedButton.title)
originalText.addAttribute(NSAttributedString.Key.underlineStyle,
value: NSNumber(value: Int8(NSUnderlineStyle.single.rawValue)),
range: range)
originalText.addAttribute(NSAttributedString.Key.foregroundColor,
value: Themer.shared().mainTextColor(),
range: NSRange(location: 0, length: underlinedButton.attributedTitle.string.count))
originalText.addAttribute(NSAttributedString.Key.font,
value: (button?.font)!,
range: NSRange(location: 0, length: underlinedButton.attributedTitle.string.count))
originalText.addAttribute(NSAttributedString.Key.paragraphStyle,
value: mutableParaghStyle,
range: NSRange(location: 0, length: underlinedButton.attributedTitle.string.count))
underlinedButton.attributedTitle = originalText
}
@IBAction func navigateToSupportTwitter(_ sender: NSButton) {
let link = sender.tag == 100 ? AboutUsConstants.TwitterLink : AboutUsConstants.AppStoreUpdateLink
guard let url = URL(string: link) else { return }
NSWorkspace.shared.open(url)
}
}
extension AppFeedbackWindowController: NSWindowDelegate {
func windowWillClose(_: Notification) {
performClosingCleanUp()
bringPreferencesWindowToFront()
}
func performClosingCleanUp() {
nameField.stringValue = UserDefaultKeys.emptyString
emailField.stringValue = UserDefaultKeys.emptyString
feedbackTextView.string = UserDefaultKeys.emptyString
isActivityInProgress = false
appFeedbackWindowDelegate?.appFeedbackWindowWillClose()
}
func bringPreferencesWindowToFront() {
let windows = NSApplication.shared.windows
let prefWindow = windows.first(where: { window in
window.identifier == NSUserInterfaceItemIdentifier("Preferences")
})
if let prefW = prefWindow {
prefW.makeKeyAndOrderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
}
}