diff options
author | Vitaly Takmazov | 2020-09-17 00:46:27 +0300 |
---|---|---|
committer | Vitaly Takmazov | 2020-12-10 18:59:55 +0300 |
commit | cf97b1cd43a91725e0419a953815287fde0bf70f (patch) | |
tree | 82961197cf7fa1bfa40550748ddd309e5e4d38c3 /Juick | |
parent | 4851a21c41488a6f0c01e60f1bd1472846f816bf (diff) |
SwiftUI WIP
Diffstat (limited to 'Juick')
-rw-r--r-- | Juick/AppDelegate.swift | 125 | ||||
-rw-r--r-- | Juick/Helpers/Data+Hex.swift | 16 | ||||
-rw-r--r-- | Juick/Helpers/LoadableState.swift | 25 | ||||
-rw-r--r-- | Juick/Helpers/NSAttributedString_Entities.h | 19 | ||||
-rw-r--r-- | Juick/Helpers/NSAttributedString_Entities.m | 90 | ||||
-rw-r--r-- | Juick/ImageFetcher.swift | 28 | ||||
-rw-r--r-- | Juick/Main.storyboard | 16 | ||||
-rw-r--r-- | Juick/MessageFetcher.swift | 46 | ||||
-rw-r--r-- | Juick/Model/Attachment.m | 29 | ||||
-rw-r--r-- | Juick/Model/Message.h | 12 | ||||
-rw-r--r-- | Juick/Model/Message.m | 2 | ||||
-rw-r--r-- | Juick/Model/User.h | 1 | ||||
-rw-r--r-- | Juick/SceneDelegate.swift | 63 | ||||
-rw-r--r-- | Juick/Supporting Files/Juick-Bridging-Header.h | 3 | ||||
-rw-r--r-- | Juick/Supporting Files/Juick-Info.plist | 17 | ||||
-rw-r--r-- | Juick/Supporting Files/main.m | 16 | ||||
-rw-r--r-- | Juick/Views/ActivityIndicator.swift | 28 | ||||
-rw-r--r-- | Juick/Views/AttributedLabelView.swift | 107 | ||||
-rw-r--r-- | Juick/Views/ContentView.swift | 47 | ||||
-rw-r--r-- | Juick/Views/FeedView.swift | 66 | ||||
-rw-r--r-- | Juick/Views/LoadableImageView.swift | 35 | ||||
-rw-r--r-- | Juick/Views/MessageView.swift | 51 |
22 files changed, 800 insertions, 42 deletions
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<UISceneSession>) { + // 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<T> { + case loading + case fetched(Result<T, FetchError>) +} + +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 <Foundation/Foundation.h> + +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 <Foundation/Foundation.h> + +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 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Rbr-km-xhI"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Rbr-km-xhI"> <device id="retina4_7" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/> <capability name="Named colors" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> @@ -96,7 +96,7 @@ <objects> <viewController id="4g9-hM-bzq" customClass="LoginViewController" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="ak5-5Q-P4e"> - <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <stackView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" axis="vertical" spacing="24" translatesAutoresizingMaskIntoConstraints="NO" id="axR-g5-sfd"> @@ -127,7 +127,7 @@ <userDefinedRuntimeAttribute type="string" keyPath="placeholder" value="Password..."/> </userDefinedRuntimeAttributes> </textField> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gPQ-xI-b5J"> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gPQ-xI-b5J"> <rect key="frame" x="0.0" y="280.5" width="351" height="33"/> <color key="backgroundColor" name="Chat"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> @@ -141,13 +141,13 @@ </subviews> </stackView> </subviews> + <viewLayoutGuide key="safeArea" id="fue-ZI-ech"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <constraints> <constraint firstItem="fue-ZI-ech" firstAttribute="trailing" secondItem="axR-g5-sfd" secondAttribute="trailing" constant="12" id="CP5-In-mwY"/> <constraint firstItem="axR-g5-sfd" firstAttribute="leading" secondItem="fue-ZI-ech" secondAttribute="leading" constant="12" id="YIV-CU-Vyy"/> <constraint firstItem="axR-g5-sfd" firstAttribute="top" secondItem="fue-ZI-ech" secondAttribute="top" constant="12" id="ZUC-uk-RH7"/> </constraints> - <viewLayoutGuide key="safeArea" id="fue-ZI-ech"/> </view> <navigationItem key="navigationItem" id="QaM-45-gms"/> <connections> @@ -231,7 +231,7 @@ <objects> <viewController title="New post" hidesBottomBarWhenPushed="YES" id="rr1-jx-MLx" customClass="NewPostViewController" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="7ep-bO-aeZ"> - <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="XYj-Y4-lfM"> @@ -241,6 +241,7 @@ <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> </textView> </subviews> + <viewLayoutGuide key="safeArea" id="UJz-7C-l1q"/> <color key="backgroundColor" name="TextBackground"/> <constraints> <constraint firstItem="UJz-7C-l1q" firstAttribute="top" secondItem="XYj-Y4-lfM" secondAttribute="top" constant="4" id="5aN-lI-YBC"/> @@ -248,7 +249,6 @@ <constraint firstItem="XYj-Y4-lfM" firstAttribute="leading" secondItem="UJz-7C-l1q" secondAttribute="leading" constant="4" id="aw6-Uf-tSS"/> <constraint firstItem="UJz-7C-l1q" firstAttribute="trailing" secondItem="XYj-Y4-lfM" secondAttribute="trailing" constant="4" id="gTz-QO-1CL"/> </constraints> - <viewLayoutGuide key="safeArea" id="UJz-7C-l1q"/> </view> <navigationItem key="navigationItem" id="Yd6-Yh-gtd"> <barButtonItem key="leftBarButtonItem" systemItem="cancel" id="pNy-rM-fck"> @@ -443,7 +443,7 @@ <color red="0.97254901960784312" green="0.97254901960784312" blue="0.97254901960784312" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> </namedColor> <namedColor name="TextBackground"> - <color red="0.99215686274509807" green="0.99215686274509807" blue="0.99607843137254903" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/> + <color red="0.99199998378753662" green="0.99199998378753662" blue="0.99599999189376831" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/> </namedColor> <namedColor name="Title"> <color red="0.23529411764705882" green="0.46666666666666667" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> diff --git a/Juick/MessageFetcher.swift b/Juick/MessageFetcher.swift new file mode 100644 index 0000000..b478e9e --- /dev/null +++ b/Juick/MessageFetcher.swift @@ -0,0 +1,46 @@ +// +// MessageFetcher.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation +import Combine + +class MessageFetcher: ObservableObject { + + typealias Feed = [MessageData] + + struct MessageData : Identifiable { + var id : String { + get { + return "\(message.mid.stringValue)-\(message.rid?.intValue ?? 0)" + } + } + var message : Message + } + + @Published var state: LoadableState<Feed> = .loading + + init(url: String) { + guard let apiUrl = URL(string: url) else { + state = .fetched(.failure(.error("Malformed API URL."))) + return + } + + AppDelegate.shared.api.pullNext(fromPath: url, params: nil) { (messages, error) in + if let error = error { + self.state = .fetched(.failure(.error(error.localizedDescription))) + return + } + + guard let messages = messages else { + self.state = .fetched(.failure(.error("Malformed response data"))) + return + } + self.state = .fetched(.success(messages.map { return MessageData(message: $0) } )) + } + } +} diff --git a/Juick/Model/Attachment.m b/Juick/Model/Attachment.m index 006b72e..e0de2ca 100644 --- a/Juick/Model/Attachment.m +++ b/Juick/Model/Attachment.m @@ -11,20 +11,23 @@ @implementation Attachment +(Attachment *) fromJSON:(NSDictionary *)jsonData { - Attachment *attachment = [Attachment new]; - attachment.url = jsonData[@"url"]; - attachment.width = jsonData[@"width"]; - attachment.height = jsonData[@"height"]; - if (jsonData[@"small"]) { - attachment.small = [Attachment fromJSON:jsonData[@"small"]]; + if (jsonData[@"url"]) { + Attachment *attachment = [Attachment new]; + attachment.url = jsonData[@"url"]; + attachment.width = jsonData[@"width"]; + attachment.height = jsonData[@"height"]; + if (jsonData[@"small"]) { + attachment.small = [Attachment fromJSON:jsonData[@"small"]]; + } + if (jsonData[@"medium"]) { + attachment.medium = [Attachment fromJSON:jsonData[@"medium"]]; + } + if (jsonData[@"thumbnail"]) { + attachment.thumbnail = [Attachment fromJSON:jsonData[@"thumbnail"]]; + } + return attachment; } - if (jsonData[@"medium"]) { - attachment.medium = [Attachment fromJSON:jsonData[@"medium"]]; - } - if (jsonData[@"thumbnail"]) { - attachment.thumbnail = [Attachment fromJSON:jsonData[@"thumbnail"]]; - } - return attachment; + return nil; } @end diff --git a/Juick/Model/Message.h b/Juick/Model/Message.h index 33d9911..5c85b1e 100644 --- a/Juick/Model/Message.h +++ b/Juick/Model/Message.h @@ -11,22 +11,26 @@ #import "Attachment.h" #import "Entity.h" +NS_ASSUME_NONNULL_BEGIN + @interface Message : NSObject -@property NSNumber *mid; -@property NSNumber *rid; +@property NSNumber * _Nonnull mid; +@property NSNumber * _Nullable rid; @property User *user; -@property NSString *text; +@property NSString * _Nullable text; @property NSArray<NSString *> *tags; @property NSArray<Entity *> *entities; @property NSString *timestamp; @property NSString *attach; @property NSString *repliesBy; @property NSNumber *repliesCount; -@property Attachment *attachment; +@property Attachment * _Nullable attachment; @property BOOL service; @property BOOL unread; +(Message *) fromJSON:(NSDictionary *)jsonData; @end + +NS_ASSUME_NONNULL_END diff --git a/Juick/Model/Message.m b/Juick/Model/Message.m index d39be54..afc499e 100644 --- a/Juick/Model/Message.m +++ b/Juick/Model/Message.m @@ -13,7 +13,7 @@ + (Message *) fromJSON:(NSDictionary *)jsonData { Message * message = [Message new]; message.mid = jsonData[@"mid"]; - message.rid = jsonData[@"rid"]; + message.rid = [NSNumber numberWithInt:jsonData[@"rid"]]; message.text = jsonData[@"body"]; message.attach = jsonData[@"photo"][@"small"]; message.repliesCount = jsonData[@"replies"]; diff --git a/Juick/Model/User.h b/Juick/Model/User.h index 4bbf332..5941a90 100644 --- a/Juick/Model/User.h +++ b/Juick/Model/User.h @@ -7,6 +7,7 @@ // @import Foundation; +@import UIKit; @interface User : NSObject @property (nonatomic, strong) NSString *uname; diff --git a/Juick/SceneDelegate.swift b/Juick/SceneDelegate.swift new file mode 100644 index 0000000..024538a --- /dev/null +++ b/Juick/SceneDelegate.swift @@ -0,0 +1,63 @@ +// +// SceneDelegate.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import UIKit +import SwiftUI + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + + let contentView = ContentView() + + // Use a UIHostingController as window root view controller. + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: contentView) + self.window = window + self.window?.tintColor = UIColor(named: "Title") + window.makeKeyAndVisible() + } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} diff --git a/Juick/Supporting Files/Juick-Bridging-Header.h b/Juick/Supporting Files/Juick-Bridging-Header.h index e11d920..03d88eb 100644 --- a/Juick/Supporting Files/Juick-Bridging-Header.h +++ b/Juick/Supporting Files/Juick-Bridging-Header.h @@ -1,3 +1,6 @@ // // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "API.h" +#import "DeviceRegistration.h" +#import "NSAttributedString_Entities.h" diff --git a/Juick/Supporting Files/Juick-Info.plist b/Juick/Supporting Files/Juick-Info.plist index 46c6650..c706fb6 100644 --- a/Juick/Supporting Files/Juick-Info.plist +++ b/Juick/Supporting Files/Juick-Info.plist @@ -2,6 +2,23 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>UIApplicationSceneManifest</key> + <dict> + <key>UIApplicationSupportsMultipleScenes</key> + <false/> + <key>UISceneConfigurations</key> + <dict> + <key>UIWindowSceneSessionRoleApplication</key> + <array> + <dict> + <key>UISceneConfigurationName</key> + <string>Default Configuration</string> + <key>UISceneDelegateClassName</key> + <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> + </dict> + </array> + </dict> + </dict> <key>CFBundleDevelopmentRegion</key> <string>en</string> <key>CFBundleDisplayName</key> diff --git a/Juick/Supporting Files/main.m b/Juick/Supporting Files/main.m deleted file mode 100644 index 8e01106..0000000 --- a/Juick/Supporting Files/main.m +++ /dev/null @@ -1,16 +0,0 @@ -// -// main.m -// Juick -// -// Created by Vitaly Takmazov on 26.10.13. -// Copyright (c) 2013 com.juick. All rights reserved. -// - -#import <UIKit/UIKit.h> - -int main(int argc, char * argv[]) -{ - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/Juick/Views/ActivityIndicator.swift b/Juick/Views/ActivityIndicator.swift new file mode 100644 index 0000000..5b160fd --- /dev/null +++ b/Juick/Views/ActivityIndicator.swift @@ -0,0 +1,28 @@ +// +// ActivityIndicator.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation +import SwiftUI + +struct ActivityIndicator: UIViewRepresentable { + let style: UIActivityIndicatorView.Style + + func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView { + return UIActivityIndicatorView(style: style) + } + + func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) { + uiView.startAnimating() + } +} + +struct ActivityIndicator_Previews: PreviewProvider { + static var previews: some View { + /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ + } +} diff --git a/Juick/Views/AttributedLabelView.swift b/Juick/Views/AttributedLabelView.swift new file mode 100644 index 0000000..08051a9 --- /dev/null +++ b/Juick/Views/AttributedLabelView.swift @@ -0,0 +1,107 @@ +// +// AttributedLabelView.swift +// Demo +// +// Created by Ernesto Rivera on 5/14/20. +// Copyright © 2020 Atributika. All rights reserved. +// +import SwiftUI +import Atributika + +@available(iOS 13.0, *) +struct AttributedLabelView: UIViewRepresentable +{ + var attributedText: AttributedText? + var configureLabel: ((AttributedLabel) -> Void)? = nil + + @State var maxWidth: CGFloat = 300 + + typealias UIViewType = MaxWidthAttributedLabel + + func makeUIView(context: Context) -> MaxWidthAttributedLabel + { + let view = MaxWidthAttributedLabel() + configureLabel?(view) + return view + } + + func updateUIView(_ uiView: MaxWidthAttributedLabel, context: Context) + { + uiView.attributedText = attributedText + uiView.maxWidth = maxWidth + } + + class MaxWidthAttributedLabel: AttributedLabel + { + var maxWidth: CGFloat! + + open override var intrinsicContentSize: CGSize + { + sizeThatFits(CGSize(width: maxWidth, height: .infinity)) + } + } +} + +@available(iOS 13.0, *) +struct AttributtedLabelView_Previews: PreviewProvider +{ + static var previews: some View + { + let all = Style.font(UIFont.preferredFont(forTextStyle: .body)) + let link = Style("a") + .foregroundColor(Color(named: "Title") ?? .blue, .normal) + .foregroundColor(.brown, .highlighted) + let configureLabel: ((AttributedLabel) -> Void) = { label in + label.numberOfLines = 0 + label.textColor = .label + } + + return GeometryReader { geometry in + List { + AttributedLabelView(attributedText: """ +Denny JA: Dengan RT ini, anda ikut memenangkan Jokowi-JK. Pilih hghghg + hghfghfgh + fghfgh + fgh + gfh + fgh + dipercaya (Jokowi) dan pengalaman (JK). #DJoJK +""" + .style(tags: link) + .styleHashtags(link) + .styleMentions(link) + .styleLinks(link) + .styleAll(all), configureLabel: configureLabel, maxWidth: geometry.size.width) + .fixedSize(horizontal: true, vertical: true) + .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 24)) + .listRowInsets(EdgeInsets()) + AttributedLabelView(attributedText: "@e2F If only Bradley's arm was longer. Best ever. 😊 #oscars https://m/p<br>Check this <a href=\"https://github.com/psharanda/Atributika\">link</a>" + .style(tags: link) + .styleHashtags(link) + .styleMentions(link) + .styleLinks(link) + .styleAll(all), configureLabel: configureLabel) + .fixedSize(horizontal: true, vertical: true) + .padding() + .listRowInsets(EdgeInsets()) + AttributedLabelView(attributedText: """ + # A big message + - With *mentions* [Ernesto Test Account](user://91010061) + - **Bold** text + ## Also + > Quotes + 1. Some `code` + 2. And data detectors (801) 917 4444, email@dot.com and http://apple.com + """ + .style(tags: link) + .styleHashtags(link) + .styleMentions(link) + .styleLinks(link) + .styleAll(all), configureLabel: configureLabel) + .fixedSize(horizontal: true, vertical: true) + .listRowInsets(EdgeInsets()) + .padding() + }.listRowInsets(EdgeInsets()) + } + } +} diff --git a/Juick/Views/ContentView.swift b/Juick/Views/ContentView.swift new file mode 100644 index 0000000..9a9f45b --- /dev/null +++ b/Juick/Views/ContentView.swift @@ -0,0 +1,47 @@ +// +// ContentView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI +import Combine + +struct ContentView: View { + @State var selectedView = 0 + var body: some View { + NavigationView { + TabView(selection: $selectedView) { + FeedView(url: "https://api.juick.com/messages?popular=1", type: .messages) + .tabItem { + Image("ei-clock") + Text("Today") + }.tag(0) + FeedView(url: "https://api.juick.com/messages/discussions", type: .messages) + .tabItem { + Image("ei-bell") + Text("Discussions") + }.tag(1) + FeedView(url: "https://api.juick.com/messages", type: .messages) + .tabItem { + Image("ei-envelope") + Text("Chats") + }.tag(2) + FeedView(url: "https://api.juick.com/messages", type: .messages) + .tabItem { + Image("ei-search") + Text("Discover") + }.tag(3) + } + .navigationBarTitle("Juick", displayMode: .inline) + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Juick/Views/FeedView.swift b/Juick/Views/FeedView.swift new file mode 100644 index 0000000..fe3aaed --- /dev/null +++ b/Juick/Views/FeedView.swift @@ -0,0 +1,66 @@ +// +// FeedView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI + +enum FeedType { + case messages + case thread +} + +struct FeedView: View { + @ObservedObject var messageFetcher : MessageFetcher + + let feedType : FeedType + + init(url: String, type: FeedType) { + feedType = type + messageFetcher = MessageFetcher(url: url) + } + + private var stateContent: AnyView { + switch messageFetcher.state { + case .loading: + return AnyView( + ActivityIndicator(style: .medium) + ) + case .fetched(let result): + switch result { + case .failure(let error): + return AnyView( + Text(error.localizedDescription) + ) + case .success(let root): + return AnyView( + List(root) { (message: MessageFetcher.MessageData) in + let destination = feedType == .thread ? AnyView(Text("YO")) : AnyView(FeedView(url: "https://api.juick.com/thread?mid=\(message.message.mid.stringValue)", type: .thread)) + ZStack { + MessageView(message: message.message).listRowInsets(.none) + NavigationLink( + destination: destination) { + EmptyView() + } + } + } + .listRowInsets(.none) + ) + } + } + } + + var body: some View { + stateContent + .navigationBarTitle(Text("Messages"), displayMode: .inline) + } +} + +struct FeedView_Previews: PreviewProvider { + static var previews: some View { + FeedView(url: "https://api.juick.com/messages", type: .messages) + } +} diff --git a/Juick/Views/LoadableImageView.swift b/Juick/Views/LoadableImageView.swift new file mode 100644 index 0000000..e639602 --- /dev/null +++ b/Juick/Views/LoadableImageView.swift @@ -0,0 +1,35 @@ +// +// LoadableImageView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI + +struct LoadableImageView: View { + @ObservedObject var imageFetcher: ImageFetcher + + init(with urlString: String) { + imageFetcher = ImageFetcher(url: urlString) + } + + var body: some View { + if let image = UIImage(data: imageFetcher.data) { + return AnyView( + Image(uiImage: image).resizable() + ) + } else { + return AnyView( + ActivityIndicator(style: .medium) + ) + } + } +} + +struct LoadableImageView_Previews: PreviewProvider { + static var previews: some View { + LoadableImageView(with: "https://i.juick.com/a/1.png") + } +} diff --git a/Juick/Views/MessageView.swift b/Juick/Views/MessageView.swift new file mode 100644 index 0000000..ffd0f19 --- /dev/null +++ b/Juick/Views/MessageView.swift @@ -0,0 +1,51 @@ +// +// MessageView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI +import Atributika + +struct MessageView: View { + var message: Message + let all = Style.font(UIFont.preferredFont(forTextStyle: .body)) + let link = Style("a") + .foregroundColor(.blue, .normal) + .foregroundColor(.brown, .highlighted) + let configureLabel: ((AttributedLabel) -> Void) = { label in + label.numberOfLines = 0 + label.textColor = .label + } + var body: some View { + VStack(alignment: .leading) { + HStack { + LoadableImageView(with: message.user.avatar ?? "") + .frame(width: 48, height: 48, alignment: .center) + Text(message.user.uname) + .font(.headline) + .foregroundColor(.accentColor) + .fixedSize(horizontal: true, vertical: false) + } + AttributedLabelView(attributedText: (message.text ?? "") + .style(tags: link) + .styleHashtags(link) + .styleMentions(link) + .styleLinks(link) + .styleAll(all), configureLabel: configureLabel) + .fixedSize(horizontal: true, vertical: true) + message.attachment.map { + LoadableImageView(with: $0.url).scaledToFit() + } + } + } +} + +struct MessageView_Previews: PreviewProvider { + static let msg = Message() + static var previews: some View { + MessageView(message: msg) + } +} |