forked from zelo72/mastodon-ios
feat: handle blocking and blocked state for profile
This commit is contained in:
parent
824d214ce7
commit
9612cc3902
|
@ -6,3 +6,15 @@ Mastodon localization template file
|
||||||
## How to contribute?
|
## How to contribute?
|
||||||
|
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
|
## How to maintains
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
// enter workdir
|
||||||
|
cd Mastodon
|
||||||
|
// edit i18n json
|
||||||
|
open ./Localization/app.json
|
||||||
|
// update resource
|
||||||
|
update_localization.sh
|
||||||
|
|
||||||
|
```
|
|
@ -86,6 +86,12 @@
|
||||||
"loader": {
|
"loader": {
|
||||||
"load_missing_posts": "Load missing posts",
|
"load_missing_posts": "Load missing posts",
|
||||||
"loading_missing_posts": "Loading missing posts..."
|
"loading_missing_posts": "Loading missing posts..."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"no_status_found": "No Status Found",
|
||||||
|
"blocking_warning": "You can’t view Artbot’s profile\n until you unblock them.\nYour account looks like this to them.",
|
||||||
|
"blocked_warning": "You can’t view Artbot’s profile\n until they unblock you.",
|
||||||
|
"suspended_warning": "This account is suspended."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -244,7 +244,6 @@
|
||||||
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; };
|
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; };
|
||||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
|
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
|
||||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; };
|
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; };
|
||||||
DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488326034BD7008B817C /* APIService+Status.swift */; };
|
|
||||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
|
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
|
||||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; };
|
DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; };
|
||||||
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; };
|
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; };
|
||||||
|
@ -295,6 +294,8 @@
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
||||||
|
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */; };
|
||||||
|
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */; };
|
||||||
DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; };
|
DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; };
|
||||||
DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -647,6 +648,8 @@
|
||||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
||||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderView.swift; sourceTree = "<group>"; };
|
||||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
||||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
|
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
|
||||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
@ -780,6 +783,7 @@
|
||||||
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
||||||
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
|
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
|
||||||
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
|
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
|
||||||
|
DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */,
|
||||||
);
|
);
|
||||||
path = Content;
|
path = Content;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -982,6 +986,7 @@
|
||||||
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
||||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
||||||
|
DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */,
|
||||||
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
|
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = TableviewCell;
|
path = TableviewCell;
|
||||||
|
@ -2022,6 +2027,7 @@
|
||||||
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */,
|
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */,
|
||||||
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */,
|
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */,
|
||||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
||||||
|
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
||||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
||||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
|
@ -2088,6 +2094,7 @@
|
||||||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||||
|
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
||||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
|
@ -2153,7 +2160,6 @@
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||||
DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */,
|
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
|
|
|
@ -116,7 +116,7 @@ extension SceneCoordinator {
|
||||||
if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
|
if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
|
||||||
switch viewController {
|
switch viewController {
|
||||||
case is ProfileViewController:
|
case is ProfileViewController:
|
||||||
let barButtonItem = UIBarButtonItem(title: navigationControllerVisibleViewController.title, style: .plain, target: nil, action: nil)
|
let barButtonItem = UIBarButtonItem(title: navigationControllerVisibleViewController.navigationItem.title, style: .plain, target: nil, action: nil)
|
||||||
barButtonItem.tintColor = .white
|
barButtonItem.tintColor = .white
|
||||||
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
|
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -22,6 +22,8 @@ enum Item {
|
||||||
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
||||||
case publicMiddleLoader(statusID: String)
|
case publicMiddleLoader(statusID: String)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
|
|
||||||
|
case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol StatusContentWarningAttribute {
|
protocol StatusContentWarningAttribute {
|
||||||
|
@ -56,6 +58,30 @@ extension Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EmptyStateHeaderAttribute: Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
let reason: Reason
|
||||||
|
|
||||||
|
enum Reason {
|
||||||
|
case noStatusFound
|
||||||
|
case blocking
|
||||||
|
case blocked
|
||||||
|
case suspended
|
||||||
|
}
|
||||||
|
|
||||||
|
init(reason: Reason) {
|
||||||
|
self.reason = reason
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: Item.EmptyStateHeaderAttribute, rhs: Item.EmptyStateHeaderAttribute) -> Bool {
|
||||||
|
return lhs.reason == rhs.reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item: Equatable {
|
extension Item: Equatable {
|
||||||
|
@ -65,12 +91,14 @@ extension Item: Equatable {
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
|
case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
case (.bottomLoader, .bottomLoader):
|
|
||||||
return true
|
|
||||||
case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
|
|
||||||
return upperLeft == upperRight
|
|
||||||
case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
|
case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
|
||||||
return upperLeft == upperRight
|
return upperLeft == upperRight
|
||||||
|
case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
|
||||||
|
return upperLeft == upperRight
|
||||||
|
case (.bottomLoader, .bottomLoader):
|
||||||
|
return true
|
||||||
|
case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
|
||||||
|
return attributeLeft == attributeRight
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -84,14 +112,16 @@ extension Item: Hashable {
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
case .status(let objectID, _):
|
case .status(let objectID, _):
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
case .publicMiddleLoader(let upper):
|
|
||||||
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
|
||||||
hasher.combine(upper)
|
|
||||||
case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
|
case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
|
||||||
hasher.combine(String(describing: Item.homeMiddleLoader.self))
|
hasher.combine(String(describing: Item.homeMiddleLoader.self))
|
||||||
hasher.combine(upper)
|
hasher.combine(upper)
|
||||||
|
case .publicMiddleLoader(let upper):
|
||||||
|
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
||||||
|
hasher.combine(upper)
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||||
|
case .emptyStateHeader(let attribute):
|
||||||
|
hasher.combine(attribute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,16 +27,16 @@ extension CategoryPickerSection {
|
||||||
cell.categoryView.titleLabel.text = item.title
|
cell.categoryView.titleLabel.text = item.title
|
||||||
cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in
|
cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in
|
||||||
if cell.isSelected {
|
if cell.isSelected {
|
||||||
cell.categoryView.bgView.backgroundColor = Asset.Colors.lightBrandBlue.color
|
cell.categoryView.bgView.backgroundColor = Asset.Colors.brandBlue.color
|
||||||
cell.categoryView.bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0)
|
cell.categoryView.bgView.applyShadow(color: Asset.Colors.brandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0)
|
||||||
if case .all = item {
|
if case .all = item {
|
||||||
cell.categoryView.titleLabel.textColor = Asset.Colors.lightWhite.color
|
cell.categoryView.titleLabel.textColor = .white
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cell.categoryView.bgView.backgroundColor = Asset.Colors.lightWhite.color
|
cell.categoryView.bgView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
cell.categoryView.bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0)
|
cell.categoryView.bgView.applyShadow(color: Asset.Colors.brandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0)
|
||||||
if case .all = item {
|
if case .all = item {
|
||||||
cell.categoryView.titleLabel.textColor = Asset.Colors.lightBrandBlue.color
|
cell.categoryView.titleLabel.textColor = Asset.Colors.brandBlue.color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,12 +79,17 @@ extension StatusSection {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
cell.startAnimating()
|
cell.startAnimating()
|
||||||
return cell
|
return cell
|
||||||
|
case .emptyStateHeader(let attribute):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineHeaderTableViewCell.self), for: indexPath) as! TimelineHeaderTableViewCell
|
||||||
|
StatusSection.configureEmptyStateHeader(cell: cell, attribute: attribute)
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusSection {
|
extension StatusSection {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
|
@ -473,6 +478,14 @@ extension StatusSection {
|
||||||
snapshot.appendItems(pollItems, toSection: .main)
|
snapshot.appendItems(pollItems, toSection: .main)
|
||||||
cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func configureEmptyStateHeader(
|
||||||
|
cell: TimelineHeaderTableViewCell,
|
||||||
|
attribute: Item.EmptyStateHeaderAttribute
|
||||||
|
) {
|
||||||
|
cell.timelineHeaderView.iconImageView.image = attribute.reason.iconImage
|
||||||
|
cell.timelineHeaderView.messageLabel.text = attribute.reason.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusSection {
|
extension StatusSection {
|
||||||
|
|
|
@ -64,11 +64,8 @@ extension ActiveLabel {
|
||||||
/// account field
|
/// account field
|
||||||
func configure(field: String) {
|
func configure(field: String) {
|
||||||
activeEntities.removeAll()
|
activeEntities.removeAll()
|
||||||
if let parseResult = try? MastodonField.parse(field: field) {
|
let parseResult = MastodonField.parse(field: field)
|
||||||
text = parseResult.value
|
text = parseResult.value
|
||||||
activeEntities = parseResult.activeEntities
|
activeEntities = parseResult.activeEntities
|
||||||
} else {
|
|
||||||
text = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,6 +172,16 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal enum Timeline {
|
internal enum Timeline {
|
||||||
|
internal enum Header {
|
||||||
|
/// You can’t view Artbot’s profile\n until they unblock you.
|
||||||
|
internal static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning")
|
||||||
|
/// You can’t view Artbot’s profile\n until you unblock them.\nYour account looks like this to them.
|
||||||
|
internal static let blockingWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockingWarning")
|
||||||
|
/// No Status Found
|
||||||
|
internal static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound")
|
||||||
|
/// This account is suspended.
|
||||||
|
internal static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning")
|
||||||
|
}
|
||||||
internal enum Loader {
|
internal enum Loader {
|
||||||
/// Loading missing posts...
|
/// Loading missing posts...
|
||||||
internal static let loadingMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadingMissingPosts")
|
internal static let loadingMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadingMissingPosts")
|
||||||
|
|
|
@ -66,9 +66,9 @@ extension StatusProviderFacade {
|
||||||
|
|
||||||
static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) {
|
static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) {
|
||||||
switch entity.type {
|
switch entity.type {
|
||||||
case .hashtag(let text, let userInfo):
|
case .hashtag:
|
||||||
break
|
break
|
||||||
case .mention(let text, let userInfo):
|
case .mention(let text, _):
|
||||||
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text)
|
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text)
|
||||||
case .url(_, _, let url, _):
|
case .url(_, _, let url, _):
|
||||||
guard let url = URL(string: url) else { return }
|
guard let url = URL(string: url) else { return }
|
||||||
|
|
|
@ -54,6 +54,13 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Status.StatusContentWarning" = "content warning";
|
"Common.Controls.Status.StatusContentWarning" = "content warning";
|
||||||
"Common.Controls.Status.UserReblogged" = "%@ reblogged";
|
"Common.Controls.Status.UserReblogged" = "%@ reblogged";
|
||||||
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
|
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
|
||||||
|
"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view Artbot’s profile
|
||||||
|
until they unblock you.";
|
||||||
|
"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view Artbot’s profile
|
||||||
|
until you unblock them.
|
||||||
|
Your account looks like this to them.";
|
||||||
|
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
||||||
|
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account is suspended.";
|
||||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||||
"Common.Countable.Photo.Multiple" = "photos";
|
"Common.Countable.Photo.Multiple" = "photos";
|
||||||
|
|
|
@ -140,63 +140,17 @@ extension ProfileViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
|
||||||
// Publishers.CombineLatest4(
|
|
||||||
// viewModel.muted.eraseToAnyPublisher(),
|
|
||||||
// viewModel.blocked.eraseToAnyPublisher(),
|
|
||||||
// viewModel.twitterUser.eraseToAnyPublisher(),
|
|
||||||
// context.authenticationService.activeTwitterAuthenticationBox.eraseToAnyPublisher()
|
|
||||||
// )
|
|
||||||
// .receive(on: DispatchQueue.main)
|
|
||||||
// .sink { [weak self] muted, blocked, twitterUser, activeTwitterAuthenticationBox in
|
|
||||||
// guard let self = self else { return }
|
|
||||||
// guard let twitterUser = twitterUser,
|
|
||||||
// let activeTwitterAuthenticationBox = activeTwitterAuthenticationBox,
|
|
||||||
// twitterUser.id != activeTwitterAuthenticationBox.twitterUserID else {
|
|
||||||
// self.navigationItem.rightBarButtonItems = []
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if #available(iOS 14.0, *) {
|
|
||||||
// self.moreMenuBarButtonItem.target = nil
|
|
||||||
// self.moreMenuBarButtonItem.action = nil
|
|
||||||
// self.moreMenuBarButtonItem.menu = UserProviderFacade.createMenuForUser(
|
|
||||||
// twitterUser: twitterUser,
|
|
||||||
// muted: muted,
|
|
||||||
// blocked: blocked,
|
|
||||||
// dependency: self
|
|
||||||
// )
|
|
||||||
// } else {
|
|
||||||
// // no menu supports for early version
|
|
||||||
// self.moreMenuBarButtonItem.target = self
|
|
||||||
// self.moreMenuBarButtonItem.action = #selector(ProfileViewController.moreMenuBarButtonItemPressed(_:))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var rightBarButtonItems: [UIBarButtonItem] = [self.moreMenuBarButtonItem]
|
|
||||||
// if muted {
|
|
||||||
// rightBarButtonItems.append(self.unmuteMenuBarButtonItem)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// self.navigationItem.rightBarButtonItems = rightBarButtonItems
|
|
||||||
// }
|
|
||||||
// .store(in: &disposeBag)
|
|
||||||
|
|
||||||
overlayScrollView.refreshControl = refreshControl
|
overlayScrollView.refreshControl = refreshControl
|
||||||
refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
|
||||||
// drawerSidebarTransitionController = DrawerSidebarTransitionController(drawerSidebarTransitionableViewController: self)
|
|
||||||
|
|
||||||
let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter())
|
let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter())
|
||||||
viewModel.domain.assign(to: \.value, on: postsUserTimelineViewModel.domain).store(in: &disposeBag)
|
bind(userTimelineViewModel: postsUserTimelineViewModel)
|
||||||
viewModel.userID.assign(to: \.value, on: postsUserTimelineViewModel.userID).store(in: &disposeBag)
|
|
||||||
|
|
||||||
let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
|
let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
|
||||||
viewModel.domain.assign(to: \.value, on: repliesUserTimelineViewModel.domain).store(in: &disposeBag)
|
bind(userTimelineViewModel: repliesUserTimelineViewModel)
|
||||||
viewModel.userID.assign(to: \.value, on: repliesUserTimelineViewModel.userID).store(in: &disposeBag)
|
|
||||||
|
|
||||||
let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
|
let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
|
||||||
viewModel.domain.assign(to: \.value, on: mediaUserTimelineViewModel.domain).store(in: &disposeBag)
|
bind(userTimelineViewModel: mediaUserTimelineViewModel)
|
||||||
viewModel.userID.assign(to: \.value, on: mediaUserTimelineViewModel.userID).store(in: &disposeBag)
|
|
||||||
|
|
||||||
profileSegmentedViewController.pagingViewController.viewModel = {
|
profileSegmentedViewController.pagingViewController.viewModel = {
|
||||||
let profilePagingViewModel = ProfilePagingViewModel(
|
let profilePagingViewModel = ProfilePagingViewModel(
|
||||||
|
@ -275,7 +229,7 @@ extension ProfileViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] name in
|
.sink { [weak self] name in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.title = name
|
self.navigationItem.title = name
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -425,6 +379,17 @@ extension ProfileViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileViewController {
|
||||||
|
|
||||||
|
private func bind(userTimelineViewModel: UserTimelineViewModel) {
|
||||||
|
viewModel.domain.assign(to: \.value, on: userTimelineViewModel.domain).store(in: &disposeBag)
|
||||||
|
viewModel.userID.assign(to: \.value, on: userTimelineViewModel.userID).store(in: &disposeBag)
|
||||||
|
viewModel.isBlocking.assign(to: \.value, on: userTimelineViewModel.isBlocking).store(in: &disposeBag)
|
||||||
|
viewModel.isBlockedBy.assign(to: \.value, on: userTimelineViewModel.isBlockedBy).store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
extension ProfileViewController {
|
extension ProfileViewController {
|
||||||
|
|
||||||
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ final class UserTimelineViewController: UIViewController, NeedsDependency {
|
||||||
let tableView = UITableView()
|
let tableView = UITableView()
|
||||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
||||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
tableView.register(TimelineHeaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineHeaderTableViewCell.self))
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.backgroundColor = .clear
|
tableView.backgroundColor = .clear
|
||||||
|
@ -100,10 +101,30 @@ extension UserTimelineViewController {
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension UserTimelineViewController: UITableViewDelegate {
|
extension UserTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
// TODO: cache cell height
|
|
||||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
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 {
|
||||||
|
if case .bottomLoader = item {
|
||||||
|
return TimelineLoaderTableViewCell.cellHeight
|
||||||
|
} else {
|
||||||
return 200
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
let key = item.hashValue
|
||||||
|
let frame = cell.frame
|
||||||
|
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,6 @@ extension UserTimelineViewModel.State {
|
||||||
switch stateClass {
|
switch stateClass {
|
||||||
case is Reloading.Type:
|
case is Reloading.Type:
|
||||||
return viewModel.userID.value != nil
|
return viewModel.userID.value != nil
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -48,10 +46,6 @@ extension UserTimelineViewModel.State {
|
||||||
return true
|
return true
|
||||||
case is NoMore.Type:
|
case is NoMore.Type:
|
||||||
return true
|
return true
|
||||||
case is NotAuthorized.Type, is Blocked.Type:
|
|
||||||
return true
|
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -116,8 +110,6 @@ extension UserTimelineViewModel.State {
|
||||||
switch stateClass {
|
switch stateClass {
|
||||||
case is Reloading.Type, is LoadingMore.Type:
|
case is Reloading.Type, is LoadingMore.Type:
|
||||||
return true
|
return true
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -129,8 +121,6 @@ extension UserTimelineViewModel.State {
|
||||||
switch stateClass {
|
switch stateClass {
|
||||||
case is Reloading.Type, is LoadingMore.Type:
|
case is Reloading.Type, is LoadingMore.Type:
|
||||||
return true
|
return true
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -146,10 +136,6 @@ extension UserTimelineViewModel.State {
|
||||||
return true
|
return true
|
||||||
case is NoMore.Type:
|
case is NoMore.Type:
|
||||||
return true
|
return true
|
||||||
case is NotAuthorized.Type, is Blocked.Type:
|
|
||||||
return true
|
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -188,7 +174,12 @@ extension UserTimelineViewModel.State {
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch user timeline fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
@ -211,52 +202,21 @@ extension UserTimelineViewModel.State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotAuthorized: UserTimelineViewModel.State {
|
|
||||||
|
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
|
||||||
switch stateClass {
|
|
||||||
case is Reloading.Type:
|
|
||||||
return true
|
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Blocked: UserTimelineViewModel.State {
|
|
||||||
|
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
|
||||||
switch stateClass {
|
|
||||||
case is Reloading.Type:
|
|
||||||
return true
|
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Suspended: UserTimelineViewModel.State {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoMore: UserTimelineViewModel.State {
|
class NoMore: UserTimelineViewModel.State {
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
switch stateClass {
|
switch stateClass {
|
||||||
case is Reloading.Type:
|
case is Reloading.Type:
|
||||||
return true
|
return true
|
||||||
case is NotAuthorized.Type, is Blocked.Type:
|
|
||||||
return true
|
|
||||||
case is Suspended.Type:
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
// trigger data source update
|
||||||
|
viewModel.statusFetchedResultsController.objectIDs.value = viewModel.statusFetchedResultsController.objectIDs.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ class UserTimelineViewModel: NSObject {
|
||||||
let userID: CurrentValueSubject<String?, Never>
|
let userID: CurrentValueSubject<String?, Never>
|
||||||
let queryFilter: CurrentValueSubject<QueryFilter, Never>
|
let queryFilter: CurrentValueSubject<QueryFilter, Never>
|
||||||
let statusFetchedResultsController: StatusFetchedResultsController
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
|
||||||
|
let isBlocking = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||||
|
@ -34,9 +38,6 @@ class UserTimelineViewModel: NSObject {
|
||||||
State.Fail(viewModel: self),
|
State.Fail(viewModel: self),
|
||||||
State.Idle(viewModel: self),
|
State.Idle(viewModel: self),
|
||||||
State.LoadingMore(viewModel: self),
|
State.LoadingMore(viewModel: self),
|
||||||
State.NotAuthorized(viewModel: self),
|
|
||||||
State.Blocked(viewModel: self),
|
|
||||||
State.Suspended(viewModel: self),
|
|
||||||
State.NoMore(viewModel: self),
|
State.NoMore(viewModel: self),
|
||||||
])
|
])
|
||||||
stateMachine.enter(State.Initial.self)
|
stateMachine.enter(State.Initial.self)
|
||||||
|
@ -59,14 +60,35 @@ class UserTimelineViewModel: NSObject {
|
||||||
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
Publishers.CombineLatest3(
|
||||||
statusFetchedResultsController.objectIDs
|
statusFetchedResultsController.objectIDs.eraseToAnyPublisher(),
|
||||||
|
isBlocking.eraseToAnyPublisher(),
|
||||||
|
isBlockedBy.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] objectIDs in
|
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] objectIDs, isBlocking, isBlockedBy in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
// var isPermissionDenied = false
|
var items: [Item] = []
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
|
||||||
|
defer {
|
||||||
|
// not animate when empty items fix loader first appear layout issue
|
||||||
|
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !isBlocking else {
|
||||||
|
snapshot.appendItems([Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .blocking))], toSection: .main)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !isBlockedBy else {
|
||||||
|
snapshot.appendItems([Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .blocked))], toSection: .main)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
||||||
let oldSnapshot = diffableDataSource.snapshot()
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
|
@ -75,10 +97,6 @@ class UserTimelineViewModel: NSObject {
|
||||||
oldSnapshotAttributeDict[objectID] = attribute
|
oldSnapshotAttributeDict[objectID] = attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
|
||||||
snapshot.appendSections([.main])
|
|
||||||
|
|
||||||
var items: [Item] = []
|
|
||||||
for objectID in objectIDs {
|
for objectID in objectIDs {
|
||||||
let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute()
|
let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute()
|
||||||
items.append(.status(objectID: objectID, attribute: attribute))
|
items.append(.status(objectID: objectID, attribute: attribute))
|
||||||
|
@ -89,14 +107,15 @@ class UserTimelineViewModel: NSObject {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case is State.Reloading, is State.LoadingMore, is State.Idle, is State.Fail:
|
case is State.Reloading, is State.LoadingMore, is State.Idle, is State.Fail:
|
||||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
case is State.NoMore:
|
||||||
|
break
|
||||||
// TODO: handle other states
|
// TODO: handle other states
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// not animate when empty items fix loader first appear layout issue
|
|
||||||
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -125,3 +144,4 @@ extension UserTimelineViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
//
|
||||||
|
// TimelineHeaderView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-4-6.
|
||||||
|
//
|
||||||
|
|
||||||
|
final class TimelineHeaderView: UIView {
|
||||||
|
|
||||||
|
let iconImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
let messageLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 17)
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.text = "info"
|
||||||
|
label.numberOfLines = 0
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineHeaderView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
let topPaddingView = UIView()
|
||||||
|
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(topPaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topPaddingView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
topPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
topPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.axis = .vertical
|
||||||
|
containerStackView.alignment = .center
|
||||||
|
containerStackView.distribution = .fill
|
||||||
|
containerStackView.spacing = 16
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
containerStackView.addArrangedSubview(iconImageView)
|
||||||
|
containerStackView.addArrangedSubview(messageLabel)
|
||||||
|
|
||||||
|
let bottomPaddingView = UIView()
|
||||||
|
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(bottomPaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomPaddingView.topAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||||
|
bottomPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
bottomPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
bottomPaddingView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topPaddingView.heightAnchor.constraint(equalToConstant: 100).priority(.defaultHigh),
|
||||||
|
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Item.EmptyStateHeaderAttribute.Reason {
|
||||||
|
var iconImage: UIImage? {
|
||||||
|
switch self {
|
||||||
|
case .noStatusFound, .blocking, .blocked, .suspended:
|
||||||
|
return UIImage(systemName: "nosign", withConfiguration: UIImage.SymbolConfiguration(pointSize: 64, weight: .bold))!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var message: String {
|
||||||
|
switch self {
|
||||||
|
case .noStatusFound:
|
||||||
|
return L10n.Common.Controls.Timeline.Header.noStatusFound
|
||||||
|
case .blocking:
|
||||||
|
return L10n.Common.Controls.Timeline.Header.blockingWarning
|
||||||
|
case .blocked:
|
||||||
|
return L10n.Common.Controls.Timeline.Header.blockedWarning
|
||||||
|
case .suspended:
|
||||||
|
return L10n.Common.Controls.Timeline.Header.suspendedWarning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG && canImport(SwiftUI)
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TimelineHeaderView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let headerView = TimelineHeaderView()
|
||||||
|
headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking.iconImage
|
||||||
|
headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking.message
|
||||||
|
return headerView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,42 @@
|
||||||
|
//
|
||||||
|
// TimelineHeaderTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-4-6.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class TimelineHeaderTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
let timelineHeaderView = TimelineHeaderView()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineHeaderTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
timelineHeaderView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(timelineHeaderView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
timelineHeaderView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
timelineHeaderView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
timelineHeaderView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||||
|
timelineHeaderView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -145,11 +145,12 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
)
|
)
|
||||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||||
guard let self = self else { return }
|
guard let _ = self else { return }
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
break
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] block update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
|
||||||
case .finished:
|
case .finished:
|
||||||
// TODO: update relationship
|
// TODO: update relationship
|
||||||
switch blockQueryType {
|
switch blockQueryType {
|
||||||
|
|
|
@ -167,10 +167,11 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
)
|
)
|
||||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||||
guard let self = self else { return }
|
guard let _ = self else { return }
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update follow fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
break
|
break
|
||||||
case .finished:
|
case .finished:
|
||||||
switch followQueryType {
|
switch followQueryType {
|
||||||
|
|
|
@ -145,11 +145,11 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
)
|
)
|
||||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||||
guard let self = self else { return }
|
guard let _ = self else { return }
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
break
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] Mute update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
case .finished:
|
case .finished:
|
||||||
// TODO: update relationship
|
// TODO: update relationship
|
||||||
switch muteQueryType {
|
switch muteQueryType {
|
||||||
|
|
Loading…
Reference in New Issue