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 | |
parent | 4851a21c41488a6f0c01e60f1bd1472846f816bf (diff) |
SwiftUI WIP
-rw-r--r-- | Juick.xcodeproj/project.pbxproj | 165 | ||||
-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 |
23 files changed, 939 insertions, 68 deletions
diff --git a/Juick.xcodeproj/project.pbxproj b/Juick.xcodeproj/project.pbxproj index 8ab2162..619ef46 100644 --- a/Juick.xcodeproj/project.pbxproj +++ b/Juick.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -14,7 +14,6 @@ 77317BAE181BBE8500D60005 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77317BAD181BBE8500D60005 /* CoreGraphics.framework */; }; 77317BB0181BBE8500D60005 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77317BAF181BBE8500D60005 /* UIKit.framework */; }; 77317BB6181BBE8500D60005 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 77317BB4181BBE8500D60005 /* InfoPlist.strings */; }; - 77317BB8181BBE8500D60005 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 77317BB7181BBE8500D60005 /* main.m */; }; 77317BBC181BBE8500D60005 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 77317BBB181BBE8500D60005 /* AppDelegate.m */; }; 77317BC2181BBE8500D60005 /* MessagesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 77317BC1181BBE8500D60005 /* MessagesViewController.m */; }; 77317BC7181BBE8500D60005 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77317BC6181BBE8500D60005 /* Images.xcassets */; }; @@ -36,6 +35,8 @@ 774746AD239F82A10001C7F9 /* NSDate+TimeAgo.m in Sources */ = {isa = PBXBuildFile; fileRef = 774746AC239F82A10001C7F9 /* NSDate+TimeAgo.m */; }; 774746B6239F872A0001C7F9 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 774746B5239F872A0001C7F9 /* CoreServices.framework */; }; 774C98CD25126C070073C70A /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774C98CC25126C070073C70A /* Service.swift */; }; + 774E6B52251AB5A4006B5D5F /* AttributedLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774E6B51251AB5A4006B5D5F /* AttributedLabelView.swift */; }; + 774E6B64251AC4D2006B5D5F /* NSAttributedString_Entities.m in Sources */ = {isa = PBXBuildFile; fileRef = 774E6B57251AB743006B5D5F /* NSAttributedString_Entities.m */; }; 7761133821766A3000D350CD /* ContentLoadingCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 7761133621766A3000D350CD /* ContentLoadingCell.m */; }; 7761133921766A3000D350CD /* ContentLoadingCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7761133721766A3000D350CD /* ContentLoadingCell.xib */; }; 7761135D21790B0300D350CD /* JuickPush.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7761135521790B0200D350CD /* JuickPush.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -60,9 +61,21 @@ 77C67EEC18283F2D00427098 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77C67EEB18283F2D00427098 /* QuartzCore.framework */; }; 77C6ADDE1F770EB2000AEA8C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 77C6ADDD1F770EB2000AEA8C /* Main.storyboard */; }; 77C6ADE41F7717BC000AEA8C /* ThreadViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 77C6ADE31F7717BC000AEA8C /* ThreadViewController.m */; }; + 77CEB65825129F550055FF30 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB65425129F550055FF30 /* SceneDelegate.swift */; }; + 77CEB65925129F550055FF30 /* ImageFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB65525129F550055FF30 /* ImageFetcher.swift */; }; + 77CEB65A25129F550055FF30 /* MessageFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB65625129F550055FF30 /* MessageFetcher.swift */; }; + 77CEB65B25129F550055FF30 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB65725129F550055FF30 /* AppDelegate.swift */; }; + 77CEB66425129F7E0055FF30 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB65F25129F7E0055FF30 /* MessageView.swift */; }; + 77CEB66525129F7E0055FF30 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB66025129F7E0055FF30 /* ActivityIndicator.swift */; }; + 77CEB66625129F7E0055FF30 /* LoadableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB66125129F7E0055FF30 /* LoadableImageView.swift */; }; + 77CEB66725129F7E0055FF30 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB66225129F7E0055FF30 /* ContentView.swift */; }; + 77CEB66825129F7E0055FF30 /* FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB66325129F7E0055FF30 /* FeedView.swift */; }; + 77CEB66D25129F980055FF30 /* LoadableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB66C25129F980055FF30 /* LoadableState.swift */; }; + 77CEB6802512A8BF0055FF30 /* Data+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEB67F2512A8BF0055FF30 /* Data+Hex.swift */; }; 77E35A82189A5B5A00B2D216 /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 77E35A81189A5B5A00B2D216 /* LoginViewController.m */; }; 77E61A5B1FD467FC00B4E304 /* QuoteView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 77E61A5A1FD467FC00B4E304 /* QuoteView.xib */; }; 77E61A5E1FD4682B00B4E304 /* QuoteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 77E61A5D1FD4682B00B4E304 /* QuoteView.m */; }; + 77E7E73A252CE95200957295 /* Atributika in Frameworks */ = {isa = PBXBuildFile; productRef = 77E7E739252CE95200957295 /* Atributika */; }; 77F2B6A2251249F300E42F6F /* JuickTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F2B6A1251249F300E42F6F /* JuickTests.swift */; }; 77FCADDF1D6A50DA00CBA649 /* API.m in Sources */ = {isa = PBXBuildFile; fileRef = 77FCADDE1D6A50DA00CBA649 /* API.m */; }; /* End PBXBuildFile section */ @@ -112,7 +125,6 @@ 77317BAF181BBE8500D60005 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 77317BB3181BBE8500D60005 /* Juick-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Juick-Info.plist"; sourceTree = "<group>"; }; 77317BB5181BBE8500D60005 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; - 77317BB7181BBE8500D60005 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 77317BB9181BBE8500D60005 /* Juick-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Juick-Prefix.pch"; sourceTree = "<group>"; }; 77317BBA181BBE8500D60005 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; }; 77317BBB181BBE8500D60005 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; }; @@ -149,6 +161,9 @@ 774746B5239F872A0001C7F9 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/CoreServices.framework; sourceTree = DEVELOPER_DIR; }; 774C98C0251263720073C70A /* Juick-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Juick-Bridging-Header.h"; sourceTree = "<group>"; }; 774C98CC25126C070073C70A /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; }; + 774E6B51251AB5A4006B5D5F /* AttributedLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedLabelView.swift; sourceTree = "<group>"; }; + 774E6B56251AB70B006B5D5F /* NSAttributedString_Entities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSAttributedString_Entities.h; sourceTree = "<group>"; }; + 774E6B57251AB743006B5D5F /* NSAttributedString_Entities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSAttributedString_Entities.m; sourceTree = "<group>"; }; 7761133521766A3000D350CD /* ContentLoadingCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContentLoadingCell.h; sourceTree = "<group>"; }; 7761133621766A3000D350CD /* ContentLoadingCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContentLoadingCell.m; sourceTree = "<group>"; }; 7761133721766A3000D350CD /* ContentLoadingCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ContentLoadingCell.xib; sourceTree = "<group>"; }; @@ -191,6 +206,18 @@ 77C6ADDD1F770EB2000AEA8C /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; }; 77C6ADE21F7717BC000AEA8C /* ThreadViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ThreadViewController.h; sourceTree = "<group>"; }; 77C6ADE31F7717BC000AEA8C /* ThreadViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadViewController.m; sourceTree = "<group>"; }; + 77CEB65425129F550055FF30 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; + 77CEB65525129F550055FF30 /* ImageFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFetcher.swift; sourceTree = "<group>"; }; + 77CEB65625129F550055FF30 /* MessageFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFetcher.swift; sourceTree = "<group>"; }; + 77CEB65725129F550055FF30 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + 77CEB65F25129F7E0055FF30 /* MessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = "<group>"; }; + 77CEB66025129F7E0055FF30 /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; }; + 77CEB66125129F7E0055FF30 /* LoadableImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadableImageView.swift; sourceTree = "<group>"; }; + 77CEB66225129F7E0055FF30 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; + 77CEB66325129F7E0055FF30 /* FeedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedView.swift; sourceTree = "<group>"; }; + 77CEB66C25129F980055FF30 /* LoadableState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadableState.swift; sourceTree = "<group>"; }; + 77CEB67F2512A8BF0055FF30 /* Data+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Hex.swift"; sourceTree = "<group>"; }; + 77CEB6872512AB680055FF30 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/iOSSupport/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; 77D40AB8218B5BD60074E14F /* Local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Local.xcconfig; sourceTree = "<group>"; }; 77D40ABB218B5CC90074E14F /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = "<group>"; }; 77E35A80189A5B5A00B2D216 /* LoginViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = "<group>"; }; @@ -218,6 +245,7 @@ 77317BB0181BBE8500D60005 /* UIKit.framework in Frameworks */, 77317BAC181BBE8500D60005 /* Foundation.framework in Frameworks */, 774746B6239F872A0001C7F9 /* CoreServices.framework in Frameworks */, + 77E7E73A252CE95200957295 /* Atributika in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -263,6 +291,7 @@ 77317BAA181BBE8500D60005 /* Frameworks */ = { isa = PBXGroup; children = ( + 77CEB6872512AB680055FF30 /* SwiftUI.framework */, 774746B5239F872A0001C7F9 /* CoreServices.framework */, 774528CB1F93EE9F004D110B /* libz.tbd */, 774528C91F93EE83004D110B /* AssetsLibrary.framework */, @@ -283,23 +312,27 @@ isa = PBXGroup; children = ( 774528CD1F96B39C004D110B /* Juick.entitlements */, + 77FCADE01D6A50EC00CBA649 /* API.h */, + 77317BBA181BBE8500D60005 /* AppDelegate.h */, + 77FCADDE1D6A50DA00CBA649 /* API.m */, + 77317BBB181BBE8500D60005 /* AppDelegate.m */, 773F23341F76ED5D00B5B0DF /* Splash.png */, 773F23351F76ED5D00B5B0DF /* Splash@2x.png */, 773F23361F76ED5E00B5B0DF /* Splash@3x.png */, 773F23321F76ECAC00B5B0DF /* LaunchScreen.storyboard */, + 77C6ADDD1F770EB2000AEA8C /* Main.storyboard */, + 77CEB65725129F550055FF30 /* AppDelegate.swift */, + 77CEB65525129F550055FF30 /* ImageFetcher.swift */, + 77CEB65625129F550055FF30 /* MessageFetcher.swift */, + 77CEB65425129F550055FF30 /* SceneDelegate.swift */, + 774C98CC25126C070073C70A /* Service.swift */, + 77C364912243FAEF0017522C /* Colors.xcassets */, + 77317BC6181BBE8500D60005 /* Images.xcassets */, 774DD4601D735E1D00C7F290 /* Helpers */, 774DD45D1D735DDD00C7F290 /* Model */, 77317BB2181BBE8500D60005 /* Supporting Files */, 774DD45F1D735E0300C7F290 /* ViewControllers */, 77FFC0151D5FD13C003BD81A /* Views */, - 77FCADE01D6A50EC00CBA649 /* API.h */, - 77FCADDE1D6A50DA00CBA649 /* API.m */, - 77317BBA181BBE8500D60005 /* AppDelegate.h */, - 77317BBB181BBE8500D60005 /* AppDelegate.m */, - 77317BC6181BBE8500D60005 /* Images.xcassets */, - 77C6ADDD1F770EB2000AEA8C /* Main.storyboard */, - 77C364912243FAEF0017522C /* Colors.xcassets */, - 774C98CC25126C070073C70A /* Service.swift */, ); path = Juick; sourceTree = "<group>"; @@ -312,7 +345,6 @@ 77317BB4181BBE8500D60005 /* InfoPlist.strings */, 77317BB3181BBE8500D60005 /* Juick-Info.plist */, 77317BB9181BBE8500D60005 /* Juick-Prefix.pch */, - 77317BB7181BBE8500D60005 /* main.m */, 77D40AB8218B5BD60074E14F /* Local.xcconfig */, 77D40ABB218B5CC90074E14F /* Production.xcconfig */, ); @@ -379,6 +411,7 @@ 774DD4601D735E1D00C7F290 /* Helpers */ = { isa = PBXGroup; children = ( + 77CEB66C25129F980055FF30 /* LoadableState.swift */, 77C67EE31828342000427098 /* NSURL+PathParameters.h */, 77C67EE41828342000427098 /* NSURL+PathParameters.m */, 77B09992189D0B9900A84F59 /* UIImage+Utils.h */, @@ -387,6 +420,9 @@ 7785605F2343D24E00BB37A2 /* NSData+Hex.m */, 774746AB239F82A10001C7F9 /* NSDate+TimeAgo.h */, 774746AC239F82A10001C7F9 /* NSDate+TimeAgo.m */, + 77CEB67F2512A8BF0055FF30 /* Data+Hex.swift */, + 774E6B56251AB70B006B5D5F /* NSAttributedString_Entities.h */, + 774E6B57251AB743006B5D5F /* NSAttributedString_Entities.m */, ); path = Helpers; sourceTree = "<group>"; @@ -414,6 +450,11 @@ 77FFC0151D5FD13C003BD81A /* Views */ = { isa = PBXGroup; children = ( + 77CEB66025129F7E0055FF30 /* ActivityIndicator.swift */, + 77CEB66225129F7E0055FF30 /* ContentView.swift */, + 77CEB66325129F7E0055FF30 /* FeedView.swift */, + 77CEB66125129F7E0055FF30 /* LoadableImageView.swift */, + 77CEB65F25129F7E0055FF30 /* MessageView.swift */, 773E63A1204BE036008B8F8D /* BubbleMessageCell.h */, 773E63A2204BE036008B8F8D /* BubbleMessageCell.m */, 773E6391204BCAD6008B8F8D /* BubbleMessageCell.xib */, @@ -432,6 +473,7 @@ 7761133521766A3000D350CD /* ContentLoadingCell.h */, 7761133621766A3000D350CD /* ContentLoadingCell.m */, 7761133721766A3000D350CD /* ContentLoadingCell.xib */, + 774E6B51251AB5A4006B5D5F /* AttributedLabelView.swift */, ); path = Views; sourceTree = "<group>"; @@ -454,6 +496,9 @@ 7761135C21790B0300D350CD /* PBXTargetDependency */, ); name = Juick; + packageProductDependencies = ( + 77E7E739252CE95200957295 /* Atributika */, + ); productName = Juick; productReference = 77317BA8181BBE8500D60005 /* Juick.app */; productType = "com.apple.product-type.application"; @@ -541,6 +586,9 @@ Base, ); mainGroup = 77317B9F181BBE8500D60005; + packageReferences = ( + 77E7E738252CE95200957295 /* XCRemoteSwiftPackageReference "Atributika" */, + ); productRefGroup = 77317BA9181BBE8500D60005 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -596,9 +644,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 774E6B64251AC4D2006B5D5F /* NSAttributedString_Entities.m in Sources */, 77C36498224417FC0017522C /* DiscussionsController.m in Sources */, + 77CEB65A25129F550055FF30 /* MessageFetcher.swift in Sources */, + 77CEB66825129F7E0055FF30 /* FeedView.swift in Sources */, + 77CEB66525129F7E0055FF30 /* ActivityIndicator.swift in Sources */, 770C86E325117D78009B6404 /* UIView+Shimmer.m in Sources */, + 77CEB66D25129F980055FF30 /* LoadableState.swift in Sources */, 7761133821766A3000D350CD /* ContentLoadingCell.m in Sources */, + 77CEB65925129F550055FF30 /* ImageFetcher.swift in Sources */, 77E61A5E1FD4682B00B4E304 /* QuoteView.m in Sources */, 77317BBC181BBE8500D60005 /* AppDelegate.m in Sources */, 776C41C11FD3FF6E0063B82E /* FeedViewController.m in Sources */, @@ -606,8 +660,12 @@ 77A0954A181F1F25002BDECD /* Message.m in Sources */, 773E63A0204BDF16008B8F8D /* ChatViewController.m in Sources */, 773E639D204BD0F2008B8F8D /* Chat.m in Sources */, + 77CEB65B25129F550055FF30 /* AppDelegate.swift in Sources */, + 77CEB66425129F7E0055FF30 /* MessageView.swift in Sources */, 77B8DCD62093FC03000DBB04 /* BlogViewController.m in Sources */, 773E639A204BCE6D008B8F8D /* DialogsViewController.m in Sources */, + 774E6B52251AB5A4006B5D5F /* AttributedLabelView.swift in Sources */, + 77CEB6802512A8BF0055FF30 /* Data+Hex.swift in Sources */, 772B4E6C2199811E0029706E /* Entity.m in Sources */, 77B09994189D0B9900A84F59 /* UIImage+Utils.m in Sources */, 77317BC2181BBE8500D60005 /* MessagesViewController.m in Sources */, @@ -617,14 +675,16 @@ 77975A1D182B6E9A00410C2B /* NewPostViewController.m in Sources */, 774746AD239F82A10001C7F9 /* NSDate+TimeAgo.m in Sources */, 77C36495224417E90017522C /* DiscoverViewController.m in Sources */, + 77CEB66725129F7E0055FF30 /* ContentView.swift in Sources */, 77C67EE51828342000427098 /* NSURL+PathParameters.m in Sources */, 77E35A82189A5B5A00B2D216 /* LoginViewController.m in Sources */, 773E6397204BCB64008B8F8D /* ConversationCell.m in Sources */, 778560602343D24E00BB37A2 /* NSData+Hex.m in Sources */, 774C98CD25126C070073C70A /* Service.swift in Sources */, + 77CEB66625129F7E0055FF30 /* LoadableImageView.swift in Sources */, 776C41BD1FD3EF180063B82E /* MessageCell.m in Sources */, - 77317BB8181BBE8500D60005 /* main.m in Sources */, 77B8B39C207A5629005CB20C /* MessageInputView.m in Sources */, + 77CEB65825129F550055FF30 /* SceneDelegate.swift in Sources */, 77C3648C2241B3060017522C /* DeviceRegistration.m in Sources */, 774528C21F930C06004D110B /* Attachment.m in Sources */, 778560632344CF6F00BB37A2 /* JuickNavigationController.m in Sources */, @@ -726,7 +786,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; @@ -776,7 +836,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; @@ -797,7 +857,10 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Juick/Supporting Files/Juick-Prefix.pch"; INFOPLIST_FILE = "Juick/Supporting Files/Juick-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.juick.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -823,7 +886,10 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Juick/Supporting Files/Juick-Prefix.pch"; INFOPLIST_FILE = "Juick/Supporting Files/Juick-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.juick.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -856,7 +922,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = JuickPush/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.juick.Juick.JuickPush; @@ -889,7 +959,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = JuickPush/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.juick.Juick.JuickPush; @@ -953,7 +1027,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; @@ -974,7 +1048,10 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Juick/Supporting Files/Juick-Prefix.pch"; INFOPLIST_FILE = "Juick/Supporting Files/Juick-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.juick.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -1008,7 +1085,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = JuickPush/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.juick.Juick.JuickPush; @@ -1038,7 +1119,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = JuickTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.juick.JuickTests; @@ -1067,7 +1152,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = JuickTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.juick.JuickTests; @@ -1096,12 +1185,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = JuickTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.juick.JuickTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Juick; @@ -1152,6 +1246,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 77E7E738252CE95200957295 /* XCRemoteSwiftPackageReference "Atributika" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/psharanda/Atributika.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.9.9; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 77E7E739252CE95200957295 /* Atributika */ = { + isa = XCSwiftPackageProductDependency; + package = 77E7E738252CE95200957295 /* XCRemoteSwiftPackageReference "Atributika" */; + productName = Atributika; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 77317BA0181BBE8500D60005 /* Project object */; } 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) + } +} |