summaryrefslogtreecommitdiff
path: root/Juick
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2020-09-17 00:46:27 +0300
committerGravatar Vitaly Takmazov2020-12-10 18:59:55 +0300
commitcf97b1cd43a91725e0419a953815287fde0bf70f (patch)
tree82961197cf7fa1bfa40550748ddd309e5e4d38c3 /Juick
parent4851a21c41488a6f0c01e60f1bd1472846f816bf (diff)
SwiftUI WIP
Diffstat (limited to 'Juick')
-rw-r--r--Juick/AppDelegate.swift125
-rw-r--r--Juick/Helpers/Data+Hex.swift16
-rw-r--r--Juick/Helpers/LoadableState.swift25
-rw-r--r--Juick/Helpers/NSAttributedString_Entities.h19
-rw-r--r--Juick/Helpers/NSAttributedString_Entities.m90
-rw-r--r--Juick/ImageFetcher.swift28
-rw-r--r--Juick/Main.storyboard16
-rw-r--r--Juick/MessageFetcher.swift46
-rw-r--r--Juick/Model/Attachment.m29
-rw-r--r--Juick/Model/Message.h12
-rw-r--r--Juick/Model/Message.m2
-rw-r--r--Juick/Model/User.h1
-rw-r--r--Juick/SceneDelegate.swift63
-rw-r--r--Juick/Supporting Files/Juick-Bridging-Header.h3
-rw-r--r--Juick/Supporting Files/Juick-Info.plist17
-rw-r--r--Juick/Supporting Files/main.m16
-rw-r--r--Juick/Views/ActivityIndicator.swift28
-rw-r--r--Juick/Views/AttributedLabelView.swift107
-rw-r--r--Juick/Views/ContentView.swift47
-rw-r--r--Juick/Views/FeedView.swift66
-rw-r--r--Juick/Views/LoadableImageView.swift35
-rw-r--r--Juick/Views/MessageView.swift51
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)
+ }
+}