mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-22 13:37:01 +00:00
Integrate stuff from old UIKit project
This commit is contained in:
parent
7863866f68
commit
c309b94ad0
30 changed files with 1812 additions and 86 deletions
|
@ -52,10 +52,22 @@
|
|||
D020F51224ECA309005AB084 /* MastodonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51024ECA309005AB084 /* MastodonContext.swift */; };
|
||||
D020F51424ECBA60005AB084 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51324ECBA60005AB084 /* LazyView.swift */; };
|
||||
D020F51524ECBA60005AB084 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51324ECBA60005AB084 /* LazyView.swift */; };
|
||||
D02D86D924EF61E4004583CC /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86D724EF61E4004583CC /* StatusTableViewCell.swift */; };
|
||||
D02D86DA24EF61E4004583CC /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D02D86D824EF61E4004583CC /* StatusTableViewCell.xib */; };
|
||||
D02D86E424EF9848004583CC /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86E324EF9848004583CC /* TouchFallthroughTextView.swift */; };
|
||||
D02D86E624EF998B004583CC /* HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86E524EF998B004583CC /* HTML.swift */; };
|
||||
D02D86E724EF998B004583CC /* HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86E524EF998B004583CC /* HTML.swift */; };
|
||||
D02D86EC24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86EB24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift */; };
|
||||
D02D86ED24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86EB24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift */; };
|
||||
D02D86EF24EFB13A004583CC /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86EE24EFB13A004583CC /* Date+Extensions.swift */; };
|
||||
D02D86F024EFB13A004583CC /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D86EE24EFB13A004583CC /* Date+Extensions.swift */; };
|
||||
D02D870524EFBB79004583CC /* String+UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D870424EFBB79004583CC /* String+UIKitExtensions.swift */; };
|
||||
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */; };
|
||||
D03658D224EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */; };
|
||||
D03DF45B24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */; };
|
||||
D03DF45C24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */; };
|
||||
D042650824F058280096ED10 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D042650724F058280096ED10 /* Localizable.stringsdict */; };
|
||||
D042650924F058280096ED10 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D042650724F058280096ED10 /* Localizable.stringsdict */; };
|
||||
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
||||
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
||||
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
|
||||
|
@ -132,6 +144,11 @@
|
|||
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
||||
D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */; };
|
||||
D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */; };
|
||||
D0A2453724EF346800B07068 /* StatusListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2453624EF346800B07068 /* StatusListViewController.swift */; };
|
||||
D0A2453924EF364100B07068 /* StatusListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2453824EF364100B07068 /* StatusListView.swift */; };
|
||||
D0A2453F24EF55D000B07068 /* StatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2453E24EF55D000B07068 /* StatusViewModel.swift */; };
|
||||
D0A2454124EF563000B07068 /* StatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2454024EF563000B07068 /* StatusService.swift */; };
|
||||
D0A2454224EF563000B07068 /* StatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2454024EF563000B07068 /* StatusService.swift */; };
|
||||
D0A652AD24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */; };
|
||||
D0A652AE24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */; };
|
||||
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
||||
|
@ -142,8 +159,6 @@
|
|||
D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
||||
D0BEC94724CA22C400E864C4 /* StatusesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */; };
|
||||
D0BEC94824CA22C400E864C4 /* StatusesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */; };
|
||||
D0BEC94A24CA231200E864C4 /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* StatusesView.swift */; };
|
||||
D0BEC94B24CA231200E864C4 /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* StatusesView.swift */; };
|
||||
D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
|
||||
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
|
||||
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; };
|
||||
|
@ -187,6 +202,7 @@
|
|||
D0E5363024E5436C00FB1CE1 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */; };
|
||||
D0E5363124E5453E00FB1CE1 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */; };
|
||||
D0E5363224E5453F00FB1CE1 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */; };
|
||||
D0E900CE24F1F28A00B55F5A /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E900CD24F1F28A00B55F5A /* UIColor+Extensions.swift */; };
|
||||
D0EC8DC224DF7D9C00A08489 /* IdentityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */; };
|
||||
D0EC8DC324DF7D9C00A08489 /* IdentityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */; };
|
||||
D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
|
||||
|
@ -286,8 +302,16 @@
|
|||
D020F50D24ECA25F005AB084 /* ContextEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextEndpoint.swift; sourceTree = "<group>"; };
|
||||
D020F51024ECA309005AB084 /* MastodonContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonContext.swift; sourceTree = "<group>"; };
|
||||
D020F51324ECBA60005AB084 /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||
D02D86D724EF61E4004583CC /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D02D86D824EF61E4004583CC /* StatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D02D86E324EF9848004583CC /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
||||
D02D86E524EF998B004583CC /* HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTML.swift; sourceTree = "<group>"; };
|
||||
D02D86EB24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingUserInfoKey+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D02D86EE24EFB13A004583CC /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D02D870424EFBB79004583CC /* String+UIKitExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+UIKitExtensions.swift"; sourceTree = "<group>"; };
|
||||
D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||
D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletionEndpoint.swift; sourceTree = "<group>"; };
|
||||
D042650724F058280096ED10 /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
||||
D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -331,12 +355,15 @@
|
|||
D075817B24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = "<group>"; };
|
||||
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0A2453624EF346800B07068 /* StatusListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListViewController.swift; sourceTree = "<group>"; };
|
||||
D0A2453824EF364100B07068 /* StatusListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListView.swift; sourceTree = "<group>"; };
|
||||
D0A2453E24EF55D000B07068 /* StatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = "<group>"; };
|
||||
D0A2454024EF563000B07068 /* StatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusService.swift; sourceTree = "<group>"; };
|
||||
D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
|
||||
D0BEC93A24C96FD500E864C4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||
D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesViewModel.swift; sourceTree = "<group>"; };
|
||||
D0BEC94924CA231200E864C4 /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = "<group>"; };
|
||||
D0C963FA24CC359D003BD330 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
|
||||
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPreferences.swift; sourceTree = "<group>"; };
|
||||
|
@ -357,6 +384,7 @@
|
|||
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = "<group>"; };
|
||||
D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; };
|
||||
D0E900CD24F1F28A00B55F5A /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityService.swift; sourceTree = "<group>"; };
|
||||
D0EC8DC424DF842700A08489 /* KeychainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
|
||||
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretsService.swift; sourceTree = "<group>"; };
|
||||
|
@ -425,7 +453,11 @@
|
|||
D0159FA224DE955900E78478 /* CustomEmojiText.swift */,
|
||||
D0159F8C24DE743700E78478 /* IdentitiesView.swift */,
|
||||
D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */,
|
||||
D0A2453824EF364100B07068 /* StatusListView.swift */,
|
||||
D02D86D724EF61E4004583CC /* StatusTableViewCell.swift */,
|
||||
D02D86D824EF61E4004583CC /* StatusTableViewCell.xib */,
|
||||
D0159F8D24DE743700E78478 /* TabNavigationView.swift */,
|
||||
D02D86E324EF9848004583CC /* TouchFallthroughTextView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -485,17 +517,27 @@
|
|||
D019E6F224DF7C9E00697C7D /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D054951024EB101F008B00A5 /* Status List Services */,
|
||||
D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */,
|
||||
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */,
|
||||
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */,
|
||||
D0EC8DC424DF842700A08489 /* KeychainService.swift */,
|
||||
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */,
|
||||
D054951024EB101F008B00A5 /* Status List Services */,
|
||||
D0A2454024EF563000B07068 /* StatusService.swift */,
|
||||
D0EC8DD724E096C900A08489 /* UserNotificationService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D02D870024EFBAD5004583CC /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D02D870424EFBB79004583CC /* String+UIKitExtensions.swift */,
|
||||
D0E900CD24F1F28A00B55F5A /* UIColor+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D047FA7F24C3E21000AF17C5 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -543,7 +585,9 @@
|
|||
D047FA8E24C3E21200AF17C5 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D02D870024EFBAD5004583CC /* Extensions */,
|
||||
D047FA8F24C3E21200AF17C5 /* Info.plist */,
|
||||
D0A2453524EF344600B07068 /* View Controllers */,
|
||||
D0159F8024DE739500E78478 /* View Models */,
|
||||
D0159F7F24DE739000E78478 /* Views */,
|
||||
);
|
||||
|
@ -592,11 +636,12 @@
|
|||
D0ED1BD624CF94B200B4899C /* Application.swift */,
|
||||
D05494E924EA3F54008B00A5 /* Attachment.swift */,
|
||||
D05494EF24EA3FE5008B00A5 /* Card.swift */,
|
||||
D020F51024ECA309005AB084 /* MastodonContext.swift */,
|
||||
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
|
||||
D02D86E524EF998B004583CC /* HTML.swift */,
|
||||
D0666A4A24C6C37700F3F04B /* Identity.swift */,
|
||||
D0666A4D24C6C39600F3F04B /* Instance.swift */,
|
||||
D057426C24EA339300839EBA /* ListTimeline.swift */,
|
||||
D020F51024ECA309005AB084 /* MastodonContext.swift */,
|
||||
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
|
||||
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */,
|
||||
D05494E624EA3F1A008B00A5 /* Mention.swift */,
|
||||
|
@ -623,10 +668,19 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D06B491E24D3F7FE00642749 /* Localizable.strings */,
|
||||
D042650724F058280096ED10 /* Localizable.stringsdict */,
|
||||
);
|
||||
path = Localizations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0A2453524EF344600B07068 /* View Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0A2453624EF346800B07068 /* StatusListViewController.swift */,
|
||||
);
|
||||
path = "View Controllers";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0DB6EF024C5224F00D965FE /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -636,7 +690,6 @@
|
|||
D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */,
|
||||
D0091B6D24DD68090040E8D2 /* PreferencesView.swift */,
|
||||
D0BEC93A24C96FD500E864C4 /* RootView.swift */,
|
||||
D0BEC94924CA231200E864C4 /* StatusesView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -661,6 +714,7 @@
|
|||
D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */,
|
||||
D0BEC93724C9632800E864C4 /* RootViewModel.swift */,
|
||||
D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */,
|
||||
D0A2453E24EF55D000B07068 /* StatusViewModel.swift */,
|
||||
);
|
||||
path = "View Models";
|
||||
sourceTree = "<group>";
|
||||
|
@ -668,13 +722,15 @@
|
|||
D0DB6F1624C665B400D965FE /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D02D86EB24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift */,
|
||||
D0EC8DE724E21FEC00A08489 /* Data+Extensions.swift */,
|
||||
D02D86EE24EFB13A004583CC /* Date+Extensions.swift */,
|
||||
D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */,
|
||||
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */,
|
||||
D0159FA424DE989700E78478 /* NSMutableAttributedString+Extensions.swift */,
|
||||
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */,
|
||||
D081A40424D0F1A8001B016E /* String+Extensions.swift */,
|
||||
D065F53A24D3B33A00741304 /* View+Extensions.swift */,
|
||||
D0EC8DE724E21FEC00A08489 /* Data+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -904,6 +960,8 @@
|
|||
D054950124EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */,
|
||||
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||
D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */,
|
||||
D02D86DA24EF61E4004583CC /* StatusTableViewCell.xib in Resources */,
|
||||
D042650824F058280096ED10 /* Localizable.stringsdict in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -912,6 +970,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D054950224EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */,
|
||||
D042650924F058280096ED10 /* Localizable.stringsdict in Resources */,
|
||||
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||
D06B492024D3FB8000642749 /* Localizable.strings in Resources */,
|
||||
);
|
||||
|
@ -975,6 +1034,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D02D86E624EF998B004583CC /* HTML.swift in Sources */,
|
||||
D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
|
||||
D05494F724EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */,
|
||||
D054951B24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */,
|
||||
|
@ -987,6 +1047,7 @@
|
|||
D0BEC94724CA22C400E864C4 /* StatusesViewModel.swift in Sources */,
|
||||
D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||
D057426724E9FE1D00839EBA /* ContentDatabase.swift in Sources */,
|
||||
D02D86E424EF9848004583CC /* TouchFallthroughTextView.swift in Sources */,
|
||||
D054951524EB1053008B00A5 /* TimelineService.swift in Sources */,
|
||||
D019E6E924DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
|
||||
D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */,
|
||||
|
@ -995,6 +1056,7 @@
|
|||
D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
||||
D05494F024EA3FE5008B00A5 /* Card.swift in Sources */,
|
||||
D019E6E524DF72E700697C7D /* AccountEndpoint.swift in Sources */,
|
||||
D0A2453924EF364100B07068 /* StatusListView.swift in Sources */,
|
||||
D075817C24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
D020F51424ECBA60005AB084 /* LazyView.swift in Sources */,
|
||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||
|
@ -1008,8 +1070,12 @@
|
|||
D019E6ED24DF7BF300697C7D /* IdentityDatabase.swift in Sources */,
|
||||
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||
D019E6E724DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */,
|
||||
D02D870524EFBB79004583CC /* String+UIKitExtensions.swift in Sources */,
|
||||
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||
D02D86EC24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */,
|
||||
D0A2454124EF563000B07068 /* StatusService.swift in Sources */,
|
||||
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||
D0E900CE24F1F28A00B55F5A /* UIColor+Extensions.swift in Sources */,
|
||||
D0EC8DEE24E2704D00A08489 /* PushSubscriptionEndpoint.swift in Sources */,
|
||||
D0159F9324DE743700E78478 /* SecondaryNavigationView.swift in Sources */,
|
||||
D019E6E324DF72E700697C7D /* PreferencesEndpoint.swift in Sources */,
|
||||
|
@ -1017,13 +1083,15 @@
|
|||
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||
D05494FA24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */,
|
||||
D0A652AD24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */,
|
||||
D02D86D924EF61E4004583CC /* StatusTableViewCell.swift in Sources */,
|
||||
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||
D05494ED24EA3FA9008B00A5 /* Poll.swift in Sources */,
|
||||
D019E6D924DF728400697C7D /* MastodonDecoder.swift in Sources */,
|
||||
D052BBCA24D74C9200A80A7A /* MockUserDefaults.swift in Sources */,
|
||||
D0A2453F24EF55D000B07068 /* StatusViewModel.swift in Sources */,
|
||||
D02D86EF24EFB13A004583CC /* Date+Extensions.swift in Sources */,
|
||||
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||
D0BEC94A24CA231200E864C4 /* StatusesView.swift in Sources */,
|
||||
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||
D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */,
|
||||
|
@ -1039,6 +1107,7 @@
|
|||
D0EC8DE524E0B44500A08489 /* UserNotificationService.swift in Sources */,
|
||||
D0EC8DCB24DFA06700A08489 /* IdentitiesService.swift in Sources */,
|
||||
D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */,
|
||||
D0A2453724EF346800B07068 /* StatusListViewController.swift in Sources */,
|
||||
D057426A24EA32AC00839EBA /* Timeline.swift in Sources */,
|
||||
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||
D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||
|
@ -1088,6 +1157,7 @@
|
|||
D0ED1BD824CF94B200B4899C /* Application.swift in Sources */,
|
||||
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
||||
D0BEC94824CA22C400E864C4 /* StatusesViewModel.swift in Sources */,
|
||||
D02D86ED24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */,
|
||||
D0159FA624DE98F600E78478 /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||
D0EC8DC324DF7D9C00A08489 /* IdentityService.swift in Sources */,
|
||||
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||
|
@ -1114,9 +1184,9 @@
|
|||
D052BBCB24D74C9300A80A7A /* MockUserDefaults.swift in Sources */,
|
||||
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||
D0EC8DE924E21FEC00A08489 /* Data+Extensions.swift in Sources */,
|
||||
D0BEC94B24CA231200E864C4 /* StatusesView.swift in Sources */,
|
||||
D05494E524EA3EF7008B00A5 /* Tag.swift in Sources */,
|
||||
D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||
D02D86E724EF998B004583CC /* HTML.swift in Sources */,
|
||||
D0159F9B24DE748900E78478 /* SidebarNavigationViewModel.swift in Sources */,
|
||||
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||
D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||
|
@ -1138,6 +1208,7 @@
|
|||
D0091B6C24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||
D0091B6F24DD68090040E8D2 /* PreferencesView.swift in Sources */,
|
||||
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */,
|
||||
D02D86F024EFB13A004583CC /* Date+Extensions.swift in Sources */,
|
||||
D0EC8DE024E09D7000A08489 /* AppDelegate.swift in Sources */,
|
||||
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
||||
D019E6EA24DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
|
||||
|
@ -1154,6 +1225,7 @@
|
|||
D0EC8DEC24E26F1100A08489 /* PushSubscription.swift in Sources */,
|
||||
D03DF45C24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */,
|
||||
D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||
D0A2454224EF563000B07068 /* StatusService.swift in Sources */,
|
||||
D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
||||
D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
|
||||
D075817D24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
|
|
|
@ -8,7 +8,7 @@ import GRDB
|
|||
struct ContentDatabase {
|
||||
private let databaseQueue: DatabaseQueue
|
||||
|
||||
init(identityID: UUID, inMemory: Bool = false) throws {
|
||||
init(identityID: UUID, environment: AppEnvironment) throws {
|
||||
guard
|
||||
let documentsDirectory = NSSearchPathForDirectoriesInDomains(
|
||||
.documentDirectory,
|
||||
|
@ -16,7 +16,7 @@ struct ContentDatabase {
|
|||
.first
|
||||
else { throw DatabaseError.documentsDirectoryNotFound }
|
||||
|
||||
if inMemory {
|
||||
if environment.inMemoryContent {
|
||||
databaseQueue = DatabaseQueue()
|
||||
} else {
|
||||
databaseQueue = try DatabaseQueue(path: "\(documentsDirectory)/\(identityID.uuidString).sqlite3")
|
||||
|
@ -24,6 +24,7 @@ struct ContentDatabase {
|
|||
|
||||
try Self.migrate(databaseQueue)
|
||||
try Self.createTemporaryTables(databaseQueue)
|
||||
Self.attributedStringCache = environment.attributedStringCache
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,6 +80,8 @@ extension ContentDatabase {
|
|||
}
|
||||
|
||||
private extension ContentDatabase {
|
||||
static var attributedStringCache: AttributedStringCache?
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
static func migrate(_ writer: DatabaseWriter) throws {
|
||||
var migrator = DatabaseMigrator()
|
||||
|
@ -181,6 +184,16 @@ private extension ContentDatabase {
|
|||
}
|
||||
|
||||
extension Account: TableRecord, FetchableRecord, PersistableRecord {
|
||||
static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] {
|
||||
var userInfo = [CodingUserInfoKey: Any]()
|
||||
|
||||
if let attributedStringCache = ContentDatabase.attributedStringCache {
|
||||
userInfo[.attributedStringCache] = attributedStringCache
|
||||
}
|
||||
|
||||
return userInfo
|
||||
}
|
||||
|
||||
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||
MastodonDecoder()
|
||||
}
|
||||
|
@ -276,7 +289,7 @@ private struct StoredStatus: Codable, Hashable {
|
|||
let uri: String
|
||||
let createdAt: Date
|
||||
let accountId: String
|
||||
let content: String
|
||||
let content: HTML
|
||||
let visibility: Status.Visibility
|
||||
let sensitive: Bool
|
||||
let spoilerText: String
|
||||
|
@ -354,6 +367,16 @@ private extension StoredStatus {
|
|||
}
|
||||
|
||||
extension StoredStatus: TableRecord, FetchableRecord, PersistableRecord {
|
||||
static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] {
|
||||
var userInfo = [CodingUserInfoKey: Any]()
|
||||
|
||||
if let attributedStringCache = ContentDatabase.attributedStringCache {
|
||||
userInfo[.attributedStringCache] = attributedStringCache
|
||||
}
|
||||
|
||||
return userInfo
|
||||
}
|
||||
|
||||
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||
MastodonDecoder()
|
||||
}
|
||||
|
|
9
Shared/Extensions/CodingUserInfoKey+Extensions.swift
Normal file
9
Shared/Extensions/CodingUserInfoKey+Extensions.swift
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias AttributedStringCache = NSCache<NSString, NSAttributedString>
|
||||
|
||||
extension CodingUserInfoKey {
|
||||
static let attributedStringCache = CodingUserInfoKey(rawValue: "com.metabolist.metatext.attributed-string-cache")!
|
||||
}
|
81
Shared/Extensions/Date+Extensions.swift
Normal file
81
Shared/Extensions/Date+Extensions.swift
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Date {
|
||||
var timeAgo: String? {
|
||||
let calendar = Calendar.autoupdatingCurrent
|
||||
let now = Date()
|
||||
|
||||
if
|
||||
let oneMinuteAgo = calendar.date(byAdding: DateComponents(minute: -1), to: now),
|
||||
oneMinuteAgo < self {
|
||||
Self.abbreviatedDateComponentsFormatter.allowedUnits = [.second]
|
||||
} else if
|
||||
let oneHourAgo = calendar.date(byAdding: DateComponents(hour: -1), to: now),
|
||||
oneHourAgo < self {
|
||||
Self.abbreviatedDateComponentsFormatter.allowedUnits = [.minute]
|
||||
} else if
|
||||
let oneDayAgo = calendar.date(byAdding: DateComponents(day: -1), to: now),
|
||||
oneDayAgo < self {
|
||||
Self.abbreviatedDateComponentsFormatter.allowedUnits = [.hour]
|
||||
} else if
|
||||
let oneWeekAgo = calendar.date(byAdding: DateComponents(weekOfMonth: -1), to: now),
|
||||
oneWeekAgo < self {
|
||||
Self.abbreviatedDateComponentsFormatter.allowedUnits = [.day]
|
||||
} else {
|
||||
return Date.shortDateStyleRelativeDateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
return Self.abbreviatedDateComponentsFormatter.string(from: self, to: now)
|
||||
}
|
||||
|
||||
var fullUnitTimeUntil: String? {
|
||||
let calendar = Calendar.autoupdatingCurrent
|
||||
let now = Date()
|
||||
|
||||
if
|
||||
let oneDayFromNow = calendar.date(byAdding: DateComponents(day: 1), to: now),
|
||||
self > oneDayFromNow {
|
||||
Self.fullDateComponentsFormatter.allowedUnits = [.day]
|
||||
} else if
|
||||
let oneHourFromNow = calendar.date(byAdding: DateComponents(hour: 1), to: now),
|
||||
self > oneHourFromNow {
|
||||
Self.fullDateComponentsFormatter.allowedUnits = [.hour]
|
||||
} else if
|
||||
let oneMinuteFromNow = calendar.date(byAdding: DateComponents(minute: 1), to: now),
|
||||
self > oneMinuteFromNow {
|
||||
Self.fullDateComponentsFormatter.allowedUnits = [.minute]
|
||||
} else {
|
||||
Self.fullDateComponentsFormatter.allowedUnits = [.second]
|
||||
}
|
||||
|
||||
return Self.fullDateComponentsFormatter.string(from: now, to: self)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Date {
|
||||
private static let abbreviatedDateComponentsFormatter: DateComponentsFormatter = {
|
||||
let dateComponentsFormatter = DateComponentsFormatter()
|
||||
|
||||
dateComponentsFormatter.unitsStyle = .abbreviated
|
||||
|
||||
return dateComponentsFormatter
|
||||
}()
|
||||
|
||||
private static let fullDateComponentsFormatter: DateComponentsFormatter = {
|
||||
let dateComponentsFormatter = DateComponentsFormatter()
|
||||
|
||||
dateComponentsFormatter.unitsStyle = .full
|
||||
|
||||
return dateComponentsFormatter
|
||||
}()
|
||||
|
||||
private static let shortDateStyleRelativeDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
dateFormatter.dateStyle = .short
|
||||
|
||||
return dateFormatter
|
||||
}()
|
||||
}
|
|
@ -26,6 +26,15 @@
|
|||
"preferences.notification-types.reblog" = "Reblog";
|
||||
"preferences.notification-types.mention" = "Mention";
|
||||
"preferences.notification-types.poll" = "Poll";
|
||||
"status.reblogged-by" = "%@ boosted";
|
||||
"status.pinned-post" = "Pinned post";
|
||||
"status.show-more" = "Show More";
|
||||
"status.show-less" = "Show Less";
|
||||
"status.poll.vote" = "Vote";
|
||||
"status.poll.participation-count" = "%ld people";
|
||||
"status.poll.time-left" = "%@ left";
|
||||
"status.poll.refresh" = "Refresh";
|
||||
"status.poll.closed" = "Closed";
|
||||
"status.visibility.public" = "Public";
|
||||
"status.visibility.unlisted" = "Unlisted";
|
||||
"status.visibility.private" = "Private";
|
||||
|
|
54
Shared/Localizations/Localizable.stringsdict
Normal file
54
Shared/Localizations/Localizable.stringsdict
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>status.reblogs-count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reblogs@</string>
|
||||
<key>reblogs</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>%ld Boost</string>
|
||||
<key>other</key>
|
||||
<string>%ld Boosts</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>status.favorites-count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@favorites@</string>
|
||||
<key>favorites</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>%ld Favorite</string>
|
||||
<key>other</key>
|
||||
<string>%ld Favorites</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>account.followers-count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@followers@</string>
|
||||
<key>followers</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>%ld Follower</string>
|
||||
<key>other</key>
|
||||
<string>%ld Followers</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -5,7 +5,7 @@ import Foundation
|
|||
struct Account: Codable, Hashable {
|
||||
struct Field: Codable, Hashable {
|
||||
let name: String
|
||||
let value: String
|
||||
let value: HTML
|
||||
let verifiedAt: Date?
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ struct Account: Codable, Hashable {
|
|||
let followersCount: Int
|
||||
let followingCount: Int
|
||||
let statusesCount: Int
|
||||
let note: String
|
||||
let note: HTML
|
||||
let url: URL
|
||||
let avatar: URL
|
||||
let avatarStatic: URL
|
||||
|
|
|
@ -8,6 +8,7 @@ struct AppEnvironment {
|
|||
let keychainServiceType: KeychainService.Type
|
||||
let userDefaults: UserDefaults
|
||||
let inMemoryContent: Bool
|
||||
let attributedStringCache = AttributedStringCache()
|
||||
}
|
||||
|
||||
extension AppEnvironment {
|
||||
|
|
117
Shared/Model/HTML.swift
Normal file
117
Shared/Model/HTML.swift
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HTML: Hashable {
|
||||
let raw: String
|
||||
let attributed: NSAttributedString
|
||||
}
|
||||
|
||||
extension HTML: Codable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let attributedStringCache = decoder.userInfo[.attributedStringCache] as? AttributedStringCache
|
||||
|
||||
raw = try container.decode(String.self)
|
||||
|
||||
if let attributed = attributedStringCache?.object(forKey: raw as NSString) {
|
||||
self.attributed = attributed
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
attributed = HTMLParser(string: raw).parse()
|
||||
attributedStringCache?.setObject(attributed, forKey: raw as NSString)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(raw)
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||
|
||||
private class HTMLParser: NSObject {
|
||||
private struct Link: Hashable {
|
||||
let href: URL
|
||||
let location: Int
|
||||
var length = 0
|
||||
}
|
||||
|
||||
private let rawString: String
|
||||
private let parser: XMLParser
|
||||
private let parseStopColumn: Int
|
||||
private var constructedString = ""
|
||||
private var attributesStack = [[String: String]]()
|
||||
private var currentLink: Link?
|
||||
private var links = Set<Link>()
|
||||
private static let containerTag = "com.metabolist.metatext.container-tag"
|
||||
private static let openingContainerTag = "<\(containerTag)>"
|
||||
private static let closingContainerTag = "</\(containerTag)>"
|
||||
|
||||
init(string: String) {
|
||||
rawString = Self.openingContainerTag + string + Self.closingContainerTag
|
||||
parser = XMLParser(data: Data(rawString.utf8))
|
||||
parseStopColumn = rawString.count - Self.closingContainerTag.count
|
||||
|
||||
super.init()
|
||||
|
||||
parser.delegate = self
|
||||
}
|
||||
|
||||
func parse() -> NSAttributedString {
|
||||
parser.parse()
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: constructedString)
|
||||
|
||||
for link in links {
|
||||
attributedString.addAttribute(.link,
|
||||
value: link.href,
|
||||
range: .init(location: link.location, length: link.length))
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLParser: XMLParserDelegate {
|
||||
func parser(_ parser: XMLParser,
|
||||
didStartElement elementName: String,
|
||||
namespaceURI: String?,
|
||||
qualifiedName qName: String?,
|
||||
attributes attributeDict: [String: String] = [:]) {
|
||||
attributesStack.append(attributeDict)
|
||||
|
||||
if elementName == "a", let hrefString = attributeDict["href"], let href = URL(string: hrefString) {
|
||||
currentLink = Link(href: href, location: constructedString.count)
|
||||
} else if elementName == "br" {
|
||||
constructedString.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser,
|
||||
didEndElement elementName: String,
|
||||
namespaceURI: String?,
|
||||
qualifiedName qName: String?) {
|
||||
let attributes = attributesStack.removeLast()
|
||||
|
||||
if attributes["class"] == "ellipsis" {
|
||||
constructedString.append("…")
|
||||
}
|
||||
|
||||
if elementName == "a", var link = currentLink {
|
||||
link.length = constructedString.count - link.location
|
||||
links.insert(link)
|
||||
} else if elementName == "p", parser.columnNumber < parseStopColumn {
|
||||
constructedString.append("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, foundCharacters string: String) {
|
||||
if attributesStack.last?["class"] != "invisible" {
|
||||
constructedString.append(string)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ class Status: Codable, Identifiable {
|
|||
let uri: String
|
||||
let createdAt: Date
|
||||
let account: Account
|
||||
let content: String
|
||||
let content: HTML
|
||||
let visibility: Visibility
|
||||
let sensitive: Bool
|
||||
let spoilerText: String
|
||||
|
@ -49,7 +49,7 @@ class Status: Codable, Identifiable {
|
|||
uri: String,
|
||||
createdAt: Date,
|
||||
account: Account,
|
||||
content: String,
|
||||
content: HTML,
|
||||
visibility: Status.Visibility,
|
||||
sensitive: Bool,
|
||||
spoilerText: String,
|
||||
|
@ -106,6 +106,12 @@ class Status: Codable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
extension Status {
|
||||
var displayStatus: Status {
|
||||
reblog ?? self
|
||||
}
|
||||
}
|
||||
|
||||
extension Status: Hashable {
|
||||
static func == (lhs: Status, rhs: Status) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
|
|
|
@ -10,7 +10,7 @@ class HTTPClient {
|
|||
private let session: Session
|
||||
private let decoder: DataDecoder
|
||||
|
||||
init(session: Session, decoder: DataDecoder = JSONDecoder()) {
|
||||
init(session: Session, decoder: DataDecoder) {
|
||||
self.session = session
|
||||
self.decoder = decoder
|
||||
}
|
||||
|
|
|
@ -7,8 +7,12 @@ class MastodonClient: HTTPClient {
|
|||
var instanceURL: URL?
|
||||
var accessToken: String?
|
||||
|
||||
init(session: Session) {
|
||||
super.init(session: session, decoder: MastodonDecoder())
|
||||
required init(environment: AppEnvironment) {
|
||||
let decoder = MastodonDecoder()
|
||||
|
||||
decoder.userInfo[.attributedStringCache] = environment.attributedStringCache
|
||||
|
||||
super.init(session: environment.session, decoder: decoder)
|
||||
}
|
||||
|
||||
override func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> {
|
||||
|
|
|
@ -9,7 +9,7 @@ struct AuthenticationService {
|
|||
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
||||
|
||||
init(environment: AppEnvironment) {
|
||||
networkClient = MastodonClient(session: environment.session)
|
||||
networkClient = MastodonClient(environment: environment)
|
||||
webAuthSessionType = environment.webAuthSessionType
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ extension IdentitiesService {
|
|||
|
||||
func deleteIdentity(_ identity: Identity) -> AnyPublisher<Void, Error> {
|
||||
let secretsService = SecretsService(identityID: identity.id, keychainService: environment.keychainServiceType)
|
||||
let networkClient = MastodonClient(session: environment.session)
|
||||
let networkClient = MastodonClient(environment: environment)
|
||||
|
||||
networkClient.instanceURL = identity.url
|
||||
|
||||
|
|
|
@ -34,11 +34,11 @@ class IdentityService {
|
|||
secretsService = SecretsService(
|
||||
identityID: identityID,
|
||||
keychainService: environment.keychainServiceType)
|
||||
networkClient = MastodonClient(session: environment.session)
|
||||
networkClient = MastodonClient(environment: environment)
|
||||
networkClient.instanceURL = identity.url
|
||||
networkClient.accessToken = try? secretsService.item(.accessToken)
|
||||
|
||||
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
|
||||
contentDatabase = try ContentDatabase(identityID: identityID, environment: environment)
|
||||
|
||||
observation.catch { [weak self] error -> Empty<Identity, Never> in
|
||||
self?.observationErrorsInput.send(error)
|
||||
|
|
|
@ -34,6 +34,32 @@ struct ContextService {
|
|||
extension ContextService: StatusListService {
|
||||
var contextParent: Status? { status }
|
||||
|
||||
func isReplyInContext(status: Status) -> Bool {
|
||||
let flatContext = flattenedContext()
|
||||
|
||||
guard
|
||||
let index = flatContext.firstIndex(where: { $0.id == status.id }),
|
||||
index > 0
|
||||
else { return false }
|
||||
|
||||
let previousStatus = flatContext[index - 1]
|
||||
|
||||
return previousStatus.id != contextParent?.id && status.inReplyToId == previousStatus.id
|
||||
}
|
||||
|
||||
func hasReplyFollowing(status: Status) -> Bool {
|
||||
let flatContext = flattenedContext()
|
||||
|
||||
guard
|
||||
let index = flatContext.firstIndex(where: { $0.id == status.id }),
|
||||
flatContext.count > index + 1
|
||||
else { return false }
|
||||
|
||||
let nextStatus = flatContext[index + 1]
|
||||
|
||||
return status.id != contextParent?.id && nextStatus.inReplyToId == status.id
|
||||
}
|
||||
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error> {
|
||||
networkClient.request(ContextEndpoint.context(id: status.id))
|
||||
.handleEvents(receiveOutput: context.send)
|
||||
|
@ -42,7 +68,17 @@ extension ContextService: StatusListService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func statusService(status: Status) -> StatusService {
|
||||
StatusService(status: status, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func contextService(status: Status) -> ContextService {
|
||||
ContextService(status: status, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
ContextService(status: status.displayStatus, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ContextService {
|
||||
func flattenedContext() -> [Status] {
|
||||
context.value.ancestors + [status] + context.value.descendants
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,20 @@ import Combine
|
|||
protocol StatusListService {
|
||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||
var contextParent: Status? { get }
|
||||
func isPinned(status: Status) -> Bool
|
||||
func isReplyInContext(status: Status) -> Bool
|
||||
func hasReplyFollowing(status: Status) -> Bool
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error>
|
||||
func statusService(status: Status) -> StatusService
|
||||
func contextService(status: Status) -> ContextService
|
||||
}
|
||||
|
||||
extension StatusListService {
|
||||
var contextParent: Status? { nil }
|
||||
|
||||
func isPinned(status: Status) -> Bool { false }
|
||||
|
||||
func isReplyInContext(status: Status) -> Bool { false }
|
||||
|
||||
func hasReplyFollowing(status: Status) -> Bool { false }
|
||||
}
|
||||
|
|
|
@ -28,7 +28,11 @@ extension TimelineService: StatusListService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func statusService(status: Status) -> StatusService {
|
||||
StatusService(status: status, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func contextService(status: Status) -> ContextService {
|
||||
ContextService(status: status, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
ContextService(status: status.displayStatus, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
||||
|
|
16
Shared/Services/StatusService.swift
Normal file
16
Shared/Services/StatusService.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct StatusService {
|
||||
let status: Status
|
||||
private let networkClient: MastodonClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
|
||||
init(status: Status, networkClient: MastodonClient, contentDatabase: ContentDatabase) {
|
||||
self.status = status
|
||||
self.networkClient = networkClient
|
||||
self.contentDatabase = contentDatabase
|
||||
}
|
||||
}
|
112
Shared/View Models/StatusViewModel.swift
Normal file
112
Shared/View Models/StatusViewModel.swift
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct StatusViewModel {
|
||||
let content: NSAttributedString
|
||||
let contentEmoji: [Emoji]
|
||||
let displayName: String
|
||||
let displayNameEmoji: [Emoji]
|
||||
let spoilerText: String
|
||||
let isReblog: Bool
|
||||
let rebloggedByDisplayName: String
|
||||
let rebloggedByDisplayNameEmoji: [Emoji]
|
||||
let pollOptionTitles: [String]
|
||||
let pollEmoji: [Emoji]
|
||||
var isPinned = false
|
||||
var isContextParent = false
|
||||
var isReplyInContext = false
|
||||
var hasReplyFollowing = false
|
||||
var sensitiveContentToggled = false
|
||||
|
||||
private let statusService: StatusService
|
||||
|
||||
init(statusService: StatusService) {
|
||||
self.statusService = statusService
|
||||
content = statusService.status.displayStatus.content.attributed
|
||||
contentEmoji = statusService.status.displayStatus.emojis
|
||||
displayName = statusService.status.displayStatus.account.displayName == ""
|
||||
? statusService.status.displayStatus.account.username
|
||||
: statusService.status.displayStatus.account.displayName
|
||||
displayNameEmoji = statusService.status.displayStatus.account.emojis
|
||||
spoilerText = statusService.status.displayStatus.spoilerText
|
||||
isReblog = statusService.status.reblog != nil
|
||||
rebloggedByDisplayName = statusService.status.account.displayName == ""
|
||||
? statusService.status.account.username
|
||||
: statusService.status.account.displayName
|
||||
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
||||
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
|
||||
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusViewModel {
|
||||
var shouldDisplaySensitiveContent: Bool {
|
||||
if statusService.status.displayStatus.sensitive {
|
||||
return sensitiveContentToggled
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var accountName: String { "@" + statusService.status.displayStatus.account.acct }
|
||||
|
||||
var avatarURL: URL { statusService.status.displayStatus.account.avatar }
|
||||
|
||||
var time: String? { statusService.status.displayStatus.createdAt.timeAgo }
|
||||
|
||||
var contextParentTime: String {
|
||||
Self.contextParentDateFormatter.string(from: statusService.status.displayStatus.createdAt)
|
||||
}
|
||||
|
||||
var applicationName: String? { statusService.status.displayStatus.application?.name }
|
||||
|
||||
var applicationURL: URL? {
|
||||
guard let website = statusService.status.displayStatus.application?.website else { return nil }
|
||||
|
||||
return URL(string: website)
|
||||
}
|
||||
|
||||
var repliesCount: Int { statusService.status.displayStatus.repliesCount }
|
||||
|
||||
var reblogsCount: Int { statusService.status.displayStatus.reblogsCount }
|
||||
|
||||
var favoritesCount: Int { statusService.status.displayStatus.favouritesCount }
|
||||
|
||||
var reblogged: Bool { statusService.status.displayStatus.reblogged ?? false }
|
||||
|
||||
var favorited: Bool { statusService.status.displayStatus.favourited ?? false }
|
||||
|
||||
var sensitive: Bool { statusService.status.displayStatus.sensitive }
|
||||
|
||||
var sharingURL: URL? { statusService.status.displayStatus.url }
|
||||
|
||||
var cardURL: URL? { statusService.status.displayStatus.card?.url }
|
||||
|
||||
var cardTitle: String? { statusService.status.displayStatus.card?.title }
|
||||
|
||||
var cardDescription: String? { statusService.status.displayStatus.card?.description }
|
||||
|
||||
var cardImageURL: URL? { statusService.status.displayStatus.card?.image }
|
||||
|
||||
var canBeReblogged: Bool {
|
||||
switch statusService.status.displayStatus.visibility {
|
||||
case .direct, .private:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusViewModel {
|
||||
private static let contextParentDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .short
|
||||
|
||||
return dateFormatter
|
||||
}()
|
||||
}
|
|
@ -7,15 +7,15 @@ class StatusesViewModel: ObservableObject {
|
|||
@Published private(set) var statusSections = [[Status]]()
|
||||
@Published var alertItem: AlertItem?
|
||||
@Published private(set) var loading = false
|
||||
let scrollToStatusID: AnyPublisher<String, Never>
|
||||
let scrollToStatus: AnyPublisher<Status, Never>
|
||||
private let statusListService: StatusListService
|
||||
private let scrollToStatusIDInput = PassthroughSubject<String, Never>()
|
||||
private let scrollToStatusInput = PassthroughSubject<Status, Never>()
|
||||
private var hasScrolledToParentAfterContextLoad = false
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(statusListService: StatusListService) {
|
||||
self.statusListService = statusListService
|
||||
scrollToStatusID = scrollToStatusIDInput.eraseToAnyPublisher()
|
||||
scrollToStatus = scrollToStatusInput.eraseToAnyPublisher()
|
||||
|
||||
statusListService.statusSections
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
|
@ -30,7 +30,7 @@ class StatusesViewModel: ObservableObject {
|
|||
!($0.first ?? []).isEmpty || !(($0.last ?? []).isEmpty),
|
||||
!self.hasScrolledToParentAfterContextLoad {
|
||||
self.hasScrolledToParentAfterContextLoad = true
|
||||
self.scrollToStatusIDInput.send(contextParent.id)
|
||||
self.scrollToStatusInput.send(contextParent)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
@ -50,7 +50,25 @@ extension StatusesViewModel {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func statusViewModel(status: Status) -> StatusViewModel {
|
||||
var statusViewModel = Self.viewModelCache[status]
|
||||
?? StatusViewModel(statusService: statusListService.statusService(status: status))
|
||||
|
||||
statusViewModel.isContextParent = status == contextParent
|
||||
statusViewModel.isPinned = statusListService.isPinned(status: status)
|
||||
statusViewModel.isReplyInContext = statusListService.isReplyInContext(status: status)
|
||||
statusViewModel.hasReplyFollowing = statusListService.hasReplyFollowing(status: status)
|
||||
|
||||
Self.viewModelCache[status] = statusViewModel
|
||||
|
||||
return statusViewModel
|
||||
}
|
||||
|
||||
func contextViewModel(status: Status) -> StatusesViewModel {
|
||||
StatusesViewModel(statusListService: statusListService.contextService(status: status))
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusesViewModel {
|
||||
static var viewModelCache = [Status: StatusViewModel]()
|
||||
}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StatusesView: View {
|
||||
@StateObject var viewModel: StatusesViewModel
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { scrollViewProxy in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(Array(zip(viewModel.statusSections.indices, viewModel.statusSections)),
|
||||
id: \.0) { _, statuses in
|
||||
ForEach(statuses) { status in
|
||||
if status == viewModel.contextParent {
|
||||
statusView(status: status)
|
||||
} else {
|
||||
NavigationLink(destination:
|
||||
LazyView(StatusesView(viewModel:
|
||||
viewModel.contextViewModel(status: status)))) {
|
||||
statusView(status: status)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
if viewModel.loading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(viewModel.scrollToStatusID.receive(on: DispatchQueue.main)) { id in
|
||||
withAnimation {
|
||||
scrollViewProxy.scrollTo(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { viewModel.request() }
|
||||
.alertItem($viewModel.alertItem)
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusesView {
|
||||
func statusView(status: Status) -> some View {
|
||||
Text(status.content)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct StatusesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
StatusesView(viewModel: .development)
|
||||
}
|
||||
}
|
||||
#endif
|
24
iOS/Extensions/String+UIKitExtensions.swift
Normal file
24
iOS/Extensions/String+UIKitExtensions.swift
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension String {
|
||||
func countEmphasizedAttributedString(count: Int, highlighted: Bool = false) -> NSAttributedString {
|
||||
let countRange = (self as NSString).range(of: String.localizedStringWithFormat("%ld", count))
|
||||
|
||||
let attributed = NSMutableAttributedString(
|
||||
string: self,
|
||||
attributes: [
|
||||
.font: UIFont.preferredFont(forTextStyle: .body),
|
||||
.foregroundColor: highlighted ? UIColor.tertiaryLabel : UIColor.secondaryLabel
|
||||
])
|
||||
attributed.addAttributes(
|
||||
[
|
||||
.font: UIFont.preferredFont(forTextStyle: .headline),
|
||||
.foregroundColor: highlighted ? UIColor.secondaryLabel : UIColor.label
|
||||
],
|
||||
range: countRange)
|
||||
|
||||
return attributed
|
||||
}
|
||||
}
|
12
iOS/Extensions/UIColor+Extensions.swift
Normal file
12
iOS/Extensions/UIColor+Extensions.swift
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
|
||||
UIGraphicsImageRenderer(size: size).image { context in
|
||||
self.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
}
|
136
iOS/View Controllers/StatusListViewController.swift
Normal file
136
iOS/View Controllers/StatusListViewController.swift
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class StatusListViewController: UITableViewController {
|
||||
private let viewModel: StatusesViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var cellHeightCaches = [CGFloat: [Status: CGFloat]]()
|
||||
|
||||
private lazy var dataSource: UITableViewDiffableDataSource<Int, Status> = {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, status in
|
||||
guard
|
||||
let self = self,
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: String(describing: StatusTableViewCell.self),
|
||||
for: indexPath) as? StatusTableViewCell
|
||||
else { return nil }
|
||||
|
||||
let statusViewModel = self.viewModel.statusViewModel(status: status)
|
||||
|
||||
cell.viewModel = statusViewModel
|
||||
cell.delegate = self
|
||||
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
|
||||
init(viewModel: StatusesViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
for cellClass in [StatusTableViewCell.self] {
|
||||
let classString = String(describing: cellClass)
|
||||
tableView.register(
|
||||
UINib(nibName: classString, bundle: nil),
|
||||
forCellReuseIdentifier: classString)
|
||||
}
|
||||
|
||||
tableView.dataSource = dataSource
|
||||
tableView.cellLayoutMarginsFollowReadableWidth = true
|
||||
tableView.separatorInset = .zero
|
||||
|
||||
viewModel.$statusSections.map { $0.snapshot() }
|
||||
.sink { [weak self] in self?.dataSource.apply($0, animatingDifferences: false) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.scrollToStatus
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in
|
||||
guard
|
||||
let self = self,
|
||||
let indexPath = self.dataSource.indexPath(for: $0)
|
||||
else { return }
|
||||
|
||||
self.tableView.scrollToRow(at: indexPath, at: .none, animated: true)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
viewModel.request()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView,
|
||||
willDisplay cell: UITableViewCell,
|
||||
forRowAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
var heightCache = cellHeightCaches[tableView.frame.width] ?? [Status: CGFloat]()
|
||||
|
||||
heightCache[item] = cell.frame.height
|
||||
cellHeightCaches[tableView.frame.width] = heightCache
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
|
||||
|
||||
return cellHeightCaches[tableView.frame.width]?[item] ?? UITableView.automaticDimension
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
viewModel.statusSections[indexPath.section][indexPath.row] != viewModel.contextParent
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let status = viewModel.statusSections[indexPath.section][indexPath.row]
|
||||
|
||||
navigationController?.pushViewController(
|
||||
StatusListViewController(viewModel: viewModel.contextViewModel(status: status)),
|
||||
animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusListViewController: StatusTableViewCellDelegate {
|
||||
func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) {
|
||||
guard let url = cell.viewModel.sharingURL else { return }
|
||||
|
||||
share(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusListViewController {
|
||||
func share(url: URL) {
|
||||
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||
|
||||
present(activityViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == [Status] {
|
||||
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Status> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, Status>()
|
||||
|
||||
let sections = [Int](0..<count)
|
||||
|
||||
snapshot.appendSections(sections)
|
||||
|
||||
for section in sections {
|
||||
snapshot.appendItems(self[section], toSection: section)
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
}
|
15
iOS/Views/StatusListView.swift
Normal file
15
iOS/Views/StatusListView.swift
Normal file
|
@ -0,0 +1,15 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StatusListView: UIViewControllerRepresentable {
|
||||
let viewModel: StatusesViewModel
|
||||
|
||||
func makeUIViewController(context: Context) -> StatusListViewController {
|
||||
StatusListViewController(viewModel: viewModel)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: StatusListViewController, context: Context) {
|
||||
|
||||
}
|
||||
}
|
367
iOS/Views/StatusTableViewCell.swift
Normal file
367
iOS/Views/StatusTableViewCell.swift
Normal file
|
@ -0,0 +1,367 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
|
||||
protocol StatusTableViewCellDelegate: class {
|
||||
func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell)
|
||||
}
|
||||
|
||||
class StatusTableViewCell: UITableViewCell {
|
||||
@IBOutlet weak var metaIcon: UIImageView!
|
||||
@IBOutlet weak var metaLabel: UILabel!
|
||||
@IBOutlet weak var contentTextView: TouchFallthroughTextView!
|
||||
@IBOutlet weak var avatarButton: UIButton!
|
||||
@IBOutlet weak var avatarImageView: AnimatedImageView!
|
||||
@IBOutlet weak var displayNameLabel: UILabel!
|
||||
@IBOutlet weak var accountLabel: UILabel!
|
||||
@IBOutlet weak var timeLabel: UILabel!
|
||||
@IBOutlet weak var spoilerTextLabel: UILabel!
|
||||
@IBOutlet weak var toggleSensitiveContentButton: UIButton!
|
||||
@IBOutlet weak var replyButton: UIButton!
|
||||
@IBOutlet weak var reblogButton: UIButton!
|
||||
@IBOutlet weak var favoriteButton: UIButton!
|
||||
@IBOutlet weak var shareButton: UIButton!
|
||||
@IBOutlet weak var cardView: UIView!
|
||||
@IBOutlet weak var cardImageView: UIImageView!
|
||||
@IBOutlet weak var cardTitleLabel: UILabel!
|
||||
@IBOutlet weak var cardDescriptionLabel: UILabel!
|
||||
@IBOutlet weak var cardURLLabel: UILabel!
|
||||
@IBOutlet weak var cardButton: UIButton!
|
||||
@IBOutlet weak var sensitiveContentView: UIStackView!
|
||||
@IBOutlet weak var hasReplyFollowingView: UIView!
|
||||
@IBOutlet weak var inReplyToView: UIView!
|
||||
@IBOutlet weak var avatarReplyContextView: UIView!
|
||||
@IBOutlet weak var nameDateView: UIStackView!
|
||||
@IBOutlet weak var contextParentAvatarNameView: UIStackView!
|
||||
@IBOutlet weak var contextParentAvatarImageView: AnimatedImageView!
|
||||
@IBOutlet weak var contextParentAvatarButton: UIButton!
|
||||
@IBOutlet weak var contextParentDisplayNameLabel: UILabel!
|
||||
@IBOutlet weak var contextParentAccountLabel: UILabel!
|
||||
@IBOutlet weak var actionButtonsView: UIStackView!
|
||||
@IBOutlet weak var contextParentReplyButton: UIButton!
|
||||
@IBOutlet weak var contextParentReblogButton: UIButton!
|
||||
@IBOutlet weak var contextParentFavoriteButton: UIButton!
|
||||
@IBOutlet weak var contextParentShareButton: UIButton!
|
||||
@IBOutlet weak var contextParentActionsButton: UIButton!
|
||||
@IBOutlet weak var contextParentTimeLabel: UILabel!
|
||||
@IBOutlet weak var timeApplicationDividerView: UILabel!
|
||||
@IBOutlet weak var applicationButton: UIButton!
|
||||
@IBOutlet weak var contextParentRebloggedByButton: UIButton!
|
||||
@IBOutlet weak var contextParentFavoritedByButton: UIButton!
|
||||
@IBOutlet weak var contextParentItems: UIStackView!
|
||||
@IBOutlet weak var contextParentRebloggedByFavoritedByView: UIStackView!
|
||||
@IBOutlet weak var contextParentRebloggedByFavoritedBySeparator: UIView!
|
||||
|
||||
weak var delegate: StatusTableViewCellDelegate?
|
||||
|
||||
@IBOutlet private var separatorConstraints: [NSLayoutConstraint]!
|
||||
|
||||
var viewModel: StatusViewModel! {
|
||||
didSet {
|
||||
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
||||
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
||||
let contentFont = UIFont.preferredFont(forTextStyle: viewModel.isContextParent ? .title3 : .callout)
|
||||
|
||||
contentTextView.shouldFallthrough = !viewModel.isContextParent
|
||||
avatarReplyContextView.isHidden = viewModel.isContextParent
|
||||
nameDateView.isHidden = viewModel.isContextParent
|
||||
contextParentAvatarNameView.isHidden = !viewModel.isContextParent
|
||||
actionButtonsView.isHidden = viewModel.isContextParent
|
||||
contextParentItems.isHidden = !viewModel.isContextParent
|
||||
|
||||
let avatarImageView: UIImageView
|
||||
let displayNameLabel: UILabel
|
||||
let accountLabel: UILabel
|
||||
|
||||
if viewModel.isContextParent {
|
||||
avatarImageView = contextParentAvatarImageView
|
||||
displayNameLabel = contextParentDisplayNameLabel
|
||||
accountLabel = contextParentAccountLabel
|
||||
} else {
|
||||
avatarImageView = self.avatarImageView
|
||||
displayNameLabel = self.displayNameLabel
|
||||
accountLabel = self.accountLabel
|
||||
}
|
||||
|
||||
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
||||
mutableContent.removeAttribute(.font, range: contentRange)
|
||||
mutableContent.addAttributes(
|
||||
[.font: contentFont as Any,
|
||||
.foregroundColor: UIColor.label],
|
||||
range: contentRange)
|
||||
mutableContent.insert(emojis: viewModel.contentEmoji) { [weak self] in
|
||||
self?.contentTextView.setNeedsDisplay()
|
||||
}
|
||||
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
|
||||
contentTextView.attributedText = mutableContent
|
||||
contentTextView.isHidden = contentTextView.text == ""
|
||||
mutableDisplayName.insert(emojis: viewModel.displayNameEmoji) { displayNameLabel.setNeedsDisplay() }
|
||||
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||
displayNameLabel.attributedText = mutableDisplayName
|
||||
mutableSpoilerText.insert(emojis: viewModel.contentEmoji) { [weak self] in
|
||||
self?.spoilerTextLabel.setNeedsDisplay()
|
||||
}
|
||||
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
|
||||
spoilerTextLabel.attributedText = mutableSpoilerText
|
||||
spoilerTextLabel.isHidden = !viewModel.sensitive || spoilerTextLabel.text == ""
|
||||
toggleSensitiveContentButton.setTitle(
|
||||
viewModel.shouldDisplaySensitiveContent
|
||||
? NSLocalizedString("status.show-less", comment: "")
|
||||
: NSLocalizedString("status.show-more", comment: ""),
|
||||
for: .normal)
|
||||
accountLabel.text = viewModel.accountName
|
||||
timeLabel.text = viewModel.time
|
||||
contextParentTimeLabel.text = viewModel.contextParentTime
|
||||
timeApplicationDividerView.isHidden = viewModel.applicationName == nil
|
||||
applicationButton.isHidden = viewModel.applicationName == nil
|
||||
applicationButton.setTitle(viewModel.applicationName, for: .normal)
|
||||
applicationButton.isEnabled = viewModel.applicationURL != nil
|
||||
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
||||
toggleSensitiveContentButton.isHidden = !viewModel.sensitive
|
||||
replyButton.setTitle(viewModel.repliesCount == 0 ? "" : String(viewModel.repliesCount), for: .normal)
|
||||
reblogButton.setTitle(viewModel.reblogsCount == 0 ? "" : String(viewModel.reblogsCount), for: .normal)
|
||||
setReblogButtonColor(reblogged: viewModel.reblogged)
|
||||
favoriteButton.setTitle(viewModel.favoritesCount == 0 ? "" : String(viewModel.favoritesCount), for: .normal)
|
||||
setFavoriteButtonColor(favorited: viewModel.favorited)
|
||||
|
||||
reblogButton.isEnabled = viewModel.canBeReblogged
|
||||
contextParentReblogButton.isEnabled = viewModel.canBeReblogged
|
||||
|
||||
let noReblogs = viewModel.reblogsCount == 0
|
||||
let noFavorites = viewModel.favoritesCount == 0
|
||||
let noInteractions = noReblogs && noFavorites
|
||||
|
||||
setAttributedLocalizedTitle(
|
||||
button: contextParentRebloggedByButton,
|
||||
localizationKey: "status.reblogs-count",
|
||||
count: viewModel.reblogsCount)
|
||||
contextParentRebloggedByButton.isHidden = noReblogs
|
||||
setAttributedLocalizedTitle(
|
||||
button: contextParentFavoritedByButton,
|
||||
localizationKey: "status.favorites-count",
|
||||
count: viewModel.favoritesCount)
|
||||
contextParentFavoritedByButton.isHidden = noFavorites
|
||||
|
||||
contextParentRebloggedByFavoritedByView.isHidden = noInteractions
|
||||
contextParentRebloggedByFavoritedBySeparator.isHidden = noInteractions
|
||||
|
||||
if
|
||||
viewModel.isReblog {
|
||||
let metaText = String.localizedStringWithFormat(
|
||||
NSLocalizedString("status.reblogged-by", comment: ""),
|
||||
viewModel.rebloggedByDisplayName)
|
||||
let mutableMetaText = NSMutableAttributedString(string: metaText)
|
||||
mutableMetaText.insert(emojis: viewModel.rebloggedByDisplayNameEmoji) { [weak self] in
|
||||
self?.metaLabel.setNeedsDisplay()
|
||||
}
|
||||
mutableMetaText.resizeAttachments(toLineHeight: metaLabel.font.lineHeight)
|
||||
metaLabel.attributedText = mutableMetaText
|
||||
metaIcon.image = UIImage(
|
||||
systemName: "arrow.2.squarepath",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
||||
metaLabel.isHidden = false
|
||||
metaIcon.isHidden = false
|
||||
} else if viewModel.isPinned {
|
||||
metaLabel.text = NSLocalizedString("status.pinned-post", comment: "")
|
||||
metaIcon.image = UIImage(
|
||||
systemName: "pin",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
||||
metaLabel.isHidden = false
|
||||
metaIcon.isHidden = false
|
||||
} else {
|
||||
metaLabel.isHidden = true
|
||||
metaIcon.isHidden = true
|
||||
}
|
||||
|
||||
if let cardURL = viewModel.cardURL {
|
||||
cardTitleLabel.text = viewModel.cardTitle
|
||||
cardDescriptionLabel.text = viewModel.cardDescription
|
||||
cardDescriptionLabel.isHidden = cardDescriptionLabel.text == ""
|
||||
|| cardDescriptionLabel.text == cardTitleLabel.text
|
||||
if
|
||||
let host = cardURL.host, host.hasPrefix("www."),
|
||||
let withoutWww = cardURL.host?.components(separatedBy: "www.").last {
|
||||
cardURLLabel.text = withoutWww
|
||||
} else {
|
||||
cardURLLabel.text = cardURL.host
|
||||
}
|
||||
|
||||
if let cardImageURL = viewModel.cardImageURL {
|
||||
cardImageView.isHidden = false
|
||||
cardImageView.kf.setImage(with: cardImageURL)
|
||||
} else {
|
||||
cardImageView.isHidden = true
|
||||
}
|
||||
cardView.isHidden = false
|
||||
} else {
|
||||
cardView.isHidden = true
|
||||
}
|
||||
|
||||
sensitiveContentView.isHidden = !viewModel.shouldDisplaySensitiveContent
|
||||
|
||||
inReplyToView.isHidden = !viewModel.isReplyInContext
|
||||
|
||||
hasReplyFollowingView.isHidden = !viewModel.hasReplyFollowing
|
||||
}
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
for constraint in separatorConstraints {
|
||||
constraint.constant = 1 / UIScreen.main.scale
|
||||
}
|
||||
|
||||
avatarImageView.kf.indicatorType = .activity
|
||||
contextParentAvatarImageView.kf.indicatorType = .activity
|
||||
cardImageView.kf.indicatorType = .activity
|
||||
|
||||
contentTextView.delegate = self
|
||||
|
||||
let highlightedButtonBackgroundImage = UIColor(white: 0, alpha: 0.5).image()
|
||||
|
||||
cardButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted)
|
||||
avatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted)
|
||||
contextParentAvatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
avatarImageView.kf.cancelDownloadTask()
|
||||
cardImageView.kf.cancelDownloadTask()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
for button: UIButton in [toggleSensitiveContentButton] where button.frame.height != 0 {
|
||||
button.layer.cornerRadius = button.frame.height / 2
|
||||
}
|
||||
|
||||
if hasReplyFollowingView.isHidden {
|
||||
separatorInset.right = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.right
|
||||
} else {
|
||||
separatorInset.right = .greatestFiniteMagnitude
|
||||
}
|
||||
|
||||
separatorInset.left = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.left
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusTableViewCell {
|
||||
@IBAction func avatarButtonTapped(_ sender: Any) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func toggleSensitiveContentButtonTapped(_ sender: Any) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func cardButtonTapped(_ sender: UIButton) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func replyButtonTapped(_ sender: UIButton) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func reblogButtonTapped(_ sender: UIButton) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func favoriteButtonTapped(_ sender: UIButton) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func actionsButtonTapped(_ sender: Any) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func shareButtonTapped(_ sender: Any) {
|
||||
delegate?.statusTableViewCellDidHaveShareButtonTapped(self)
|
||||
}
|
||||
|
||||
@IBAction func applicationButtonTapped(_ sender: Any) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func contextParentRebloggedByButtonTapped(_ sender: Any) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func contextParentFavoritedByButtonTapped(_ sender: Any) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusTableViewCell: UITextViewDelegate {
|
||||
func textView(
|
||||
_ textView: UITextView,
|
||||
shouldInteractWith URL: URL,
|
||||
in characterRange: NSRange,
|
||||
interaction: UITextItemInteraction) -> Bool {
|
||||
switch interaction {
|
||||
case .invokeDefaultAction: print(URL); return false
|
||||
case .preview: return false
|
||||
case .presentActions: return false
|
||||
@unknown default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusTableViewCell {
|
||||
private static let defaultAspectRatioConstraintMultiplier: CGFloat = 4.0 / 3.0
|
||||
private static let hasReplyFollowingSeparatorInsets = UIEdgeInsets(
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: .greatestFiniteMagnitude)
|
||||
|
||||
private func setReblogButtonColor(reblogged: Bool) {
|
||||
let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel
|
||||
let reblogButton: UIButton
|
||||
|
||||
if viewModel.isContextParent {
|
||||
reblogButton = contextParentReblogButton
|
||||
} else {
|
||||
reblogButton = self.reblogButton
|
||||
}
|
||||
|
||||
reblogButton.tintColor = reblogColor
|
||||
reblogButton.setTitleColor(reblogColor, for: .normal)
|
||||
}
|
||||
|
||||
private func setFavoriteButtonColor(favorited: Bool) {
|
||||
let favoriteColor: UIColor = favorited ? .systemYellow : .secondaryLabel
|
||||
let favoriteButton: UIButton
|
||||
let scale: UIImage.SymbolScale
|
||||
|
||||
if viewModel.isContextParent {
|
||||
favoriteButton = contextParentFavoriteButton
|
||||
scale = .medium
|
||||
} else {
|
||||
favoriteButton = self.favoriteButton
|
||||
scale = .small
|
||||
}
|
||||
|
||||
favoriteButton.tintColor = favoriteColor
|
||||
favoriteButton.setTitleColor(favoriteColor, for: .normal)
|
||||
favoriteButton.setImage(UIImage(
|
||||
systemName: favorited ? "star.fill" : "star",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: scale)),
|
||||
for: .normal)
|
||||
}
|
||||
|
||||
private func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) {
|
||||
let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count)
|
||||
|
||||
button.setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal)
|
||||
button.setAttributedTitle(
|
||||
localizedTitle.countEmphasizedAttributedString(count: count, highlighted: true),
|
||||
for: .highlighted)
|
||||
}
|
||||
}
|
556
iOS/Views/StatusTableViewCell.xib
Normal file
556
iOS/Views/StatusTableViewCell.xib
Normal file
|
@ -0,0 +1,556 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="1003" id="KGk-i7-Jjw" customClass="StatusTableViewCell" customModule="Metatext" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="377" height="1003"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="377" height="1003"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="PuM-4R-mah">
|
||||
<rect key="frame" x="16" y="11" width="345" height="981"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="NHl-tq-tPi">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="981"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="arrow.2.squarepath" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="uhT-HE-MkE">
|
||||
<rect key="frame" x="29.5" y="0.0" width="20.5" height="14.5"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tqr-8D-x5Z">
|
||||
<rect key="frame" x="24" y="-11" width="2" height="37.5"/>
|
||||
<color key="backgroundColor" systemColor="quaternaryLabelColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="2" id="Zpn-G6-ZXB"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rag-3x-Vbq" customClass="AnimatedImageView" customModule="Kingfisher">
|
||||
<rect key="frame" x="0.0" y="26.5" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="OSq-XT-4Ch"/>
|
||||
<constraint firstAttribute="width" constant="50" id="jO1-At-ia1"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="25"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<button opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dRj-hF-Ov8">
|
||||
<rect key="frame" x="0.0" y="26.5" width="50" height="50"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="25"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="avatarButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="CZ5-0T-jMR"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="H9F-5F-FkJ">
|
||||
<rect key="frame" x="24" y="76.5" width="2" height="915.5"/>
|
||||
<color key="backgroundColor" systemColor="quaternaryLabelColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="2" id="zZf-bJ-KBC"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="tqr-8D-x5Z" firstAttribute="centerX" secondItem="Rag-3x-Vbq" secondAttribute="centerX" id="2CR-TM-SV1"/>
|
||||
<constraint firstItem="H9F-5F-FkJ" firstAttribute="centerX" secondItem="Rag-3x-Vbq" secondAttribute="centerX" id="9GK-oD-aKw"/>
|
||||
<constraint firstItem="dRj-hF-Ov8" firstAttribute="leading" secondItem="Rag-3x-Vbq" secondAttribute="leading" id="9VS-vb-y46"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Rag-3x-Vbq" secondAttribute="trailing" id="Cau-hc-kV9"/>
|
||||
<constraint firstItem="dRj-hF-Ov8" firstAttribute="bottom" secondItem="Rag-3x-Vbq" secondAttribute="bottom" id="M6f-Zs-pEW"/>
|
||||
<constraint firstItem="dRj-hF-Ov8" firstAttribute="trailing" secondItem="Rag-3x-Vbq" secondAttribute="trailing" id="NIM-Pj-Jrj"/>
|
||||
<constraint firstItem="dRj-hF-Ov8" firstAttribute="top" secondItem="Rag-3x-Vbq" secondAttribute="top" id="NXO-iK-dvp"/>
|
||||
<constraint firstItem="Rag-3x-Vbq" firstAttribute="leading" secondItem="NHl-tq-tPi" secondAttribute="leading" id="R0T-s6-Kon"/>
|
||||
<constraint firstItem="tqr-8D-x5Z" firstAttribute="bottom" secondItem="Rag-3x-Vbq" secondAttribute="top" id="a20-V8-0xV"/>
|
||||
<constraint firstItem="H9F-5F-FkJ" firstAttribute="top" secondItem="Rag-3x-Vbq" secondAttribute="bottom" id="gby-3L-E2T"/>
|
||||
<constraint firstAttribute="trailing" secondItem="uhT-HE-MkE" secondAttribute="trailing" id="osH-xC-Krv"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qtT-hh-JlW">
|
||||
<rect key="frame" x="58" y="0.0" width="287" height="981"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="top" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Vfx-Uf-SCV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="167"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ful-we-pCE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="Cyx-R2-XUe">
|
||||
<rect key="frame" x="0.0" y="22.5" width="287" height="18"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aHQ-z9-LWz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="43.5" height="18"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hrV-68-5hn">
|
||||
<rect key="frame" x="47.5" y="0.0" width="198" height="18"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lGC-HZ-ZT3">
|
||||
<rect key="frame" x="249.5" y="0.0" width="37.5" height="18"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="P6a-zL-9B9">
|
||||
<rect key="frame" x="0.0" y="48.5" width="287" height="50"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oXX-vk-ESS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BLe-zN-EsU" customClass="AnimatedImageView" customModule="Kingfisher">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="50" id="1bA-D3-I4a"/>
|
||||
<constraint firstAttribute="height" constant="50" id="G3i-Y5-C3e"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="25"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<button opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aNl-ag-2fc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="25"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="avatarButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="S3W-K2-aSY"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="BLe-zN-EsU" secondAttribute="bottom" id="0vg-93-dFw"/>
|
||||
<constraint firstItem="aNl-ag-2fc" firstAttribute="leading" secondItem="BLe-zN-EsU" secondAttribute="leading" id="1Mr-NZ-A1g"/>
|
||||
<constraint firstItem="BLe-zN-EsU" firstAttribute="top" secondItem="oXX-vk-ESS" secondAttribute="top" id="5vR-d6-1ne"/>
|
||||
<constraint firstItem="BLe-zN-EsU" firstAttribute="leading" secondItem="oXX-vk-ESS" secondAttribute="leading" id="G4T-oU-MdP"/>
|
||||
<constraint firstItem="aNl-ag-2fc" firstAttribute="bottom" secondItem="BLe-zN-EsU" secondAttribute="bottom" id="HPR-zH-Wia"/>
|
||||
<constraint firstItem="aNl-ag-2fc" firstAttribute="trailing" secondItem="BLe-zN-EsU" secondAttribute="trailing" id="Xx3-36-S6g"/>
|
||||
<constraint firstAttribute="trailing" secondItem="BLe-zN-EsU" secondAttribute="trailing" id="baJ-by-gCl"/>
|
||||
<constraint firstItem="aNl-ag-2fc" firstAttribute="top" secondItem="BLe-zN-EsU" secondAttribute="top" id="zdp-yv-BHJ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="mYD-9U-oFJ">
|
||||
<rect key="frame" x="58" y="0.0" width="229" height="50"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ya9-zV-dMO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="229" height="32"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aa2-u6-YBQ">
|
||||
<rect key="frame" x="0.0" y="32" width="229" height="18"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Mz-X2-wmw">
|
||||
<rect key="frame" x="0.0" y="106.5" width="287" height="19.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Occ-DT-kxh">
|
||||
<rect key="frame" x="0.0" y="134" width="287" height="33"/>
|
||||
<color key="backgroundColor" systemColor="linkColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<state key="normal" title="Show More">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="toggleSensitiveContentButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="m7M-MA-f02"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Rdc-ZD-zdc">
|
||||
<rect key="frame" x="0.0" y="175" width="287" height="654.5"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="751" scrollEnabled="NO" editable="NO" text="Content" textAlignment="natural" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GvW-gH-ofw" customClass="TouchFallthroughTextView" customModule="Metatext" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="345"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gCY-Qy-hbS">
|
||||
<rect key="frame" x="0.0" y="353" width="287" height="301.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="3C9-uk-wu2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="301.5"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="9jB-zW-tJB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="215"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="9jB-zW-tJB" secondAttribute="height" multiplier="4:3" priority="999" id="ioh-Zj-QGD"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="V94-ed-vFr">
|
||||
<rect key="frame" x="0.0" y="215" width="287" height="86.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dS1-ny-kA1">
|
||||
<rect key="frame" x="8" y="8" width="271" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RAj-xv-7Sr">
|
||||
<rect key="frame" x="8" y="36.5" width="271" height="18"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IhY-1O-KAR">
|
||||
<rect key="frame" x="8" y="62.5" width="271" height="16"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<directionalEdgeInsets key="directionalLayoutMargins" top="8" leading="8" bottom="8" trailing="8"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6pk-EO-aa0">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="301.5"/>
|
||||
<connections>
|
||||
<action selector="cardButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="0L8-TA-GxD"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="6pk-EO-aa0" firstAttribute="leading" secondItem="gCY-Qy-hbS" secondAttribute="leading" id="4oF-J2-goz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="3C9-uk-wu2" secondAttribute="trailing" id="6in-BD-bgA"/>
|
||||
<constraint firstItem="3C9-uk-wu2" firstAttribute="leading" secondItem="gCY-Qy-hbS" secondAttribute="leading" id="BRL-4d-akw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6pk-EO-aa0" secondAttribute="trailing" id="LCI-Ey-TJ0"/>
|
||||
<constraint firstItem="6pk-EO-aa0" firstAttribute="top" secondItem="gCY-Qy-hbS" secondAttribute="top" id="XYJ-Zq-6iT"/>
|
||||
<constraint firstItem="3C9-uk-wu2" firstAttribute="top" secondItem="gCY-Qy-hbS" secondAttribute="top" id="dAK-CM-V0l"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6pk-EO-aa0" secondAttribute="bottom" id="e4U-OB-8hi"/>
|
||||
<constraint firstAttribute="bottom" secondItem="3C9-uk-wu2" secondAttribute="bottom" id="uvq-Pb-NhR"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="8"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="wFL-zS-Rev">
|
||||
<rect key="frame" x="0.0" y="837.5" width="287" height="18.5"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" adjustsImageSizeForAccessibilityContentSizeCategory="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1mc-Dy-v2f">
|
||||
<rect key="frame" x="0.0" y="0.0" width="72" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption2"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<inset key="titleEdgeInsets" minX="4" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<state key="normal" title="69" image="bubble.right" catalog="system">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="replyButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="fIh-jy-zyy"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" adjustsImageSizeForAccessibilityContentSizeCategory="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="25b-IE-HZK">
|
||||
<rect key="frame" x="72" y="0.0" width="71.5" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption2"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<inset key="titleEdgeInsets" minX="4" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<state key="normal" title="69" image="arrow.2.squarepath" catalog="system">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="reblogButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="2od-kC-4K5"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" adjustsImageSizeForAccessibilityContentSizeCategory="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7f5-WO-gpi">
|
||||
<rect key="frame" x="143.5" y="0.0" width="72" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption2"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<inset key="titleEdgeInsets" minX="4" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<state key="normal" title="69" image="star" catalog="system">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="favoriteButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="hyh-wI-9WM"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageSizeForAccessibilityContentSizeCategory="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nrc-xp-Llw">
|
||||
<rect key="frame" x="215.5" y="0.0" width="71.5" height="18.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption2"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" image="square.and.arrow.up" catalog="system">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="shareButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="Owv-iB-VxJ"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Fhw-OW-ixv">
|
||||
<rect key="frame" x="0.0" y="864" width="287" height="117"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="xvi-yh-xAX">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="28"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRM-1y-2pU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="33" height="28"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="•" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NkD-O9-e9D">
|
||||
<rect key="frame" x="37" y="0.0" width="6.5" height="28"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hkk-pX-8oP">
|
||||
<rect key="frame" x="47.5" y="0.0" width="239.5" height="28"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<state key="normal" title="Button"/>
|
||||
<state key="disabled">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="applicationButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="yAU-hM-cUP"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4ZR-31-F0G">
|
||||
<rect key="frame" x="0.0" y="36" width="287" height="1"/>
|
||||
<color key="backgroundColor" systemColor="separatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="jaw-Uw-Xby"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="VJg-8P-ax9">
|
||||
<rect key="frame" x="0.0" y="45" width="287" height="33"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pqY-NQ-5Fz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="143.5" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" title="666">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" systemColor="tertiaryLabelColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="contextParentRebloggedByButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="X5h-c5-5Yq"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Olw-ZF-pYB">
|
||||
<rect key="frame" x="143.5" y="0.0" width="143.5" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="666">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" systemColor="tertiaryLabelColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="contextParentFavoritedByButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="eFA-jK-7pJ"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="SlW-Ii-ozK">
|
||||
<rect key="frame" x="0.0" y="86" width="287" height="1"/>
|
||||
<color key="backgroundColor" systemColor="separatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="DoC-rC-wYG"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="XOe-fg-wO6">
|
||||
<rect key="frame" x="0.0" y="95" width="287" height="22"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="38F-f5-8Ha">
|
||||
<rect key="frame" x="0.0" y="0.0" width="51" height="22"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" image="bubble.right" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="medium"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="replyButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="jkz-ad-n2Y"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Rjo-bC-85y">
|
||||
<rect key="frame" x="59" y="0.0" width="51" height="22"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" image="arrow.2.squarepath" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="medium"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="reblogButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="hYp-sm-i4K"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q79-pC-6ru">
|
||||
<rect key="frame" x="118" y="0.0" width="51" height="22"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" image="star" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="medium"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="favoriteButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="NdC-VQ-9ZD"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5Ft-M2-BMM">
|
||||
<rect key="frame" x="177" y="0.0" width="51" height="22"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" image="square.and.arrow.up" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="medium"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="shareButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="hUE-mZ-QST"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Z1Y-Ch-xB7">
|
||||
<rect key="frame" x="236" y="0.0" width="51" height="22"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||
<state key="normal" image="ellipsis" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="medium"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="actionsButtonTapped:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="7qm-km-Ydt"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="uhT-HE-MkE" firstAttribute="centerY" secondItem="ful-we-pCE" secondAttribute="centerY" id="5rP-PQ-irT"/>
|
||||
<constraint firstItem="Rag-3x-Vbq" firstAttribute="top" secondItem="Cyx-R2-XUe" secondAttribute="top" constant="4" id="8h6-s3-iOk"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="PuM-4R-mah" secondAttribute="trailing" id="7TB-kA-Oo4"/>
|
||||
<constraint firstItem="PuM-4R-mah" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="81w-ID-ymP"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="PuM-4R-mah" secondAttribute="bottom" id="DB9-xL-qNI"/>
|
||||
<constraint firstItem="PuM-4R-mah" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="VY2-YX-92L"/>
|
||||
<constraint firstItem="tqr-8D-x5Z" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="YwS-sf-Z70"/>
|
||||
<constraint firstAttribute="bottom" secondItem="H9F-5F-FkJ" secondAttribute="bottom" id="bWC-zo-sgU"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="accountLabel" destination="hrV-68-5hn" id="Kn4-pt-nQa"/>
|
||||
<outlet property="actionButtonsView" destination="wFL-zS-Rev" id="h7S-r0-vMa"/>
|
||||
<outlet property="applicationButton" destination="hkk-pX-8oP" id="tHC-cy-B25"/>
|
||||
<outlet property="avatarButton" destination="dRj-hF-Ov8" id="1aD-hK-1iz"/>
|
||||
<outlet property="avatarImageView" destination="Rag-3x-Vbq" id="BaZ-ad-Qgs"/>
|
||||
<outlet property="avatarReplyContextView" destination="NHl-tq-tPi" id="CCJ-SE-Egv"/>
|
||||
<outlet property="cardButton" destination="6pk-EO-aa0" id="5SC-dt-7P7"/>
|
||||
<outlet property="cardDescriptionLabel" destination="RAj-xv-7Sr" id="Ph4-KM-qED"/>
|
||||
<outlet property="cardImageView" destination="9jB-zW-tJB" id="0Vr-xC-rIa"/>
|
||||
<outlet property="cardTitleLabel" destination="dS1-ny-kA1" id="hYw-f7-WrH"/>
|
||||
<outlet property="cardURLLabel" destination="IhY-1O-KAR" id="9Cl-2P-Z7x"/>
|
||||
<outlet property="cardView" destination="gCY-Qy-hbS" id="u7g-iK-qhw"/>
|
||||
<outlet property="contentTextView" destination="GvW-gH-ofw" id="sVs-dt-0Zt"/>
|
||||
<outlet property="contextParentAccountLabel" destination="aa2-u6-YBQ" id="NeO-aF-DPZ"/>
|
||||
<outlet property="contextParentActionsButton" destination="Z1Y-Ch-xB7" id="7S4-vc-gKe"/>
|
||||
<outlet property="contextParentAvatarButton" destination="aNl-ag-2fc" id="Tah-tr-ckM"/>
|
||||
<outlet property="contextParentAvatarImageView" destination="BLe-zN-EsU" id="99c-tt-zvI"/>
|
||||
<outlet property="contextParentAvatarNameView" destination="P6a-zL-9B9" id="eGx-i7-i1a"/>
|
||||
<outlet property="contextParentDisplayNameLabel" destination="ya9-zV-dMO" id="5QN-x6-V5E"/>
|
||||
<outlet property="contextParentFavoriteButton" destination="Q79-pC-6ru" id="pZI-Yh-Tbh"/>
|
||||
<outlet property="contextParentFavoritedByButton" destination="Olw-ZF-pYB" id="AwZ-wP-X55"/>
|
||||
<outlet property="contextParentItems" destination="Fhw-OW-ixv" id="wws-EA-hjY"/>
|
||||
<outlet property="contextParentReblogButton" destination="Rjo-bC-85y" id="hGu-h5-nbh"/>
|
||||
<outlet property="contextParentRebloggedByButton" destination="pqY-NQ-5Fz" id="Cd9-Lf-lTr"/>
|
||||
<outlet property="contextParentRebloggedByFavoritedBySeparator" destination="SlW-Ii-ozK" id="bKO-mz-IIW"/>
|
||||
<outlet property="contextParentRebloggedByFavoritedByView" destination="VJg-8P-ax9" id="fzs-Fe-m0t"/>
|
||||
<outlet property="contextParentReplyButton" destination="38F-f5-8Ha" id="bwI-S3-ZUM"/>
|
||||
<outlet property="contextParentShareButton" destination="5Ft-M2-BMM" id="EC4-Xj-qcK"/>
|
||||
<outlet property="contextParentTimeLabel" destination="hRM-1y-2pU" id="nwb-rf-gXn"/>
|
||||
<outlet property="displayNameLabel" destination="aHQ-z9-LWz" id="v3P-4w-7mg"/>
|
||||
<outlet property="favoriteButton" destination="7f5-WO-gpi" id="VvN-cW-anv"/>
|
||||
<outlet property="hasReplyFollowingView" destination="H9F-5F-FkJ" id="abM-Ye-pzJ"/>
|
||||
<outlet property="inReplyToView" destination="tqr-8D-x5Z" id="l7j-I4-vLJ"/>
|
||||
<outlet property="metaIcon" destination="uhT-HE-MkE" id="1z9-PN-DvJ"/>
|
||||
<outlet property="metaLabel" destination="ful-we-pCE" id="FtP-We-cCj"/>
|
||||
<outlet property="nameDateView" destination="Cyx-R2-XUe" id="FGc-TV-Ufc"/>
|
||||
<outlet property="reblogButton" destination="25b-IE-HZK" id="iZD-V3-d3T"/>
|
||||
<outlet property="replyButton" destination="1mc-Dy-v2f" id="p07-xE-ZUI"/>
|
||||
<outlet property="sensitiveContentView" destination="Rdc-ZD-zdc" id="yAX-uL-QH0"/>
|
||||
<outlet property="shareButton" destination="nrc-xp-Llw" id="VeC-Mh-brV"/>
|
||||
<outlet property="spoilerTextLabel" destination="7Mz-X2-wmw" id="7OY-Oa-Lhk"/>
|
||||
<outlet property="timeApplicationDividerView" destination="NkD-O9-e9D" id="ygX-on-Xqu"/>
|
||||
<outlet property="timeLabel" destination="lGC-HZ-ZT3" id="4Xu-ia-eDI"/>
|
||||
<outlet property="toggleSensitiveContentButton" destination="Occ-DT-kxh" id="yd4-xI-94j"/>
|
||||
<outletCollection property="separatorConstraints" destination="jaw-Uw-Xby" collectionClass="NSMutableArray" id="OvM-ag-v1s"/>
|
||||
<outletCollection property="separatorConstraints" destination="DoC-rC-wYG" collectionClass="NSMutableArray" id="6NT-uI-Td4"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="340" y="-525"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="arrow.2.squarepath" catalog="system" width="128" height="89"/>
|
||||
<image name="bubble.right" catalog="system" width="128" height="110"/>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<image name="square.and.arrow.up" catalog="system" width="115" height="128"/>
|
||||
<image name="star" catalog="system" width="128" height="116"/>
|
||||
<systemColor name="linkColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="quaternaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.17999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondarySystemBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="separatorColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="tertiaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -40,7 +40,7 @@ private extension TabNavigationView {
|
|||
func view(tab: TabNavigationViewModel.Tab) -> some View {
|
||||
switch tab {
|
||||
case .timelines:
|
||||
StatusesView(viewModel: viewModel.timelineViewModel)
|
||||
StatusListView(viewModel: viewModel.timelineViewModel)
|
||||
.navigationBarTitle(viewModel.identity.handle, displayMode: .inline)
|
||||
.navigationBarItems(
|
||||
leading: Button {
|
||||
|
|
100
iOS/Views/TouchFallthroughTextView.swift
Normal file
100
iOS/Views/TouchFallthroughTextView.swift
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class TouchFallthroughTextView: UITextView {
|
||||
var shouldFallthrough: Bool = true
|
||||
|
||||
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
initializationActions()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
initializationActions()
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
shouldFallthrough ? urlAndRect(at: point) != nil : super.point(inside: point, with: event)
|
||||
}
|
||||
|
||||
override var selectedTextRange: UITextRange? {
|
||||
get { shouldFallthrough ? nil : super.selectedTextRange }
|
||||
set {
|
||||
if !shouldFallthrough {
|
||||
super.selectedTextRange = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return text == "" ? .zero : super.intrinsicContentSize
|
||||
}
|
||||
|
||||
func urlAndRect(at point: CGPoint) -> (URL, CGRect)? {
|
||||
guard
|
||||
let pos = closestPosition(to: point),
|
||||
let range = tokenizer.rangeEnclosingPosition(
|
||||
pos, with: .character,
|
||||
inDirection: UITextDirection.layout(.left))
|
||||
else { return nil }
|
||||
|
||||
let urlAtPointIndex = offset(from: beginningOfDocument, to: range.start)
|
||||
|
||||
guard let url = attributedText.attribute(
|
||||
.link, at: offset(from: beginningOfDocument, to: range.start),
|
||||
effectiveRange: nil) as? URL
|
||||
else { return nil }
|
||||
|
||||
let maxLength = attributedText.length
|
||||
var min = urlAtPointIndex
|
||||
var max = urlAtPointIndex
|
||||
|
||||
attributedText.enumerateAttribute(
|
||||
.link,
|
||||
in: NSRange(location: 0, length: urlAtPointIndex),
|
||||
options: .reverse) { attribute, range, stop in
|
||||
if let attributeURL = attribute as? URL, attributeURL == url, min > 0 {
|
||||
min = range.location
|
||||
} else {
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
|
||||
attributedText.enumerateAttribute(
|
||||
.link,
|
||||
in: NSRange(location: urlAtPointIndex, length: maxLength - urlAtPointIndex),
|
||||
options: []) { attribute, range, stop in
|
||||
if let attributeURL = attribute as? URL, attributeURL == url, max < maxLength {
|
||||
max = range.location + range.length
|
||||
} else {
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
|
||||
var urlRect = CGRect.zero
|
||||
|
||||
layoutManager.enumerateEnclosingRects(
|
||||
forGlyphRange: NSRange(location: min, length: max - min),
|
||||
withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
|
||||
in: textContainer) { rect, _ in
|
||||
if urlRect.origin == .zero {
|
||||
urlRect.origin = rect.origin
|
||||
}
|
||||
|
||||
urlRect = urlRect.union(rect)
|
||||
}
|
||||
|
||||
return (url, urlRect)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TouchFallthroughTextView {
|
||||
private func initializationActions() {
|
||||
textDragInteraction?.isEnabled = false
|
||||
textContainerInset = .zero
|
||||
textContainer.lineFragmentPadding = 0
|
||||
linkTextAttributes = [.foregroundColor: tintColor as Any, .underlineColor: UIColor.clear]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue