Merge branch 'develop' into feature/settings-rebase

# Conflicts:
#	CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
#	Localization/app.json
#	Mastodon/Generated/Assets.swift
#	Mastodon/Generated/Strings.swift
#	Mastodon/Resources/en.lproj/Localizable.strings
#	Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
This commit is contained in:
ihugo 2021-04-17 14:13:01 +08:00
commit f6dabb5e36
122 changed files with 2634 additions and 307 deletions

View File

@ -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"/>
@ -236,16 +237,17 @@
<element name="Emoji" positionX="0" positionY="0" width="128" height="149"/>
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="629"/>
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
<element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
<element name="Setting" positionX="72" positionY="162" 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="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"/>
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
<element name="Setting" positionX="72" positionY="162" width="128" height="134"/>
<element name="Subscription" positionX="81" positionY="171" width="128" height="149"/>
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="149"/>
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
</elements>
</model>

View File

@ -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

View File

@ -94,7 +94,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",
@ -203,7 +204,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"
@ -244,6 +245,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",
@ -258,7 +260,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..."
@ -327,6 +330,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"
}
},
"settings": {
"title": "Settings",
"section": {
@ -363,4 +378,4 @@
}
}
}
}
}

View File

@ -155,6 +155,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 */; };
@ -229,7 +231,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 */; };
@ -274,6 +276,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 */; };
@ -317,6 +328,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 */; };
@ -546,6 +558,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>"; };
@ -626,7 +640,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>"; };
@ -673,6 +687,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>"; };
@ -715,6 +738,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>"; };
@ -887,6 +911,7 @@
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */,
0F1E2D0A2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift */,
DBB9759B262462E1004620BD /* ThreadMetaView.swift */,
);
path = Content;
sourceTree = "<group>";
@ -1092,9 +1117,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;
@ -1361,6 +1388,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 */,
@ -1483,7 +1511,7 @@
children = (
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */,
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
@ -1611,6 +1639,7 @@
DB9D6BFD25E4F57B0051B173 /* Notification */,
DB9D6C0825E4F5A60051B173 /* Profile */,
DB789A1025F9F29B0071ACA0 /* Compose */,
DB938EEB2623F52600E5B6C1 /* Thread */,
);
path = Scene;
sourceTree = "<group>";
@ -1651,6 +1680,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 = (
@ -1757,6 +1800,7 @@
children = (
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */,
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */,
);
path = Control;
sourceTree = "<group>";
@ -2257,6 +2301,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 */,
@ -2265,6 +2310,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 */,
@ -2278,6 +2324,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 */,
@ -2301,13 +2348,14 @@
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
5B90C461262599800002E742 /* SettingsLinkTableViewCell.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 */,
@ -2324,6 +2372,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 */,
@ -2394,8 +2443,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 */,
@ -2413,11 +2464,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 */,
@ -2466,6 +2519,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 */,
@ -2487,7 +2541,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 */,

View File

@ -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>

View File

@ -51,6 +51,9 @@ extension SceneCoordinator {
// compose
case compose(viewModel: ComposeViewModel)
// thread
case thread(viewModel: ThreadViewModel)
// Hashtag Timeline
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
@ -227,6 +230,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

View File

@ -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):

View File

@ -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
) {

View File

@ -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)

View File

@ -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) {

View File

@ -17,4 +17,3 @@ extension UIBarButtonItem {
}
}

View File

@ -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)
}
}

View File

@ -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")
@ -81,7 +80,6 @@ internal enum Asset {
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
internal static let valid = ColorAsset(name: "Colors/TextField/valid")
}
internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey")
internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
internal static let danger = ColorAsset(name: "Colors/danger")
internal static let disabled = ColorAsset(name: "Colors/disabled")
@ -92,31 +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 Settings {
internal static let appearanceAutomatic = ImageAsset(name: "Settings/appearance.automatic")
internal static let appearanceDark = ImageAsset(name: "Settings/appearance.dark")
internal static let appearanceLight = ImageAsset(name: "Settings/appearance.light")
}
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

View File

@ -35,14 +35,6 @@ internal enum L10n {
/// Server Error
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
}
internal enum SignOut {
/// Sign Out
internal static let confirm = L10n.tr("Localizable", "Common.Alerts.SignOut.Confirm")
/// Are you sure you want to sign out?
internal static let message = L10n.tr("Localizable", "Common.Alerts.SignOut.Message")
/// Sign out
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignOut.Title")
}
internal enum SignUpFailure {
/// Sign Up Failure
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
@ -211,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")
}
}
}
@ -230,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 {
@ -265,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
@ -589,59 +591,31 @@ internal enum L10n {
internal static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm")
}
}
internal enum Settings {
/// Settings
internal static let title = L10n.tr("Localizable", "Scene.Settings.Title")
internal enum Section {
internal enum Appearance {
/// Automatic
internal static let automatic = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Automatic")
/// Always Dark
internal static let dark = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Dark")
/// Always Light
internal static let light = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Light")
/// Appearance
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title")
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))
}
internal enum Boringzone {
/// Privacy Policy
internal static let privacy = L10n.tr("Localizable", "Scene.Settings.Section.Boringzone.Privacy")
/// Terms of Service
internal static let terms = L10n.tr("Localizable", "Scene.Settings.Section.Boringzone.Terms")
/// The Boring zone
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Boringzone.Title")
/// %@ favorite
internal static func single(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Thread.Favorite.Single", String(describing: p1))
}
internal enum Notifications {
/// Reblogs my post
internal static let boosts = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Boosts")
/// Favorites my post
internal static let favorites = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Favorites")
/// Follows me
internal static let follows = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Follows")
/// Mentions me
internal static let mentions = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Mentions")
/// Notifications
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Title")
internal enum Trigger {
/// anyone
internal static let anyone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Anyone")
/// anyone I follow
internal static let follow = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follow")
/// a follower
internal static let follower = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follower")
/// no one
internal static let noone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Noone")
/// Notify me when
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Title")
}
}
internal enum Reblog {
/// %@ reblogs
internal static func multiple(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Thread.Reblog.Multiple", String(describing: p1))
}
internal enum Spicyzone {
/// Clear Media Cache
internal static let clear = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Clear")
/// Sign Out
internal static let signout = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Signout")
/// The spicy zone
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Title")
/// %@ reblog
internal static func single(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Thread.Reblog.Single", String(describing: p1))
}
}
}

View File

@ -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)")

View File

@ -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

View File

@ -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 {}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View 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
}
}

View 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

View 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

View File

@ -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"

View File

@ -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
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -71,6 +71,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
@ -88,10 +89,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";
@ -210,5 +213,11 @@ any server.";
"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
"Scene.Settings.Title" = "Settings";
"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.";
back in your hands.";

View File

@ -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
}
}

View File

@ -1,5 +1,5 @@
//
// ComposeStatusAttachmentTableViewCell.swift
// ComposeStatusAttachmentCollectionViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-17.

View File

@ -91,7 +91,7 @@ extension ComposeStatusContentCollectionViewCell {
statusContentWarningEditorView.containerView.isHidden = true
}
}
// MARK: - TextEditorViewChangeObserver

View File

@ -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)

View File

@ -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

View File

@ -27,6 +27,7 @@ extension ComposeViewModel {
dependency: dependency,
managedObjectContext: context.managedObjectContext,
composeKind: composeKind,
repliedToCellFrameSubscriber: repliedToCellFrame,
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,

View File

@ -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

View File

@ -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 }

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -28,7 +28,8 @@ extension HashtagTimelineViewModel {
managedObjectContext: context.managedObjectContext,
timestampUpdatePublisher: timestampUpdatePublisher,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
threadReplyLoaderTableViewCellDelegate: nil
)
}
}

View File

@ -33,8 +33,9 @@ extension HomeTimelineViewController {
guard let self = self else { return }
self.showProfileAction(action)
},
UIAction(title: "Settings", image: UIImage(systemName: "escape"), attributes: []) { [weak self] action in
self?.coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil))
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 }
@ -307,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

View File

@ -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

View File

@ -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?) {

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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([

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More