Merge pull request #103 from tootsuite/feature/post-thread
Add thread scene and reply post support
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -115,6 +115,7 @@
|
|||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mentions" inverseEntity="Status"/>
|
||||
|
@ -209,7 +210,7 @@
|
|||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
|
@ -217,4 +218,4 @@
|
|||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||
</elements>
|
||||
</model>
|
||||
</model>
|
|
@ -10,6 +10,9 @@ import Foundation
|
|||
|
||||
public final class Mention: NSManagedObject {
|
||||
public typealias ID = UUID
|
||||
|
||||
@NSManaged public private(set) var index: NSNumber
|
||||
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var id: String
|
||||
@NSManaged public private(set) var createAt: Date
|
||||
|
@ -32,9 +35,11 @@ public extension Mention {
|
|||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
property: Property,
|
||||
index: Int
|
||||
) -> Mention {
|
||||
let mention: Mention = context.insertObject()
|
||||
mention.index = NSNumber(value: index)
|
||||
mention.id = property.id
|
||||
mention.username = property.username
|
||||
mention.acct = property.acct
|
||||
|
|
|
@ -89,7 +89,8 @@
|
|||
"timeline": {
|
||||
"loader": {
|
||||
"load_missing_posts": "Load missing posts",
|
||||
"loading_missing_posts": "Loading missing posts..."
|
||||
"loading_missing_posts": "Loading missing posts...",
|
||||
"show_more_replies": "Show more replies"
|
||||
},
|
||||
"header": {
|
||||
"no_status_found": "No Status Found",
|
||||
|
@ -198,7 +199,7 @@
|
|||
},
|
||||
"confirm_email": {
|
||||
"title": "One last thing.",
|
||||
"subtitle": "We just sent an email to %@,\ntap the link to confirm your account.",
|
||||
"subtitle": "We just sent an email to %s,\ntap the link to confirm your account.",
|
||||
"button": {
|
||||
"open_email_app": "Open Email App",
|
||||
"dont_receive_email": "I never got an email"
|
||||
|
@ -239,6 +240,7 @@
|
|||
},
|
||||
"content_input_placeholder": "Type or paste what's on your mind",
|
||||
"compose_action": "Publish",
|
||||
"replying_to_user": "replying to %s",
|
||||
"attachment": {
|
||||
"photo": "photo",
|
||||
"video": "video",
|
||||
|
@ -253,7 +255,8 @@
|
|||
"six_hours": "6 Hours",
|
||||
"one_day": "1 Day",
|
||||
"three_days": "3 Days",
|
||||
"seven_days": "7 Days"
|
||||
"seven_days": "7 Days",
|
||||
"option_number": "Option %ld"
|
||||
},
|
||||
"content_warning": {
|
||||
"placeholder": "Write an accurate warning here..."
|
||||
|
@ -321,6 +324,18 @@
|
|||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
},
|
||||
"thread": {
|
||||
"back_title": "Post",
|
||||
"title": "Post from %s",
|
||||
"reblog": {
|
||||
"single": "%s reblog",
|
||||
"multiple": "%s reblogs"
|
||||
},
|
||||
"favorite": {
|
||||
"single": "%s favorite",
|
||||
"multiple": "%s favorites"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -144,6 +144,8 @@
|
|||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
|
@ -218,7 +220,7 @@
|
|||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */; };
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
|
||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
||||
|
@ -263,6 +265,15 @@
|
|||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; };
|
||||
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */; };
|
||||
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */; };
|
||||
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */; };
|
||||
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */; };
|
||||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; };
|
||||
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; };
|
||||
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */; };
|
||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; };
|
||||
DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */; };
|
||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; };
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; };
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
||||
|
@ -306,6 +317,7 @@
|
|||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
|
||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */; };
|
||||
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
|
||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
|
||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
|
||||
|
@ -524,6 +536,8 @@
|
|||
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
|
@ -604,7 +618,7 @@
|
|||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||
|
@ -651,6 +665,15 @@
|
|||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
||||
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = "<group>"; };
|
||||
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewController.swift; sourceTree = "<group>"; };
|
||||
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = "<group>"; };
|
||||
DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedThreadViewModel.swift; sourceTree = "<group>"; };
|
||||
DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = "<group>"; };
|
||||
DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = "<group>"; };
|
||||
DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Thread.swift"; sourceTree = "<group>"; };
|
||||
DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
|
||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
|
||||
|
@ -693,6 +716,7 @@
|
|||
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardView.swift; sourceTree = "<group>"; };
|
||||
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = "<group>"; };
|
||||
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
||||
|
@ -865,6 +889,7 @@
|
|||
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
|
||||
DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */,
|
||||
0F1E2D0A2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift */,
|
||||
DBB9759B262462E1004620BD /* ThreadMetaView.swift */,
|
||||
);
|
||||
path = Content;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1070,9 +1095,11 @@
|
|||
children = (
|
||||
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */,
|
||||
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
||||
DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */,
|
||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
||||
DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */,
|
||||
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */,
|
||||
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
|
||||
);
|
||||
path = TableviewCell;
|
||||
|
@ -1310,6 +1337,7 @@
|
|||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
||||
DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */,
|
||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
||||
DB9A488F26035963008B817C /* APIService+Media.swift */,
|
||||
|
@ -1430,7 +1458,7 @@
|
|||
children = (
|
||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
|
||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */,
|
||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
|
||||
|
@ -1554,6 +1582,7 @@
|
|||
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
||||
DB9D6C0825E4F5A60051B173 /* Profile */,
|
||||
DB789A1025F9F29B0071ACA0 /* Compose */,
|
||||
DB938EEB2623F52600E5B6C1 /* Thread */,
|
||||
);
|
||||
path = Scene;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1594,6 +1623,20 @@
|
|||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB938EEB2623F52600E5B6C1 /* Thread */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */,
|
||||
DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */,
|
||||
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */,
|
||||
DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */,
|
||||
DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */,
|
||||
DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */,
|
||||
DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */,
|
||||
);
|
||||
path = Thread;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98335F25C93B0400AD9700 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1700,6 +1743,7 @@
|
|||
children = (
|
||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */,
|
||||
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
|
||||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */,
|
||||
);
|
||||
path = Control;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2200,6 +2244,7 @@
|
|||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
||||
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||
0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */,
|
||||
|
@ -2208,6 +2253,7 @@
|
|||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
||||
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
||||
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
|
||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
|
||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||
|
@ -2221,6 +2267,7 @@
|
|||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
|
||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
|
||||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||
|
@ -2242,13 +2289,14 @@
|
|||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
|
||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
|
||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
|
||||
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
||||
|
@ -2265,6 +2313,7 @@
|
|||
DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
|
||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
||||
|
@ -2331,8 +2380,10 @@
|
|||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
|
@ -2350,11 +2401,13 @@
|
|||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||
2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */,
|
||||
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
|
||||
DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */,
|
||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
|
||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
||||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
|
||||
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
|
||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
|
||||
|
@ -2401,6 +2454,7 @@
|
|||
0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */,
|
||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||
|
@ -2422,7 +2476,9 @@
|
|||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||
DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */,
|
||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>10</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
|
|
@ -51,6 +51,9 @@ extension SceneCoordinator {
|
|||
// compose
|
||||
case compose(viewModel: ComposeViewModel)
|
||||
|
||||
// thread
|
||||
case thread(viewModel: ThreadViewModel)
|
||||
|
||||
// Hashtag Timeline
|
||||
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
||||
|
||||
|
@ -226,6 +229,10 @@ private extension SceneCoordinator {
|
|||
let _viewController = ComposeViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .thread(let viewModel):
|
||||
let _viewController = ThreadViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .hashtagTimeline(let viewModel):
|
||||
let _viewController = HashtagTimelineViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
|
|
@ -14,6 +14,12 @@ import MastodonSDK
|
|||
enum Item {
|
||||
// timeline
|
||||
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
|
||||
// thread
|
||||
case root(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
case reply(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
case leaf(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
case leafBottomLoader(statusObjectID: NSManagedObjectID)
|
||||
|
||||
// normal list
|
||||
case status(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
|
@ -21,6 +27,7 @@ enum Item {
|
|||
// loader
|
||||
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
||||
case publicMiddleLoader(statusID: String)
|
||||
case topLoader
|
||||
case bottomLoader
|
||||
|
||||
case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
|
||||
|
@ -35,13 +42,16 @@ extension Item {
|
|||
class StatusAttribute: StatusContentWarningAttribute {
|
||||
var isStatusTextSensitive: Bool?
|
||||
var isStatusSensitive: Bool?
|
||||
var isSeparatorLineHidden: Bool
|
||||
|
||||
init(
|
||||
isStatusTextSensitive: Bool? = nil,
|
||||
isStatusSensitive: Bool? = nil
|
||||
isStatusSensitive: Bool? = nil,
|
||||
isSeparatorLineHidden: Bool = false
|
||||
) {
|
||||
self.isStatusTextSensitive = isStatusTextSensitive
|
||||
self.isStatusSensitive = isStatusSensitive
|
||||
self.isSeparatorLineHidden = isSeparatorLineHidden
|
||||
}
|
||||
|
||||
// delay attribute init
|
||||
|
@ -59,6 +69,23 @@ extension Item {
|
|||
}
|
||||
}
|
||||
|
||||
// class LeafAttribute {
|
||||
// let identifier = UUID()
|
||||
// let statusID: Status.ID
|
||||
// var level: Int = 0
|
||||
// var hasReply: Bool = true
|
||||
//
|
||||
// init(
|
||||
// statusID: Status.ID,
|
||||
// level: Int,
|
||||
// hasReply: Bool = true
|
||||
// ) {
|
||||
// self.statusID = statusID
|
||||
// self.level = level
|
||||
// self.hasReply = hasReply
|
||||
// }
|
||||
// }
|
||||
|
||||
class EmptyStateHeaderAttribute: Hashable {
|
||||
let id = UUID()
|
||||
let reason: Reason
|
||||
|
@ -99,12 +126,22 @@ extension Item: Equatable {
|
|||
switch (lhs, rhs) {
|
||||
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.root(let objectIDLeft, _), .root(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.reply(let objectIDLeft, _), .reply(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.leaf(let objectIDLeft, _), .leaf(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.leafBottomLoader(let objectIDLeft), .leafBottomLoader(let objectIDRight)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
|
||||
return upperLeft == upperRight
|
||||
case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
|
||||
return upperLeft == upperRight
|
||||
case (.topLoader, .topLoader):
|
||||
return true
|
||||
case (.bottomLoader, .bottomLoader):
|
||||
return true
|
||||
case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
|
||||
|
@ -120,6 +157,14 @@ extension Item: Hashable {
|
|||
switch self {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .root(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .reply(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .leaf(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .leafBottomLoader(let objectID):
|
||||
hasher.combine(objectID)
|
||||
case .status(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
|
||||
|
@ -128,6 +173,8 @@ extension Item: Hashable {
|
|||
case .publicMiddleLoader(let upper):
|
||||
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
||||
hasher.combine(upper)
|
||||
case .topLoader:
|
||||
hasher.combine(String(describing: Item.topLoader.self))
|
||||
case .bottomLoader:
|
||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||
case .emptyStateHeader(let attribute):
|
||||
|
|
|
@ -34,6 +34,7 @@ extension ComposeStatusSection {
|
|||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
composeKind: ComposeKind,
|
||||
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
|
@ -50,8 +51,29 @@ extension ComposeStatusSection {
|
|||
weak composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
switch item {
|
||||
case .replyTo(let repliedToStatusObjectID):
|
||||
case .replyTo(let replyToStatusObjectID):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
|
||||
managedObjectContext.perform {
|
||||
guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||
return
|
||||
}
|
||||
let status = replyTo.reblog ?? replyTo
|
||||
|
||||
// set avatar
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||
// set name username
|
||||
cell.statusView.nameLabel.text = {
|
||||
let author = status.author
|
||||
return author.displayName.isEmpty ? author.username : author.displayName
|
||||
}()
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
// set text
|
||||
cell.statusView.activeTextLabel.configure(content: status.content)
|
||||
// set date
|
||||
cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow
|
||||
|
||||
cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .input(let replyToStatusObjectID, let attribute):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
||||
|
@ -63,16 +85,22 @@ extension ComposeStatusSection {
|
|||
return
|
||||
}
|
||||
cell.statusView.headerContainerStackView.isHidden = false
|
||||
cell.statusView.headerInfoLabel.text = "[TODO] \(replyTo.author.displayName)"
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback)
|
||||
}
|
||||
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||
cell.composeContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { text in
|
||||
// self size input cell
|
||||
// needs restore content offset to resolve issue #83
|
||||
let oldContentOffset = collectionView.contentOffset
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
collectionView.layoutIfNeeded()
|
||||
collectionView.contentOffset = oldContentOffset
|
||||
|
||||
// bind input data
|
||||
attribute.composeContent.value = text
|
||||
}
|
||||
|
@ -167,6 +195,7 @@ extension ComposeStatusSection {
|
|||
case .pollOption(let attribute):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
|
||||
cell.pollOptionView.optionTextField.text = attribute.option.value
|
||||
cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
|
||||
cell.pollOption
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: attribute.option)
|
||||
|
@ -196,7 +225,7 @@ extension ComposeStatusSection {
|
|||
|
||||
extension ComposeStatusSection {
|
||||
|
||||
static func configure(
|
||||
static func configureStatusContent(
|
||||
cell: ComposeStatusContentCollectionViewCell,
|
||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||
) {
|
||||
|
|
|
@ -22,9 +22,16 @@ extension StatusSection {
|
|||
managedObjectContext: NSManagedObjectContext,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?,
|
||||
threadReplyLoaderTableViewCellDelegate: ThreadReplyLoaderTableViewCellDelegate?
|
||||
) -> UITableViewDiffableDataSource<StatusSection, Item> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak statusTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency,
|
||||
weak statusTableViewCellDelegate,
|
||||
weak timelineMiddleLoaderTableViewCellDelegate,
|
||||
weak threadReplyLoaderTableViewCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let dependency = dependency else { return UITableViewCell() }
|
||||
guard let statusTableViewCellDelegate = statusTableViewCellDelegate else { return UITableViewCell() }
|
||||
|
||||
switch item {
|
||||
|
@ -46,7 +53,10 @@ extension StatusSection {
|
|||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
return cell
|
||||
case .status(let objectID, let attribute):
|
||||
case .status(let objectID, let attribute),
|
||||
.root(let objectID, let attribute),
|
||||
.reply(let objectID, let attribute),
|
||||
.leaf(let objectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
||||
|
@ -62,8 +72,30 @@ extension StatusSection {
|
|||
requestUserID: requestUserID,
|
||||
statusItemAttribute: attribute
|
||||
)
|
||||
|
||||
switch item {
|
||||
case .root:
|
||||
StatusSection.configureThreadMeta(cell: cell, status: status)
|
||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { change in
|
||||
guard case .update(let object) = change.changeType,
|
||||
let status = object as? Status else { return }
|
||||
StatusSection.configureThreadMeta(cell: cell, status: status)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
|
||||
return cell
|
||||
case .leafBottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self), for: indexPath) as! ThreadReplyLoaderTableViewCell
|
||||
cell.delegate = threadReplyLoaderTableViewCellDelegate
|
||||
return cell
|
||||
case .publicMiddleLoader(let upperTimelineStatusID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||
|
@ -75,6 +107,10 @@ extension StatusSection {
|
|||
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
||||
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
|
||||
return cell
|
||||
case .topLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.startAnimating()
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.startAnimating()
|
||||
|
@ -288,6 +324,9 @@ extension StatusSection {
|
|||
// toolbar
|
||||
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
||||
|
||||
// separator line
|
||||
cell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
|
||||
|
||||
// set date
|
||||
let createdAt = (status.reblog ?? status).createdAt
|
||||
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||
|
@ -312,6 +351,41 @@ extension StatusSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
static func configureThreadMeta(
|
||||
cell: StatusTableViewCell,
|
||||
status: Status
|
||||
) {
|
||||
cell.selectionStyle = .none
|
||||
cell.threadMetaView.dateLabel.text = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: status.createdAt)
|
||||
}()
|
||||
let reblogCountTitle: String = {
|
||||
let count = status.reblogsCount.intValue
|
||||
if count > 1 {
|
||||
return L10n.Scene.Thread.Reblog.multiple(String(count))
|
||||
} else {
|
||||
return L10n.Scene.Thread.Reblog.single(String(count))
|
||||
}
|
||||
}()
|
||||
cell.threadMetaView.reblogButton.setTitle(reblogCountTitle, for: .normal)
|
||||
|
||||
let favoriteCountTitle: String = {
|
||||
let count = status.favouritesCount.intValue
|
||||
if count > 1 {
|
||||
return L10n.Scene.Thread.Favorite.multiple(String(count))
|
||||
} else {
|
||||
return L10n.Scene.Thread.Favorite.single(String(count))
|
||||
}
|
||||
}()
|
||||
cell.threadMetaView.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
||||
|
||||
cell.threadMetaView.isHidden = false
|
||||
}
|
||||
|
||||
|
||||
static func configureHeader(
|
||||
cell: StatusTableViewCell,
|
||||
|
@ -319,16 +393,19 @@ extension StatusSection {
|
|||
) {
|
||||
if status.reblog != nil {
|
||||
cell.statusView.headerContainerStackView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
let author = status.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userReblogged(name)
|
||||
}()
|
||||
} else if let replyTo = status.replyTo {
|
||||
} else if status.inReplyToID != nil {
|
||||
cell.statusView.headerContainerStackView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
guard let replyTo = status.replyTo else {
|
||||
return L10n.Common.Controls.Status.userRepliedTo("-")
|
||||
}
|
||||
let author = replyTo.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||
|
|
|
@ -26,7 +26,7 @@ extension CGImage {
|
|||
let pointer = CFDataGetBytePtr(data) else { return nil }
|
||||
|
||||
let length = CFDataGetLength(data)
|
||||
guard length > 0 else { return nil}
|
||||
guard length > 0 else { return nil }
|
||||
|
||||
var luma: CGFloat = 0.0
|
||||
for i in stride(from: 0, to: length, by: 4) {
|
||||
|
|
|
@ -17,4 +17,3 @@ extension UIBarButtonItem {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ extension UIImage {
|
|||
}
|
||||
}
|
||||
|
||||
public extension UIImage {
|
||||
extension UIImage {
|
||||
func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? {
|
||||
let maxRadius = min(size.width, size.height) / 2
|
||||
let cornerRadius: CGFloat = {
|
||||
|
@ -75,3 +75,18 @@ public extension UIImage {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
static func adaptiveUserInterfaceStyleImage(lightImage: UIImage, darkImage: UIImage) -> UIImage {
|
||||
let imageAsset = UIImageAsset()
|
||||
imageAsset.register(lightImage, with: UITraitCollection(traitsFrom: [
|
||||
UITraitCollection(displayScale: 1.0),
|
||||
UITraitCollection(userInterfaceStyle: .light)
|
||||
]))
|
||||
imageAsset.register(darkImage, with: UITraitCollection(traitsFrom: [
|
||||
UITraitCollection(displayScale: 1.0),
|
||||
UITraitCollection(userInterfaceStyle: .dark)
|
||||
]))
|
||||
return imageAsset.image(with: UITraitCollection.current)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,6 @@ internal enum Asset {
|
|||
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
|
||||
internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar")
|
||||
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
||||
internal static let searchResult = ColorAsset(name: "Colors/Background/searchResult")
|
||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||
|
@ -91,26 +90,35 @@ internal enum Asset {
|
|||
internal enum Connectivity {
|
||||
internal static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum Banner {
|
||||
internal static let bioEditBackgroundGray = ColorAsset(name: "Profile/Banner/bio.edit.background.gray")
|
||||
internal static let nameEditBackgroundGray = ColorAsset(name: "Profile/Banner/name.edit.background.gray")
|
||||
internal static let usernameGray = ColorAsset(name: "Profile/Banner/username.gray")
|
||||
}
|
||||
internal enum Human {
|
||||
internal static let faceSmilingAdaptive = ImageAsset(name: "Human/face.smiling.adaptive")
|
||||
}
|
||||
internal enum Welcome {
|
||||
internal enum Illustration {
|
||||
internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
|
||||
internal static let cloudBase = ImageAsset(name: "Welcome/illustration/cloud.base")
|
||||
internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Welcome/illustration/elephant.on.airplane.with.contrail")
|
||||
internal static let elephantThreeOnGrass = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass")
|
||||
internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.three")
|
||||
internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.two")
|
||||
internal enum Scene {
|
||||
internal enum Compose {
|
||||
internal static let background = ColorAsset(name: "Scene/Compose/background")
|
||||
internal static let toolbarBackground = ColorAsset(name: "Scene/Compose/toolbar.background")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum Banner {
|
||||
internal static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray")
|
||||
internal static let nameEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/name.edit.background.gray")
|
||||
internal static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray")
|
||||
}
|
||||
}
|
||||
internal enum Welcome {
|
||||
internal enum Illustration {
|
||||
internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan")
|
||||
internal static let cloudBase = ImageAsset(name: "Scene/Welcome/illustration/cloud.base")
|
||||
internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Scene/Welcome/illustration/elephant.on.airplane.with.contrail")
|
||||
internal static let elephantThreeOnGrass = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass")
|
||||
internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three")
|
||||
internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two")
|
||||
}
|
||||
internal static let mastodonLogoBlack = ImageAsset(name: "Scene/Welcome/mastodon.logo.black")
|
||||
internal static let mastodonLogoBlackLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.black.large")
|
||||
internal static let mastodonLogo = ImageAsset(name: "Scene/Welcome/mastodon.logo")
|
||||
internal static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large")
|
||||
}
|
||||
internal static let mastodonLogoBlack = ImageAsset(name: "Welcome/mastodon.logo.black")
|
||||
internal static let mastodonLogoBlackLarge = ImageAsset(name: "Welcome/mastodon.logo.black.large")
|
||||
internal static let mastodonLogo = ImageAsset(name: "Welcome/mastodon.logo")
|
||||
internal static let mastodonLogoLarge = ImageAsset(name: "Welcome/mastodon.logo.large")
|
||||
}
|
||||
}
|
||||
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||
|
|
|
@ -203,6 +203,8 @@ internal enum L10n {
|
|||
internal static let loadingMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadingMissingPosts")
|
||||
/// Load missing posts
|
||||
internal static let loadMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadMissingPosts")
|
||||
/// Show more replies
|
||||
internal static let showMoreReplies = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.ShowMoreReplies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -222,6 +224,10 @@ internal enum L10n {
|
|||
internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
|
||||
/// Type or paste what's on your mind
|
||||
internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
|
||||
/// replying to %@
|
||||
internal static func replyingToUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1))
|
||||
}
|
||||
internal enum Attachment {
|
||||
/// This %@ is broken and can't be\nuploaded to Mastodon.
|
||||
internal static func attachmentBroken(_ p1: Any) -> String {
|
||||
|
@ -257,6 +263,10 @@ internal enum L10n {
|
|||
internal static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay")
|
||||
/// 1 Hour
|
||||
internal static let oneHour = L10n.tr("Localizable", "Scene.Compose.Poll.OneHour")
|
||||
/// Option %ld
|
||||
internal static func optionNumber(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Poll.OptionNumber", p1)
|
||||
}
|
||||
/// 7 Days
|
||||
internal static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays")
|
||||
/// 6 Hours
|
||||
|
@ -581,6 +591,34 @@ internal enum L10n {
|
|||
internal static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm")
|
||||
}
|
||||
}
|
||||
internal enum Thread {
|
||||
/// Post
|
||||
internal static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle")
|
||||
/// Post from %@
|
||||
internal static func title(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Thread.Title", String(describing: p1))
|
||||
}
|
||||
internal enum Favorite {
|
||||
/// %@ favorites
|
||||
internal static func multiple(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Thread.Favorite.Multiple", String(describing: p1))
|
||||
}
|
||||
/// %@ favorite
|
||||
internal static func single(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Thread.Favorite.Single", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum Reblog {
|
||||
/// %@ reblogs
|
||||
internal static func multiple(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Thread.Reblog.Multiple", String(describing: p1))
|
||||
}
|
||||
/// %@ reblog
|
||||
internal static func single(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Thread.Reblog.Single", String(describing: p1))
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum Welcome {
|
||||
/// Social networking\nback in your hands.
|
||||
internal static let slogan = L10n.tr("Localizable", "Scene.Welcome.Slogan")
|
||||
|
|
|
@ -11,7 +11,7 @@ import ActiveLabel
|
|||
enum MastodonField {
|
||||
|
||||
static func parse(field string: String) -> ParseResult {
|
||||
let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.]+)?)")
|
||||
let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)")
|
||||
let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
|
||||
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||
|
||||
|
|
|
@ -33,6 +33,10 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
// MARK: - ActionToolbarContainerDelegate
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, replyButtonDidPressed sender: UIButton) {
|
||||
StatusProviderFacade.responseToStatusReplyAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
|
||||
StatusProviderFacade.responseToStatusReblogAction(provider: self, cell: cell)
|
||||
}
|
||||
|
@ -46,9 +50,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
guard let item = item(for: cell, indexPath: nil) else { return }
|
||||
|
||||
switch item {
|
||||
case .homeTimelineIndex(_, let attribute):
|
||||
attribute.isStatusTextSensitive = false
|
||||
case .status(_, let attribute):
|
||||
case .homeTimelineIndex(_, let attribute),
|
||||
.status(_, let attribute),
|
||||
.root(_, let attribute),
|
||||
.reply(_, let attribute),
|
||||
.leaf(_, let attribute):
|
||||
attribute.isStatusTextSensitive = false
|
||||
default:
|
||||
return
|
||||
|
@ -81,9 +87,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
guard let item = item(for: cell, indexPath: nil) else { return }
|
||||
|
||||
switch item {
|
||||
case .homeTimelineIndex(_, let attribute):
|
||||
attribute.isStatusSensitive = false
|
||||
case .status(_, let attribute):
|
||||
case .homeTimelineIndex(_, let attribute),
|
||||
.status(_, let attribute),
|
||||
.root(_, let attribute),
|
||||
.reply(_, let attribute),
|
||||
.leaf(_, let attribute):
|
||||
attribute.isStatusSensitive = false
|
||||
default:
|
||||
return
|
||||
|
|
|
@ -12,9 +12,6 @@ import os.log
|
|||
import UIKit
|
||||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
// TODO:
|
||||
// func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
// }
|
||||
|
||||
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// update poll when status appear
|
||||
|
@ -102,6 +99,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func handleTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
StatusProviderFacade.coordinateToStatusThreadScene(for: .primary, provider: self, indexPath: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {}
|
||||
|
|
|
@ -13,7 +13,7 @@ import CoreDataStack
|
|||
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
||||
// async
|
||||
func status() -> Future<Status?, Never>
|
||||
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never>
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never>
|
||||
func status(for cell: UICollectionViewCell) -> Future<Status?, Never>
|
||||
|
||||
// sync
|
||||
|
|
|
@ -60,6 +60,54 @@ extension StatusProviderFacade {
|
|||
}
|
||||
.store(in: &provider.disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, indexPath: IndexPath) {
|
||||
_coordinateToStatusThreadScene(
|
||||
for: target,
|
||||
provider: provider,
|
||||
status: provider.status(for: nil, indexPath: indexPath)
|
||||
)
|
||||
}
|
||||
|
||||
static func coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
|
||||
_coordinateToStatusThreadScene(
|
||||
for: target,
|
||||
provider: provider,
|
||||
status: provider.status(for: cell, indexPath: nil)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, status: Future<Status?, Never>) {
|
||||
status
|
||||
.sink { [weak provider] status in
|
||||
guard let provider = provider else { return }
|
||||
let _status: Status? = {
|
||||
switch target {
|
||||
case .primary: return status?.reblog ?? status // original status
|
||||
case .secondary: return status // reblog or status
|
||||
}
|
||||
}()
|
||||
guard let status = _status else { return }
|
||||
|
||||
let threadViewModel = CachedThreadViewModel(context: provider.context, status: status)
|
||||
DispatchQueue.main.async {
|
||||
if provider.navigationController == nil {
|
||||
let from = provider.presentingViewController ?? provider
|
||||
provider.dismiss(animated: true) {
|
||||
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
|
||||
}
|
||||
} else {
|
||||
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: provider, transition: .show)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &provider.disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
@ -229,7 +277,6 @@ extension StatusProviderFacade {
|
|||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
|
||||
static func responseToStatusReblogAction(provider: StatusProvider) {
|
||||
_responseToStatusReblogAction(
|
||||
|
@ -337,10 +384,41 @@ extension StatusProviderFacade {
|
|||
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func responseToStatusReplyAction(provider: StatusProvider) {
|
||||
_responseToStatusReplyAction(
|
||||
provider: provider,
|
||||
status: provider.status()
|
||||
)
|
||||
}
|
||||
|
||||
static func responseToStatusReplyAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||
_responseToStatusReplyAction(
|
||||
provider: provider,
|
||||
status: provider.status(for: cell, indexPath: nil)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _responseToStatusReplyAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||
status
|
||||
.sink { [weak provider] status in
|
||||
guard let provider = provider else { return }
|
||||
guard let status = status?.reblog ?? status else { return }
|
||||
|
||||
let composeViewModel = ComposeViewModel(context: provider.context, composeKind: .reply(repliedToStatusObjectID: status.objectID))
|
||||
provider.coordinator.present(scene: .compose(viewModel: composeViewModel), from: provider, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
.store(in: &provider.context.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
enum Target {
|
||||
case primary // original
|
||||
case secondary // attachment reblog or reply
|
||||
case primary // original status
|
||||
case secondary // wrapper status or reply (when needs. e.g tap header of status view)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,12 @@ import UIKit
|
|||
import AVKit
|
||||
|
||||
// Check List Last Updated
|
||||
// - FavoriteViewController: 2021/4/8
|
||||
// - HomeViewController: 2021/4/13
|
||||
// - FavoriteViewController: 2021/4/14
|
||||
// - HashtagTimelineViewController: 2021/4/8
|
||||
// - UserTimelineViewController: 2021/4/8
|
||||
// * StatusTableViewControllerAspect: 2021/4/7
|
||||
// - UserTimelineViewController: 2021/4/13
|
||||
// - ThreadViewController: 2021/4/13
|
||||
// * StatusTableViewControllerAspect: 2021/4/12
|
||||
|
||||
// (Fake) Aspect protocol to group common protocol extension implementations
|
||||
// Needs update related view controller when aspect interface changes
|
||||
|
@ -69,7 +71,7 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
|
|||
}
|
||||
}
|
||||
|
||||
// [B4] StatusTableViewControllerAspect.aspectTableView(_:didEndDisplaying:forRowAt:)
|
||||
// [B4] aspectTableView(_:didEndDisplaying:forRowAt:)
|
||||
extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegate & StatusProvider {
|
||||
/// [Media] hook to notify video service
|
||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
|
@ -93,6 +95,14 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
|
|||
}
|
||||
}
|
||||
|
||||
// [B5] aspectTableView(_:didSelectRowAt:)
|
||||
extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegate & StatusProvider {
|
||||
/// [UI] hook to coordinator to thread
|
||||
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
handleTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching [C]
|
||||
|
||||
// [C1] aspectTableView(:prefetchRowsAt)
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
"blue" : "0x2E",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x2C"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFE",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFE"
|
||||
"blue" : "254",
|
||||
"green" : "255",
|
||||
"red" : "254"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2E",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x2C"
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"blue" : "0xFE",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
"red" : "0xFE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x23",
|
||||
"red" : "0x1F"
|
||||
"blue" : "0x3C",
|
||||
"green" : "0x3A",
|
||||
"red" : "0x3A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x23",
|
||||
"red" : "0x1F"
|
||||
"blue" : "0x3C",
|
||||
"green" : "0x3A",
|
||||
"red" : "0x3A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
25
Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "emojiIconLight.pdf",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "emojiIconDark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
97
Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconDark.pdf
vendored
Normal file
|
@ -0,0 +1,97 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.225600 0.613812 0.894400 scn
|
||||
48.000000 0.000000 m
|
||||
74.509674 0.000000 96.000000 21.490326 96.000000 48.000000 c
|
||||
96.000000 74.509666 74.509674 96.000000 48.000000 96.000000 c
|
||||
21.490332 96.000000 0.000000 74.509666 0.000000 48.000000 c
|
||||
0.000000 21.490326 21.490332 0.000000 48.000000 0.000000 c
|
||||
h
|
||||
48.000023 39.999962 m
|
||||
38.338688 39.999962 31.928020 41.125294 24.000021 42.666630 c
|
||||
22.189354 43.015961 18.666687 42.666630 18.666687 37.333294 c
|
||||
18.666687 26.666626 30.920021 13.333298 48.000023 13.333298 c
|
||||
65.077354 13.333298 77.333359 26.666626 77.333359 37.333294 c
|
||||
77.333359 42.666630 73.810692 43.018627 72.000023 42.666630 c
|
||||
64.072021 41.125294 57.661354 39.999962 48.000023 39.999962 c
|
||||
h
|
||||
38.666645 59.999981 m
|
||||
38.666645 54.845322 35.681877 50.666649 31.999979 50.666649 c
|
||||
28.318081 50.666649 25.333313 54.845322 25.333313 59.999981 c
|
||||
25.333313 65.154640 28.318081 69.333313 31.999979 69.333313 c
|
||||
35.681877 69.333313 38.666645 65.154640 38.666645 59.999981 c
|
||||
h
|
||||
63.999977 50.666649 m
|
||||
67.681877 50.666649 70.666641 54.845322 70.666641 59.999981 c
|
||||
70.666641 65.154640 67.681877 69.333313 63.999977 69.333313 c
|
||||
60.318081 69.333313 57.333313 65.154640 57.333313 59.999981 c
|
||||
57.333313 54.845322 60.318081 50.666649 63.999977 50.666649 c
|
||||
h
|
||||
48.000000 34.666645 m
|
||||
32.000000 34.666645 24.000000 37.333313 24.000000 37.333313 c
|
||||
24.000000 37.333313 29.333334 26.666649 48.000000 26.666649 c
|
||||
66.666672 26.666649 72.000000 37.333313 72.000000 37.333313 c
|
||||
72.000000 37.333313 64.000000 34.666645 48.000000 34.666645 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1603
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 96.000000 96.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001693 00000 n
|
||||
0000001716 00000 n
|
||||
0000001889 00000 n
|
||||
0000001963 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2022
|
||||
%%EOF
|
103
Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconLight.pdf
vendored
Normal file
|
@ -0,0 +1,103 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.168627 0.564706 0.850980 scn
|
||||
90.000000 48.000000 m
|
||||
90.000000 24.804031 71.195969 6.000000 48.000000 6.000000 c
|
||||
24.804039 6.000000 6.000000 24.804031 6.000000 48.000000 c
|
||||
6.000000 71.195961 24.804041 90.000000 48.000000 90.000000 c
|
||||
71.195969 90.000000 90.000000 71.195961 90.000000 48.000000 c
|
||||
h
|
||||
48.000000 0.000000 m
|
||||
74.509674 0.000000 96.000000 21.490326 96.000000 48.000000 c
|
||||
96.000000 74.509666 74.509674 96.000000 48.000000 96.000000 c
|
||||
21.490332 96.000000 0.000000 74.509666 0.000000 48.000000 c
|
||||
0.000000 21.490326 21.490332 0.000000 48.000000 0.000000 c
|
||||
h
|
||||
38.666645 59.999981 m
|
||||
38.666645 54.845322 35.681877 50.666649 31.999979 50.666649 c
|
||||
28.318081 50.666649 25.333313 54.845322 25.333313 59.999981 c
|
||||
25.333313 65.154640 28.318081 69.333313 31.999979 69.333313 c
|
||||
35.681877 69.333313 38.666645 65.154640 38.666645 59.999981 c
|
||||
h
|
||||
63.999977 50.666649 m
|
||||
67.681877 50.666649 70.666641 54.845322 70.666641 59.999981 c
|
||||
70.666641 65.154640 67.681877 69.333313 63.999977 69.333313 c
|
||||
60.318081 69.333313 57.333313 65.154640 57.333313 59.999981 c
|
||||
57.333313 54.845322 60.318081 50.666649 63.999977 50.666649 c
|
||||
h
|
||||
48.000023 39.999962 m
|
||||
38.338688 39.999962 31.928020 41.125294 24.000021 42.666630 c
|
||||
22.189354 43.015961 18.666687 42.666630 18.666687 37.333294 c
|
||||
18.666687 26.666626 30.920021 13.333298 48.000023 13.333298 c
|
||||
65.077354 13.333298 77.333359 26.666626 77.333359 37.333294 c
|
||||
77.333359 42.666630 73.810684 43.018627 72.000023 42.666630 c
|
||||
64.072021 41.125294 57.661354 39.999962 48.000023 39.999962 c
|
||||
h
|
||||
24.000000 37.333313 m
|
||||
24.000000 37.333313 32.000000 34.666645 48.000000 34.666645 c
|
||||
64.000000 34.666645 72.000000 37.333313 72.000000 37.333313 c
|
||||
72.000000 37.333313 66.666672 26.666649 48.000000 26.666649 c
|
||||
29.333334 26.666649 24.000000 37.333313 24.000000 37.333313 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1869
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 96.000000 96.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001959 00000 n
|
||||
0000001982 00000 n
|
||||
0000002155 00000 n
|
||||
0000002229 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2288
|
||||
%%EOF
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFE",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFE"
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
"blue" : "30",
|
||||
"green" : "28",
|
||||
"red" : "28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "222",
|
||||
"green" : "216",
|
||||
"red" : "214"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "43",
|
||||
"green" : "43",
|
||||
"red" : "43"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
@ -68,6 +68,7 @@ Your account looks like this to them.";
|
|||
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended.";
|
||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||
|
@ -85,10 +86,12 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.Poll.DurationTime" = "Duration: %@";
|
||||
"Scene.Compose.Poll.OneDay" = "1 Day";
|
||||
"Scene.Compose.Poll.OneHour" = "1 Hour";
|
||||
"Scene.Compose.Poll.OptionNumber" = "Option %ld";
|
||||
"Scene.Compose.Poll.SevenDays" = "7 Days";
|
||||
"Scene.Compose.Poll.SixHours" = "6 Hours";
|
||||
"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
|
||||
"Scene.Compose.Poll.ThreeDays" = "3 Days";
|
||||
"Scene.Compose.ReplyingToUser" = "replying to %@";
|
||||
"Scene.Compose.Title.NewPost" = "New Post";
|
||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||
"Scene.Compose.Visibility.Direct" = "Only people I mention";
|
||||
|
@ -186,5 +189,11 @@ any server.";
|
|||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Thread.BackTitle" = "Post";
|
||||
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
|
||||
"Scene.Thread.Favorite.Single" = "%@ favorite";
|
||||
"Scene.Thread.Reblog.Multiple" = "%@ reblogs";
|
||||
"Scene.Thread.Reblog.Single" = "%@ reblog";
|
||||
"Scene.Thread.Title" = "Post from %@";
|
||||
"Scene.Welcome.Slogan" = "Social networking
|
||||
back in your hands.";
|
|
@ -6,9 +6,24 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let statusView = StatusView()
|
||||
|
||||
let framePublisher = PassthroughSubject<CGRect, Never>()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
statusView.isStatusTextSensitive = false
|
||||
statusView.cleanUpContentWarning()
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -19,12 +34,29 @@ final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCel
|
|||
_init()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
framePublisher.send(bounds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeRepliedToStatusContentCollectionViewCell {
|
||||
|
||||
private func _init() {
|
||||
backgroundColor = .clear
|
||||
statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
|
||||
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||
])
|
||||
|
||||
statusView.actionToolbarContainer.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// ComposeStatusAttachmentTableViewCell.swift
|
||||
// ComposeStatusAttachmentCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-17.
|
|
@ -91,7 +91,7 @@ extension ComposeStatusContentCollectionViewCell {
|
|||
|
||||
statusContentWarningEditorView.containerView.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - TextEditorViewChangeObserver
|
||||
|
|
|
@ -29,7 +29,7 @@ final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionVi
|
|||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.tertiarySystemBackground.color : Asset.Colors.Background.secondarySystemBackground.color
|
||||
pollOptionView.plusCircleImageView.tintColor = isHighlighted ? Asset.Colors.Button.normal.color.withAlphaComponent(0.5) : Asset.Colors.Button.normal.color
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
|
|||
pollOptionView.optionTextField.isHidden = true
|
||||
pollOptionView.plusCircleImageView.isHidden = false
|
||||
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
setupBorderColor()
|
||||
|
||||
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
|
||||
|
|
|
@ -31,7 +31,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
return button
|
||||
}()
|
||||
|
@ -49,7 +49,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
|
||||
collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
|
||||
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
||||
collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
||||
collectionView.alwaysBounceVertical = true
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
|
@ -66,20 +67,9 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
return view
|
||||
}()
|
||||
|
||||
let composeToolbarView: ComposeToolbarView = {
|
||||
let composeToolbarView = ComposeToolbarView()
|
||||
let text = UITextView()
|
||||
let inputView = UIInputView(frame: .init(x: 0, y: 0, width: 40, height: 40), inputViewStyle: .keyboard)
|
||||
text.inputAccessoryView = inputView
|
||||
composeToolbarView.backgroundColor = inputView.backgroundColor
|
||||
return composeToolbarView
|
||||
}()
|
||||
let composeToolbarView = ComposeToolbarView()
|
||||
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
|
||||
let composeToolbarBackgroundView: UIView = {
|
||||
let backgroundView = UIView()
|
||||
backgroundView.backgroundColor = .secondarySystemBackground
|
||||
return backgroundView
|
||||
}()
|
||||
let composeToolbarBackgroundView = UIView()
|
||||
|
||||
private(set) lazy var imagePicker: PHPickerViewController = {
|
||||
var configuration = PHPickerConfiguration()
|
||||
|
@ -135,7 +125,7 @@ extension ComposeViewController {
|
|||
self.title = title
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
view.backgroundColor = Asset.Scene.Compose.background.color
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
|
@ -202,14 +192,27 @@ extension ComposeViewController {
|
|||
)
|
||||
.sink(receiveValue: { [weak self] isShow, state, endFrame, isCustomEmojiComposing in
|
||||
guard let self = self else { return }
|
||||
|
||||
let extraMargin: CGFloat = {
|
||||
if self.view.safeAreaInsets.bottom == .zero {
|
||||
// needs extra margin for zero inset device to workaround UIKit issue
|
||||
return self.composeToolbarView.frame.height
|
||||
} else {
|
||||
// default some magic 16 extra margin
|
||||
return 16
|
||||
}
|
||||
}()
|
||||
|
||||
// update keyboard background color
|
||||
|
||||
guard isShow, state == .dock else {
|
||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom
|
||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
|
||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
self.updateKeyboardBackground(isKeyboardDisplay: isShow)
|
||||
return
|
||||
}
|
||||
// isShow AND dock state
|
||||
|
@ -218,22 +221,23 @@ extension ComposeViewController {
|
|||
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
||||
let padding = contentFrame.maxY - endFrame.minY
|
||||
guard padding > 0 else {
|
||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom
|
||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
|
||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
||||
return
|
||||
}
|
||||
|
||||
// add 16pt margin
|
||||
self.collectionView.contentInset.bottom = padding + 16
|
||||
self.collectionView.verticalScrollIndicatorInsets.bottom = padding + 16
|
||||
self.collectionView.contentInset.bottom = padding + extraMargin
|
||||
self.collectionView.verticalScrollIndicatorInsets.bottom = padding + extraMargin
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.composeToolbarViewBottomLayoutConstraint.constant = padding
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
self.updateKeyboardBackground(isKeyboardDisplay: isShow)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -266,13 +270,17 @@ extension ComposeViewController {
|
|||
.store(in: &disposeBag)
|
||||
|
||||
// bind visibility toolbar UI
|
||||
viewModel.selectedStatusVisibility
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] type in
|
||||
guard let self = self else { return }
|
||||
self.composeToolbarView.visibilityButton.setImage(type.image, for: .normal)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
Publishers.CombineLatest(
|
||||
viewModel.selectedStatusVisibility,
|
||||
viewModel.traitCollectionDidChangePublisher
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] type, _ in
|
||||
guard let self = self else { return }
|
||||
let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
|
||||
self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.characterCount
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -324,6 +332,24 @@ extension ComposeViewController {
|
|||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// setup snap behavior
|
||||
Publishers.CombineLatest(
|
||||
viewModel.repliedToCellFrame.removeDuplicates().eraseToAnyPublisher(),
|
||||
viewModel.collectionViewState.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] repliedToCellFrame, collectionViewState in
|
||||
guard let self = self else { return }
|
||||
guard repliedToCellFrame != .zero else { return }
|
||||
switch collectionViewState {
|
||||
case .fold:
|
||||
self.collectionView.contentInset.top = -repliedToCellFrame.height
|
||||
case .expand:
|
||||
self.collectionView.contentInset.top = 0
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -336,6 +362,12 @@ extension ComposeViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
viewModel.traitCollectionDidChangePublisher.send()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
|
@ -463,6 +495,20 @@ extension ComposeViewController {
|
|||
imagePicker.delegate = self
|
||||
return imagePicker
|
||||
}
|
||||
|
||||
private func updateKeyboardBackground(isKeyboardDisplay: Bool) {
|
||||
guard isKeyboardDisplay else {
|
||||
composeToolbarBackgroundView.backgroundColor = Asset.Scene.Compose.toolbarBackground.color
|
||||
return
|
||||
}
|
||||
composeToolbarBackgroundView.backgroundColor = UIColor(dynamicProvider: { traitCollection -> UIColor in
|
||||
// avoid elevated color
|
||||
switch traitCollection.userInterfaceStyle {
|
||||
case .light: return .white
|
||||
default: return .black
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -538,7 +584,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||
|
||||
let stringRange = NSRange(location: 0, length: string.length)
|
||||
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.]+)?|#([^\\s.]+))")
|
||||
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))")
|
||||
// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
||||
// precondition :\B with following space
|
||||
let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
|
||||
|
@ -727,6 +773,32 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension ComposeViewController {
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard scrollView === collectionView else { return }
|
||||
|
||||
let repliedToCellFrame = viewModel.repliedToCellFrame.value
|
||||
guard repliedToCellFrame != .zero else { return }
|
||||
let throttle = viewModel.repliedToCellFrame.value.height - scrollView.adjustedContentInset.top
|
||||
// print("\(throttle) - \(scrollView.contentOffset.y)")
|
||||
|
||||
switch viewModel.collectionViewState.value {
|
||||
case .fold:
|
||||
if scrollView.contentOffset.y < throttle {
|
||||
viewModel.collectionViewState.value = .expand
|
||||
}
|
||||
os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
case .expand:
|
||||
if scrollView.contentOffset.y > -44 {
|
||||
viewModel.collectionViewState.value = .fold
|
||||
os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension ComposeViewController: UICollectionViewDelegate {
|
||||
|
||||
|
@ -763,6 +835,10 @@ extension ComposeViewController: UICollectionViewDelegate {
|
|||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
return .fullScreen
|
||||
}
|
||||
|
||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||
return viewModel.shouldDismiss.value
|
||||
|
|
|
@ -27,6 +27,7 @@ extension ComposeViewModel {
|
|||
dependency: dependency,
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
composeKind: composeKind,
|
||||
repliedToCellFrameSubscriber: repliedToCellFrame,
|
||||
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
|
@ -64,6 +65,15 @@ extension ComposeViewModel.PublishState {
|
|||
guard viewModel.isPollComposing.value else { return nil }
|
||||
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||
}()
|
||||
let inReplyToID: Mastodon.Entity.Status.ID? = {
|
||||
guard case let .reply(repliedToStatusObjectID) = viewModel.composeKind else { return nil }
|
||||
var id: Mastodon.Entity.Status.ID?
|
||||
viewModel.context.managedObjectContext.performAndWait {
|
||||
guard let replyTo = viewModel.context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
|
||||
id = replyTo.id
|
||||
}
|
||||
return id
|
||||
}()
|
||||
let sensitive: Bool = viewModel.isContentWarningComposing.value
|
||||
let spoilerText: String? = {
|
||||
let text = viewModel.composeStatusAttribute.contentWarningContent.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
@ -105,6 +115,7 @@ extension ComposeViewModel.PublishState {
|
|||
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||
pollOptions: pollOptions,
|
||||
pollExpiresIn: pollExpiresIn,
|
||||
inReplyToID: inReplyToID,
|
||||
sensitive: sensitive,
|
||||
spoilerText: spoilerText,
|
||||
visibility: visibility
|
||||
|
|
|
@ -26,9 +26,11 @@ final class ComposeViewModel {
|
|||
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let selectedStatusVisibility = CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>(.public)
|
||||
let selectedStatusVisibility: CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>
|
||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
||||
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
|
||||
// output
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||
|
@ -55,6 +57,7 @@ final class ComposeViewModel {
|
|||
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let characterCount = CurrentValueSubject<Int, Never>(0)
|
||||
let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
|
||||
|
||||
// for hashtag: #<hashag>' '
|
||||
// for mention: @<mention>' '
|
||||
|
@ -83,10 +86,40 @@ final class ComposeViewModel {
|
|||
case .post, .hashtag, .mention: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newPost)
|
||||
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
|
||||
}
|
||||
self.selectedStatusVisibility = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value?.user.locked == true ? .private : .public)
|
||||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||
// end init
|
||||
if case let .hashtag(text) = composeKind {
|
||||
if case let .reply(repliedToStatusObjectID) = composeKind {
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
|
||||
let composeAuthor: MastodonUser? = {
|
||||
guard let objectID = self.activeAuthentication.value?.user.objectID else { return nil }
|
||||
guard let author = context.managedObjectContext.object(with: objectID) as? MastodonUser else { return nil }
|
||||
return author
|
||||
}()
|
||||
|
||||
var mentionAccts: [String] = []
|
||||
if composeAuthor?.id != status.author.id {
|
||||
mentionAccts.append("@" + status.author.acct)
|
||||
}
|
||||
let mentions = (status.mentions ?? Set())
|
||||
.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||
.filter { $0.id != composeAuthor?.id }
|
||||
for mention in mentions {
|
||||
mentionAccts.append("@" + mention.acct)
|
||||
}
|
||||
for acct in mentionAccts {
|
||||
UITextChecker.learnWord(acct)
|
||||
}
|
||||
|
||||
let initialComposeContent = mentionAccts.joined(separator: " ")
|
||||
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
|
||||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent.value = preInsertedContent
|
||||
}
|
||||
|
||||
} else if case let .hashtag(text) = composeKind {
|
||||
let initialComposeContent = "#" + text
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
|
@ -346,6 +379,13 @@ final class ComposeViewModel {
|
|||
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
enum CollectionViewState {
|
||||
case fold // snap to input
|
||||
case expand // snap to reply
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
func createNewPollOptionIfPossible() {
|
||||
guard pollOptionAttributes.value.count < 4 else { return }
|
||||
|
|
|
@ -41,7 +41,10 @@ final class ComposeToolbarView: UIView {
|
|||
let emojiButton: UIButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||
let image = Asset.Human.faceSmilingAdaptive.image
|
||||
.af.imageScaled(to: CGSize(width: 20, height: 20))
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
button.setImage(image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -80,8 +83,12 @@ final class ComposeToolbarView: UIView {
|
|||
}
|
||||
|
||||
extension ComposeToolbarView {
|
||||
|
||||
private func _init() {
|
||||
backgroundColor = .secondarySystemBackground
|
||||
// magic keyboard color (iOS 14):
|
||||
// light with white background: RGB 214 216 222
|
||||
// dark with black background: RGB 43 43 43
|
||||
backgroundColor = Asset.Scene.Compose.toolbarBackground.color
|
||||
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
|
@ -125,9 +132,18 @@ extension ComposeToolbarView {
|
|||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
|
||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
||||
visibilityButton.menu = createVisibilityContextMenu()
|
||||
visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle)
|
||||
visibilityButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
updateToolbarButtonUserInterfaceStyle()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
updateToolbarButtonUserInterfaceStyle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeToolbarView {
|
||||
|
@ -152,12 +168,16 @@ extension ComposeToolbarView {
|
|||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage {
|
||||
switch self {
|
||||
case .public: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
|
||||
case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
|
||||
case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
|
||||
case .public:
|
||||
switch interfaceStyle {
|
||||
case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
}
|
||||
case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,6 +202,23 @@ extension ComposeToolbarView {
|
|||
button.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
private func updateToolbarButtonUserInterfaceStyle() {
|
||||
switch traitCollection.userInterfaceStyle {
|
||||
case .light:
|
||||
mediaButton.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
|
||||
contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
|
||||
|
||||
case .dark:
|
||||
mediaButton.setImage(UIImage(systemName: "photo.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
|
||||
contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle)
|
||||
}
|
||||
|
||||
private func createMediaContextMenu() -> UIMenu {
|
||||
var children: [UIMenuElement] = []
|
||||
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||
|
@ -208,9 +245,9 @@ extension ComposeToolbarView {
|
|||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
}
|
||||
|
||||
private func createVisibilityContextMenu() -> UIMenu {
|
||||
private func createVisibilityContextMenu(interfaceStyle: UIUserInterfaceStyle) -> UIMenu {
|
||||
let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in
|
||||
UIAction(title: type.title, image: type.image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||
UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue)
|
||||
self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type)
|
||||
|
|
|
@ -18,14 +18,14 @@ extension HashtagTimelineViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
|
|
|
@ -57,7 +57,7 @@ extension HashtagTimelineViewController {
|
|||
titleView.update(title: viewModel.hashtag, subtitle: nil)
|
||||
navigationItem.titleView = titleView
|
||||
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
|
||||
navigationItem.rightBarButtonItem = composeBarButtonItem
|
||||
|
||||
|
@ -218,6 +218,10 @@ extension HashtagTimelineViewController: UITableViewDelegate {
|
|||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
|
|
|
@ -28,7 +28,8 @@ extension HashtagTimelineViewModel {
|
|||
managedObjectContext: context.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
|
||||
threadReplyLoaderTableViewCellDelegate: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,10 @@ extension HomeTimelineViewController {
|
|||
guard let self = self else { return }
|
||||
self.showProfileAction(action)
|
||||
},
|
||||
UIAction(title: "Show Thread", image: UIImage(systemName: "bubble.left.and.bubble.right"), attributes: []) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.showThreadAction(action)
|
||||
},
|
||||
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.signOutAction(action)
|
||||
|
@ -304,5 +308,20 @@ extension HomeTimelineViewController {
|
|||
coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
@objc private func showThreadAction(_ sender: UIAction) {
|
||||
let alertController = UIAlertController(title: "Enter Status ID", message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField()
|
||||
let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in
|
||||
guard let self = self else { return }
|
||||
guard let textField = alertController?.textFields?.first else { return }
|
||||
let threadViewModel = RemoteThreadViewModel(context: self.context, statusID: textField.text ?? "")
|
||||
self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show)
|
||||
}
|
||||
alertController.addAction(showAction)
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -18,14 +18,14 @@ extension HomeTimelineViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
|
|
|
@ -47,7 +47,6 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency {
|
|||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
@ -71,7 +70,7 @@ extension HomeTimelineViewController {
|
|||
super.viewDidLoad()
|
||||
|
||||
title = L10n.Scene.HomeTimeline.title
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
navigationItem.leftBarButtonItem = settingBarButtonItem
|
||||
navigationItem.titleView = titleView
|
||||
titleView.delegate = self
|
||||
|
@ -179,6 +178,8 @@ extension HomeTimelineViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
aspectViewWillAppear(animated)
|
||||
|
||||
// needs trigger manually after onboarding dismiss
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
|
@ -198,8 +199,8 @@ extension HomeTimelineViewController {
|
|||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
context.videoPlaybackService.viewDidDisappear(from: self)
|
||||
context.audioPlaybackService.viewDidDisappear(from: self)
|
||||
|
||||
aspectViewDidDisappear(animated)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
|
@ -262,11 +263,19 @@ extension HomeTimelineViewController {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerAspect
|
||||
extension HomeTimelineViewController: StatusTableViewControllerAspect { }
|
||||
|
||||
extension HomeTimelineViewController: TableViewCellHeightCacheableContainer {
|
||||
var cellFrameCache: NSCache<NSNumber, NSValue> { return viewModel.cellFrameCache }
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension HomeTimelineViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
handleScrollViewDidScroll(scrollView)
|
||||
self.viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
|
||||
aspectScrollViewDidScroll(scrollView)
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,32 +290,26 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
|||
extension HomeTimelineViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 200
|
||||
// TODO:
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||
//
|
||||
// guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||
// return 200
|
||||
// }
|
||||
// // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
|
||||
//
|
||||
// return ceil(frame.height)
|
||||
aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||
aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
handleTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
aspectTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,7 +320,6 @@ extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControl
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||
extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
||||
|
|
|
@ -29,7 +29,8 @@ extension HomeTimelineViewModel {
|
|||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
|
||||
threadReplyLoaderTableViewCellDelegate: nil
|
||||
)
|
||||
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
|
@ -88,6 +89,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
|
||||
for (i, timelineIndex) in timelineIndexes.enumerated() {
|
||||
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute()
|
||||
attribute.isSeparatorLineHidden = false
|
||||
|
||||
// append new item into snapshot
|
||||
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
||||
|
@ -96,6 +98,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
switch (isLast, timelineIndex.hasMore) {
|
||||
case (false, true):
|
||||
newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID))
|
||||
attribute.isSeparatorLineHidden = true
|
||||
case (true, true):
|
||||
shouldAddBottomLoader = true
|
||||
default:
|
||||
|
|
|
@ -16,14 +16,14 @@ final class WelcomeIllustrationView: UIView {
|
|||
let leftHillImageView = UIImageView()
|
||||
let centerHillImageView = UIImageView()
|
||||
|
||||
private let cloudBaseImage = Asset.Welcome.Illustration.cloudBase.image
|
||||
private let elephantThreeOnGrassWithTreeTwoImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image
|
||||
private let elephantThreeOnGrassWithTreeThreeImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image
|
||||
private let elephantThreeOnGrassImage = Asset.Welcome.Illustration.elephantThreeOnGrass.image
|
||||
private let cloudBaseImage = Asset.Scene.Welcome.Illustration.cloudBase.image
|
||||
private let elephantThreeOnGrassWithTreeTwoImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image
|
||||
private let elephantThreeOnGrassWithTreeThreeImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image
|
||||
private let elephantThreeOnGrassImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrass.image
|
||||
|
||||
// layout outside
|
||||
let elephantOnAirplaneWithContrailImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
|
||||
let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
@ -43,7 +43,7 @@ final class WelcomeIllustrationView: UIView {
|
|||
extension WelcomeIllustrationView {
|
||||
|
||||
private func _init() {
|
||||
backgroundColor = Asset.Welcome.Illustration.backgroundCyan.color
|
||||
backgroundColor = Asset.Scene.Welcome.Illustration.backgroundCyan.color
|
||||
|
||||
let topPaddingView = UIView()
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
|||
var welcomeIllustrationViewBottomAnchorLayoutConstraint: NSLayoutConstraint?
|
||||
|
||||
private(set) lazy var logoImageView: UIImageView = {
|
||||
let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Welcome.mastodonLogo.image : Asset.Welcome.mastodonLogoBlackLarge.image
|
||||
let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Scene.Welcome.mastodonLogo.image : Asset.Scene.Welcome.mastodonLogoBlackLarge.image
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
|
|
|
@ -10,8 +10,8 @@ import CoreDataStack
|
|||
|
||||
final class CachedProfileViewModel: ProfileViewModel {
|
||||
|
||||
convenience init(context: AppContext, mastodonUser: MastodonUser) {
|
||||
self.init(context: context, optionalMastodonUser: mastodonUser)
|
||||
init(context: AppContext, mastodonUser: MastodonUser) {
|
||||
super.init(context: context, optionalMastodonUser: mastodonUser)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,14 +18,14 @@ extension FavoriteViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
|
|
|
@ -45,7 +45,7 @@ extension FavoriteViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
navigationItem.titleView = titleView
|
||||
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil)
|
||||
|
||||
|
@ -114,6 +114,10 @@ extension FavoriteViewController: UITableViewDelegate {
|
|||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
|
|
|
@ -25,7 +25,8 @@ extension FavoriteViewModel {
|
|||
managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: nil
|
||||
timelineMiddleLoaderTableViewCellDelegate: nil,
|
||||
threadReplyLoaderTableViewCellDelegate: nil
|
||||
)
|
||||
|
||||
// set empty section to make update animation top-to-bottom style
|
||||
|
|
|
@ -100,7 +100,7 @@ final class ProfileHeaderView: UIView {
|
|||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.textColor = Asset.Profile.Banner.usernameGray.color
|
||||
label.textColor = Asset.Scene.Profile.Banner.usernameGray.color
|
||||
label.text = "@alice"
|
||||
label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
|
||||
return label
|
||||
|
@ -131,7 +131,7 @@ final class ProfileHeaderView: UIView {
|
|||
textEditorView.scrollView.isScrollEnabled = false
|
||||
textEditorView.isScrollEnabled = false
|
||||
textEditorView.font = .preferredFont(forTextStyle: .body)
|
||||
textEditorView.backgroundColor = Asset.Profile.Banner.bioEditBackgroundGray.color
|
||||
textEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||
textEditorView.layer.masksToBounds = true
|
||||
textEditorView.layer.cornerCurve = .continuous
|
||||
textEditorView.layer.cornerRadius = 10
|
||||
|
@ -356,9 +356,9 @@ extension ProfileHeaderView {
|
|||
bioTextEditorView.backgroundColor = .clear
|
||||
animator.addAnimations {
|
||||
self.bannerImageViewOverlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
|
||||
self.nameTextFieldBackgroundView.backgroundColor = Asset.Profile.Banner.nameEditBackgroundGray.color
|
||||
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
|
||||
self.editAvatarBackgroundView.alpha = 1
|
||||
self.bioTextEditorView.backgroundColor = Asset.Profile.Banner.bioEditBackgroundGray.color
|
||||
self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ final class ProfileRelationshipActionButton: RoundedEdgesButton {
|
|||
|
||||
extension ProfileRelationshipActionButton {
|
||||
private func _init() {
|
||||
titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
|
||||
actvityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(actvityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -625,6 +625,11 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
|||
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController postTimelineViewController: ScrollViewContainer, atIndex index: Int) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
|
||||
|
||||
// update segemented control
|
||||
if index < profileHeaderViewController.pageSegmentedControl.numberOfSegments {
|
||||
profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = index
|
||||
}
|
||||
|
||||
// save content offset
|
||||
overlayScrollView.contentOffset.y = contentOffsets[index] ?? containerScrollView.contentOffset.y
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ import MastodonSDK
|
|||
|
||||
final class RemoteProfileViewModel: ProfileViewModel {
|
||||
|
||||
convenience init(context: AppContext, userID: Mastodon.Entity.Account.ID) {
|
||||
self.init(context: context, optionalMastodonUser: nil)
|
||||
init(context: AppContext, userID: Mastodon.Entity.Account.ID) {
|
||||
super.init(context: context, optionalMastodonUser: nil)
|
||||
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
|
@ -47,8 +47,6 @@ final class RemoteProfileViewModel: ProfileViewModel {
|
|||
self.mastodonUser.value = mastodonUser
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -18,14 +18,14 @@ extension UserTimelineViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
|
|
|
@ -45,7 +45,7 @@ extension UserTimelineViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
|
@ -124,6 +124,10 @@ extension UserTimelineViewController: UITableViewDelegate {
|
|||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
|
|
|
@ -25,7 +25,8 @@ extension UserTimelineViewModel {
|
|||
managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: nil
|
||||
timelineMiddleLoaderTableViewCellDelegate: nil,
|
||||
threadReplyLoaderTableViewCellDelegate: nil
|
||||
)
|
||||
|
||||
// set empty section to make update animation top-to-bottom style
|
||||
|
|
|
@ -19,14 +19,14 @@ extension PublicTimelineViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
|
|
|
@ -28,7 +28,8 @@ extension PublicTimelineViewModel {
|
|||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
|
||||
threadReplyLoaderTableViewCellDelegate: nil
|
||||
)
|
||||
items.value = []
|
||||
stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
|
||||
|
|