From cf97b1cd43a91725e0419a953815287fde0bf70f Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 17 Sep 2020 00:46:27 +0300 Subject: SwiftUI WIP --- Juick/AppDelegate.swift | 125 +++++++++++++++++++++++++ Juick/Helpers/Data+Hex.swift | 16 ++++ Juick/Helpers/LoadableState.swift | 25 +++++ Juick/Helpers/NSAttributedString_Entities.h | 19 ++++ Juick/Helpers/NSAttributedString_Entities.m | 90 ++++++++++++++++++ Juick/ImageFetcher.swift | 28 ++++++ Juick/Main.storyboard | 16 ++-- Juick/MessageFetcher.swift | 46 +++++++++ Juick/Model/Attachment.m | 29 +++--- Juick/Model/Message.h | 12 ++- Juick/Model/Message.m | 2 +- Juick/Model/User.h | 1 + Juick/SceneDelegate.swift | 63 +++++++++++++ Juick/Supporting Files/Juick-Bridging-Header.h | 3 + Juick/Supporting Files/Juick-Info.plist | 17 ++++ Juick/Supporting Files/main.m | 16 ---- Juick/Views/ActivityIndicator.swift | 28 ++++++ Juick/Views/AttributedLabelView.swift | 107 +++++++++++++++++++++ Juick/Views/ContentView.swift | 47 ++++++++++ Juick/Views/FeedView.swift | 66 +++++++++++++ Juick/Views/LoadableImageView.swift | 35 +++++++ Juick/Views/MessageView.swift | 51 ++++++++++ 22 files changed, 800 insertions(+), 42 deletions(-) create mode 100644 Juick/AppDelegate.swift create mode 100644 Juick/Helpers/Data+Hex.swift create mode 100644 Juick/Helpers/LoadableState.swift create mode 100644 Juick/Helpers/NSAttributedString_Entities.h create mode 100644 Juick/Helpers/NSAttributedString_Entities.m create mode 100644 Juick/ImageFetcher.swift create mode 100644 Juick/MessageFetcher.swift create mode 100644 Juick/SceneDelegate.swift delete mode 100644 Juick/Supporting Files/main.m create mode 100644 Juick/Views/ActivityIndicator.swift create mode 100644 Juick/Views/AttributedLabelView.swift create mode 100644 Juick/Views/ContentView.swift create mode 100644 Juick/Views/FeedView.swift create mode 100644 Juick/Views/LoadableImageView.swift create mode 100644 Juick/Views/MessageView.swift (limited to 'Juick') diff --git a/Juick/AppDelegate.swift b/Juick/AppDelegate.swift new file mode 100644 index 0000000..d1062f4 --- /dev/null +++ b/Juick/AppDelegate.swift @@ -0,0 +1,125 @@ +// +// AppDelegate.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import UIKit +import CoreData + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { + lazy var api : API = { + return API() + }() + lazy var sharedDateFormatter : DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + return dateFormatter + }() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // cleanup synchronized credentials which are not used anymore + let allCreds = URLCredentialStorage.shared.allCredentials + for (space, _) in allCreds { + if space.host == "api.juick.com" { + allCreds[space]?.values.forEach { + URLCredentialStorage.shared.remove($0, for: space, options: [NSURLCredentialStorageRemoveSynchronizableCredentials:true]) + } + } + } + #if !targetEnvironment(simulator) + registerForRemoteNotifications() + #endif + if let userInfo = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] { + parseNotificationPayload(userInfo: userInfo as! [AnyHashable : Any]) + } + return true + } + + func registerForRemoteNotifications() { + let center = UNUserNotificationCenter.current() + center.delegate = self + center.requestAuthorization(options: [.sound, .alert, .badge]) { (granted, error) in + if (error == nil) { + OperationQueue.main.addOperation { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + let token = deviceToken.hexString + OperationQueue().addOperation { + let registration = DeviceRegistration() + registration.type = "apns" + registration.token = token + self.api.refreshDeviceRegistration(registration, callback: { + (success) in + debugPrint("Successfully refreshed registration with \(token)") + }) + } + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + debugPrint("APNS error: \(error.localizedDescription)") + } + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + if userInfo["service"] as? Bool ?? false { + if let user = User.fromJSON(userInfo["user"] as? [AnyHashable : Any]) { + application.applicationIconBadgeNumber = user.unreadCount + } + } + } + // Called when a notification is delivered to a foreground app. + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.sound, .alert, .badge]) + } + //Called to let your app know which action was selected by the user for a given notification. + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + parseNotificationPayload(userInfo: userInfo) + OperationQueue.main.addOperation { + // TODO: initialize correct tab + } + } + + var pushedThread : Int? + var pushedReplyId : Int? + var pushedUname : String? + + func parseNotificationPayload(userInfo:[AnyHashable: Any]) { + self.pushedThread = userInfo["mid"] as? Int; + self.pushedUname = userInfo["uname"] as? String; + self.pushedReplyId = userInfo["rid"] as? Int; + } + + func cleanupPushedData() { + self.pushedUname = nil; + self.pushedThread = nil; + self.pushedReplyId = nil; + } + + static var shared : AppDelegate { + return UIApplication.shared.delegate as! AppDelegate + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + +} + diff --git a/Juick/Helpers/Data+Hex.swift b/Juick/Helpers/Data+Hex.swift new file mode 100644 index 0000000..69c170d --- /dev/null +++ b/Juick/Helpers/Data+Hex.swift @@ -0,0 +1,16 @@ +// +// Data+Hex.swift +// Juick +// +// Created by Vitaly Takmazov on 16.09.2020. +// Copyright © 2020 com.juick. All rights reserved. +// + +import Foundation + +extension Data { + var hexString: String { + let hexString = map { String(format: "%02.2hhx", $0) }.joined() + return hexString + } +} diff --git a/Juick/Helpers/LoadableState.swift b/Juick/Helpers/LoadableState.swift new file mode 100644 index 0000000..a45edb2 --- /dev/null +++ b/Juick/Helpers/LoadableState.swift @@ -0,0 +1,25 @@ +// +// LoadableState.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation + +enum LoadableState { + case loading + case fetched(Result) +} + +enum FetchError: Error { + case error(String) + + var localizedDescription: String { + switch self { + case .error(let message): + return message + } + } +} diff --git a/Juick/Helpers/NSAttributedString_Entities.h b/Juick/Helpers/NSAttributedString_Entities.h new file mode 100644 index 0000000..691d3d4 --- /dev/null +++ b/Juick/Helpers/NSAttributedString_Entities.h @@ -0,0 +1,19 @@ +// +// NSAttributedString+NSAttributedString_Entities.h +// Juick +// +// Created by Vitaly Takmazov on 23.09.2020. +// Copyright © 2020 com.juick. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSAttributedString (Entities) + ++(NSAttributedString *) attributedStringFromMessage:(Message *)message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Juick/Helpers/NSAttributedString_Entities.m b/Juick/Helpers/NSAttributedString_Entities.m new file mode 100644 index 0000000..3f7159b --- /dev/null +++ b/Juick/Helpers/NSAttributedString_Entities.m @@ -0,0 +1,90 @@ +// +// NSAttributedString+NSAttributedString.h +// Juick +// +// Created by Vitaly Takmazov on 23.09.2020. +// Copyright © 2020 com.juick. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSAttributedString (Entities) + +NSMutableParagraphStyle *quoteStyle; +UIFont *boldFont; +UIFont *italicFont; + +__attribute__((constructor)) +static void initialize_fonts() { + quoteStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + quoteStyle.firstLineHeadIndent = 12.0f; + quoteStyle.headIndent = 12.0f; + quoteStyle.paragraphSpacing = 6.0f; + UIFontDescriptor* fontDescriptor = [UIFontDescriptor + preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; + UIFontDescriptor* boldFontDescriptor = [fontDescriptor + fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold]; + boldFont = [UIFont fontWithDescriptor:boldFontDescriptor size: 0.0]; + UIFontDescriptor* italicFontDescriptor = [fontDescriptor + fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic]; + italicFont = [UIFont fontWithDescriptor:italicFontDescriptor size: 0.0]; +} + ++(NSAttributedString *) attributedStringFromMessage:(Message *)msg { + if (msg.text) { + NSMutableAttributedString *txt = [[NSMutableAttributedString alloc] + initWithString:msg.text + attributes:@{NSFontAttributeName:[UIFont preferredFontForTextStyle:UIFontTextStyleBody], + NSForegroundColorAttributeName:[UIColor colorNamed:@"Text"] + }]; + [txt beginEditing]; + for (Entity *entity in msg.entities) { + NSUInteger start = entity.start ? [entity.start unsignedIntegerValue] : 0; + NSUInteger end = entity.end ? [entity.end unsignedIntegerValue] : 0; + NSString *text = entity.text ? entity.text : @""; + NSRange currentRange = NSMakeRange(start, end - start); + [txt addAttribute:@"displayText" value:text range:currentRange]; + if ([entity.type isEqualToString:@"a"]) { + [txt addAttribute:NSLinkAttributeName value:entity.link range:currentRange]; + } + if ([entity.type isEqualToString:@"q"]) { + [txt addAttribute:NSForegroundColorAttributeName value:[UIColor colorNamed:@"Muted"] range:currentRange]; + [txt addAttribute:NSParagraphStyleAttributeName value:quoteStyle range:currentRange]; + } + if ([entity.type isEqualToString:@"u"]) { + [txt addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:currentRange]; + } + + if ([entity.type isEqualToString:@"b"]) { + [txt addAttribute:NSFontAttributeName value:boldFont range:currentRange]; + } + if ([entity.type isEqualToString:@"i"]) { + [txt addAttribute:NSFontAttributeName value:italicFont range:currentRange]; + } + } + [txt enumerateAttribute:@"displayText" inRange:NSMakeRange(0, [txt length]) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + if (value) { + [txt replaceCharactersInRange:range withString:value]; + } + }]; + if ([msg.tags count] > 0) { + NSString *tagsList = [NSString stringWithFormat:@"%@\n", [msg.tags componentsJoinedByString:@", "]]; + [txt insertAttributedString:[[NSAttributedString alloc] + initWithString:tagsList + attributes:@{ + NSFontAttributeName:italicFont, + NSForegroundColorAttributeName:[UIColor colorNamed:@"Muted"] + }] atIndex:0]; + } + [txt endEditing]; + return txt; + } else { + return [[NSAttributedString alloc] initWithString:@""]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Juick/ImageFetcher.swift b/Juick/ImageFetcher.swift new file mode 100644 index 0000000..f76c0ba --- /dev/null +++ b/Juick/ImageFetcher.swift @@ -0,0 +1,28 @@ +// +// ImageFetcher.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation +import Combine + +class ImageFetcher: ObservableObject { + + @Published var data: Data = Data() + + init(url: String) { + guard let imageUrl = URL(string: url) else { + return + } + + URLSession.shared.dataTask(with: imageUrl) { (data, _, _) in + guard let data = data else { return } + DispatchQueue.main.async { [weak self] in + self?.data = data + } + }.resume() + } +} diff --git a/Juick/Main.storyboard b/Juick/Main.storyboard index 5d99c97..3e7e571 100644 --- a/Juick/Main.storyboard +++ b/Juick/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -96,7 +96,7 @@ - + @@ -127,7 +127,7 @@ -