Merge pull request #187 from tootsuite/fix/recommand-refresh
Add refresh trigger for recommend list and disable reblog if needs
This commit is contained in:
commit
0dd2aaa068
|
@ -4778,7 +4778,7 @@
|
||||||
repositoryURL = "https://github.com/TwidereProject/MetaTextView.git";
|
repositoryURL = "https://github.com/TwidereProject/MetaTextView.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 1.2.1;
|
version = 1.2.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
|
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>23</integer>
|
<integer>21</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>21</integer>
|
<integer>22</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -114,8 +114,8 @@
|
||||||
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
|
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "2660fc30ef6ed8de347ddca499341a965d1fda56",
|
"revision": "d48cf6a2479ce6fc4f836b6c4d7ba855cdbc71cc",
|
||||||
"version": "1.2.1"
|
"version": "1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -25,6 +25,7 @@ extension ComposeStatusItem {
|
||||||
|
|
||||||
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
|
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
|
||||||
let displayName = CurrentValueSubject<String?, Never>(nil)
|
let displayName = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||||
let username = CurrentValueSubject<String?, Never>(nil)
|
let username = CurrentValueSubject<String?, Never>(nil)
|
||||||
let composeContent = CurrentValueSubject<String?, Never>(nil)
|
let composeContent = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ extension ComposeStatusItem {
|
||||||
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
||||||
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
||||||
lhs.displayName.value == rhs.displayName.value &&
|
lhs.displayName.value == rhs.displayName.value &&
|
||||||
|
lhs.emojiDict.value == rhs.emojiDict.value &&
|
||||||
lhs.username.value == rhs.username.value &&
|
lhs.username.value == rhs.username.value &&
|
||||||
lhs.composeContent.value == rhs.composeContent.value &&
|
lhs.composeContent.value == rhs.composeContent.value &&
|
||||||
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
|
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
|
||||||
|
|
|
@ -43,13 +43,14 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
// set display name and username
|
// set display name and username
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest3(
|
||||||
attribute.displayName.eraseToAnyPublisher(),
|
attribute.displayName,
|
||||||
|
attribute.emojiDict,
|
||||||
attribute.username.eraseToAnyPublisher()
|
attribute.username.eraseToAnyPublisher()
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { displayName, username in
|
.sink { displayName, emojiDict, username in
|
||||||
cell.statusView.nameLabel.text = displayName
|
cell.statusView.nameLabel.configure(content: displayName ?? " ", emojiDict: emojiDict)
|
||||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
|
@ -574,9 +574,8 @@ extension StatusSection {
|
||||||
cell.statusView.contentMetaText.textView.accessibilityLanguage = (status.reblog ?? status).language
|
cell.statusView.contentMetaText.textView.accessibilityLanguage = (status.reblog ?? status).language
|
||||||
|
|
||||||
// set visibility
|
// set visibility
|
||||||
if let visibility = (status.reblog ?? status).visibility {
|
if let visibility = (status.reblog ?? status).visibilityEnum {
|
||||||
cell.statusView.updateVisibility(visibility: visibility)
|
cell.statusView.updateVisibility(visibility: visibility)
|
||||||
|
|
||||||
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak cell] isHidden in
|
.sink { [weak cell] isHidden in
|
||||||
|
@ -953,6 +952,13 @@ extension StatusSection {
|
||||||
guard status.reblogsCount.intValue > 0 else { return nil }
|
guard status.reblogsCount.intValue > 0 else { return nil }
|
||||||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.reblogsCount.intValue)
|
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.reblogsCount.intValue)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// disable reblog when non-public (except self)
|
||||||
|
cell.statusView.actionToolbarContainer.reblogButton.isEnabled = true
|
||||||
|
if let visibility = status.visibilityEnum, visibility != .public, status.author.id != requestUserID {
|
||||||
|
cell.statusView.actionToolbarContainer.reblogButton.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
// set like
|
// set like
|
||||||
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||||
let favoriteCountTitle: String = {
|
let favoriteCountTitle: String = {
|
||||||
|
|
|
@ -89,3 +89,10 @@ extension Status {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Status: EmojiContainer { }
|
extension Status: EmojiContainer { }
|
||||||
|
|
||||||
|
|
||||||
|
extension Status {
|
||||||
|
var visibilityEnum: Mastodon.Entity.Status.Visibility? {
|
||||||
|
return visibility.flatMap { Mastodon.Entity.Status.Visibility(rawValue: $0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -767,6 +767,22 @@ extension ComposeViewController: UITextViewDelegate {
|
||||||
return autoCompleteInfo
|
return autoCompleteInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
|
if textView === textEditorView()?.textView {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
|
if textView === textEditorView()?.textView {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - TextEditorViewTextAttributesDelegate
|
// MARK: - TextEditorViewTextAttributesDelegate
|
||||||
|
|
|
@ -181,6 +181,7 @@ final class ComposeViewModel: NSObject {
|
||||||
}
|
}
|
||||||
return displayName
|
return displayName
|
||||||
}()
|
}()
|
||||||
|
self.composeStatusAttribute.emojiDict.value = mastodonUser?.emojiDict ?? [:]
|
||||||
self.composeStatusAttribute.username.value = username
|
self.composeStatusAttribute.username.value = username
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
|
@ -27,6 +27,7 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
|
||||||
metaText.textView.backgroundColor = .clear
|
metaText.textView.backgroundColor = .clear
|
||||||
metaText.textView.isScrollEnabled = false
|
metaText.textView.isScrollEnabled = false
|
||||||
metaText.textView.keyboardType = .twitter
|
metaText.textView.keyboardType = .twitter
|
||||||
|
metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
|
||||||
metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset
|
metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset
|
||||||
metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||||
metaText.textView.attributedPlaceholder = {
|
metaText.textView.attributedPlaceholder = {
|
||||||
|
|
|
@ -202,15 +202,6 @@ extension ComposeToolbarView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageNameForTimeline() -> String {
|
|
||||||
switch self {
|
|
||||||
case .public: return "globe"
|
|
||||||
// case .unlisted: return "eye.slash"
|
|
||||||
case .private: return "person.3"
|
|
||||||
case .direct: return "at"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var visibility: Mastodon.Entity.Status.Visibility {
|
var visibility: Mastodon.Entity.Status.Visibility {
|
||||||
switch self {
|
switch self {
|
||||||
case .public: return .public
|
case .public: return .public
|
||||||
|
|
|
@ -56,7 +56,7 @@ extension HashtagTimelineViewController {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
title = "#\(viewModel.hashtag)"
|
title = "#\(viewModel.hashtag)"
|
||||||
titleView.update(title: viewModel.hashtag, subtitle: nil)
|
titleView.update(title: viewModel.hashtag, subtitle: nil, emojiDict: [:])
|
||||||
navigationItem.titleView = titleView
|
navigationItem.titleView = titleView
|
||||||
|
|
||||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
|
@ -143,7 +143,7 @@ extension HashtagTimelineViewController {
|
||||||
private func updatePromptTitle() {
|
private func updatePromptTitle() {
|
||||||
var subtitle: String?
|
var subtitle: String?
|
||||||
defer {
|
defer {
|
||||||
titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle)
|
titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle, emojiDict: [:])
|
||||||
}
|
}
|
||||||
guard let histories = viewModel.hashtagEntity.value?.history else {
|
guard let histories = viewModel.hashtagEntity.value?.history else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -49,7 +49,7 @@ extension FavoriteViewController {
|
||||||
|
|
||||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
navigationItem.titleView = titleView
|
navigationItem.titleView = titleView
|
||||||
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil)
|
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil, emojiDict: [:])
|
||||||
|
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(tableView)
|
view.addSubview(tableView)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ActiveLabel
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
import CropViewController
|
import CropViewController
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
||||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
||||||
|
@ -166,14 +167,27 @@ extension ProfileHeaderViewController {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
Publishers.CombineLatest3(
|
Publishers.CombineLatest4(
|
||||||
viewModel.isEditing.eraseToAnyPublisher(),
|
viewModel.isEditing,
|
||||||
viewModel.displayProfileInfo.name.removeDuplicates().eraseToAnyPublisher(),
|
viewModel.displayProfileInfo.name.removeDuplicates(),
|
||||||
viewModel.editProfileInfo.name.removeDuplicates().eraseToAnyPublisher()
|
viewModel.editProfileInfo.name.removeDuplicates(),
|
||||||
|
viewModel.emojiDict
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] isEditing, name, editingName in
|
.sink { [weak self] isEditing, name, editingName, emojiDict in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
do {
|
||||||
|
var emojis = MastodonContent.Emojis()
|
||||||
|
for (key, value) in emojiDict {
|
||||||
|
emojis[key] = value.absoluteString
|
||||||
|
}
|
||||||
|
let metaContent = try MastodonMetaContent.convert(
|
||||||
|
document: MastodonContent(content: name ?? " ", emojis: emojis)
|
||||||
|
)
|
||||||
|
self.profileHeaderView.nameMetaText.configure(content: metaContent)
|
||||||
|
} catch {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
self.profileHeaderView.nameTextField.text = isEditing ? editingName : name
|
self.profileHeaderView.nameTextField.text = isEditing ? editingName : name
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -412,7 +426,7 @@ extension ProfileHeaderViewController {
|
||||||
profileHeaderView.avatarImageView.alpha = alpha
|
profileHeaderView.avatarImageView.alpha = alpha
|
||||||
profileHeaderView.editAvatarBackgroundView.alpha = alpha
|
profileHeaderView.editAvatarBackgroundView.alpha = alpha
|
||||||
profileHeaderView.nameTextFieldBackgroundView.alpha = alpha
|
profileHeaderView.nameTextFieldBackgroundView.alpha = alpha
|
||||||
profileHeaderView.nameTextField.alpha = alpha
|
profileHeaderView.displayNameStackView.alpha = alpha
|
||||||
profileHeaderView.usernameLabel.alpha = alpha
|
profileHeaderView.usernameLabel.alpha = alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
import FLAnimatedImage
|
import FLAnimatedImage
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
protocol ProfileHeaderViewDelegate: AnyObject {
|
protocol ProfileHeaderViewDelegate: AnyObject {
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView)
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView)
|
||||||
|
@ -111,7 +112,24 @@ final class ProfileHeaderView: UIView {
|
||||||
view.layer.cornerRadius = 10
|
view.layer.cornerRadius = 10
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let displayNameStackView = UIStackView()
|
||||||
|
let nameMetaText: MetaText = {
|
||||||
|
let metaText = MetaText()
|
||||||
|
metaText.textView.backgroundColor = .clear
|
||||||
|
metaText.textView.isEditable = false
|
||||||
|
metaText.textView.isSelectable = false
|
||||||
|
metaText.textView.isScrollEnabled = false
|
||||||
|
metaText.textView.layer.masksToBounds = false
|
||||||
|
metaText.textView.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold), maximumPointSize: 28)
|
||||||
|
metaText.textView.textColor = .white
|
||||||
|
metaText.textView.textContainer.lineFragmentPadding = 0
|
||||||
|
metaText.textAttributes = [
|
||||||
|
.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold), maximumPointSize: 28),
|
||||||
|
.foregroundColor: UIColor.white
|
||||||
|
]
|
||||||
|
return metaText
|
||||||
|
}()
|
||||||
let nameTextField: UITextField = {
|
let nameTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold), maximumPointSize: 28)
|
textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold), maximumPointSize: 28)
|
||||||
|
@ -303,7 +321,6 @@ extension ProfileHeaderView {
|
||||||
nameContainerStackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
nameContainerStackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
let displayNameStackView = UIStackView()
|
|
||||||
displayNameStackView.axis = .horizontal
|
displayNameStackView.axis = .horizontal
|
||||||
nameTextField.translatesAutoresizingMaskIntoConstraints = false
|
nameTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
displayNameStackView.addArrangedSubview(nameTextField)
|
displayNameStackView.addArrangedSubview(nameTextField)
|
||||||
|
@ -321,6 +338,16 @@ extension ProfileHeaderView {
|
||||||
])
|
])
|
||||||
displayNameStackView.bringSubviewToFront(nameTextField)
|
displayNameStackView.bringSubviewToFront(nameTextField)
|
||||||
displayNameStackView.addArrangedSubview(UIView())
|
displayNameStackView.addArrangedSubview(UIView())
|
||||||
|
|
||||||
|
// overlay meta text for display name
|
||||||
|
nameMetaText.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
displayNameStackView.addSubview(nameMetaText.textView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
nameMetaText.textView.topAnchor.constraint(equalTo: nameTextField.topAnchor),
|
||||||
|
nameMetaText.textView.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor),
|
||||||
|
nameMetaText.textView.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor),
|
||||||
|
nameMetaText.textView.bottomAnchor.constraint(equalTo: nameTextField.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
nameContainerStackView.addArrangedSubview(displayNameStackView)
|
nameContainerStackView.addArrangedSubview(displayNameStackView)
|
||||||
nameContainerStackView.addArrangedSubview(usernameLabel)
|
nameContainerStackView.addArrangedSubview(usernameLabel)
|
||||||
|
@ -436,6 +463,8 @@ extension ProfileHeaderView {
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
case .normal:
|
case .normal:
|
||||||
|
nameMetaText.textView.alpha = 1
|
||||||
|
nameTextField.alpha = 0
|
||||||
nameTextField.isEnabled = false
|
nameTextField.isEnabled = false
|
||||||
bioActiveLabelContainer.isHidden = false
|
bioActiveLabelContainer.isHidden = false
|
||||||
bioTextEditorView.isHidden = true
|
bioTextEditorView.isHidden = true
|
||||||
|
@ -449,7 +478,9 @@ extension ProfileHeaderView {
|
||||||
self.editAvatarBackgroundView.isHidden = true
|
self.editAvatarBackgroundView.isHidden = true
|
||||||
}
|
}
|
||||||
case .editing:
|
case .editing:
|
||||||
|
nameMetaText.textView.alpha = 0
|
||||||
nameTextField.isEnabled = true
|
nameTextField.isEnabled = true
|
||||||
|
nameTextField.alpha = 1
|
||||||
bioActiveLabelContainer.isHidden = true
|
bioActiveLabelContainer.isHidden = true
|
||||||
bioTextEditorView.isHidden = false
|
bioTextEditorView.isHidden = false
|
||||||
|
|
||||||
|
|
|
@ -303,12 +303,13 @@ extension ProfileViewController {
|
||||||
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
||||||
|
|
||||||
// bind view model
|
// bind view model
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest3(
|
||||||
viewModel.name.eraseToAnyPublisher(),
|
viewModel.name,
|
||||||
viewModel.statusesCount.eraseToAnyPublisher()
|
viewModel.emojiDict,
|
||||||
|
viewModel.statusesCount
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] name, statusesCount in
|
.sink { [weak self] name, emojiDict, statusesCount in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let title = name, let statusesCount = statusesCount,
|
guard let title = name, let statusesCount = statusesCount,
|
||||||
let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else {
|
let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else {
|
||||||
|
@ -316,7 +317,7 @@ extension ProfileViewController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let subtitle = L10n.Scene.Profile.subtitle(formattedStatusCount)
|
let subtitle = L10n.Scene.Profile.subtitle(formattedStatusCount)
|
||||||
self.titleView.update(title: title, subtitle: subtitle)
|
self.titleView.update(title: title, subtitle: subtitle, emojiDict: emojiDict)
|
||||||
self.titleView.isHidden = false
|
self.titleView.isHidden = false
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -368,7 +369,7 @@ extension ProfileViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name)
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
viewModel.fileds
|
viewModel.fields
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.map { fields -> [ProfileFieldItem.FieldValue] in
|
.map { fields -> [ProfileFieldItem.FieldValue] in
|
||||||
fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value) }
|
fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value) }
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ProfileViewModel: NSObject {
|
||||||
let statusesCount: CurrentValueSubject<Int?, Never>
|
let statusesCount: CurrentValueSubject<Int?, Never>
|
||||||
let followingCount: CurrentValueSubject<Int?, Never>
|
let followingCount: CurrentValueSubject<Int?, Never>
|
||||||
let followersCount: CurrentValueSubject<Int?, Never>
|
let followersCount: CurrentValueSubject<Int?, Never>
|
||||||
let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never>
|
let fields: CurrentValueSubject<[Mastodon.Entity.Field], Never>
|
||||||
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
|
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
|
||||||
|
|
||||||
// fulfill this before editing
|
// fulfill this before editing
|
||||||
|
@ -82,7 +82,7 @@ class ProfileViewModel: NSObject {
|
||||||
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) })
|
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) })
|
||||||
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
||||||
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
||||||
self.fileds = CurrentValueSubject(mastodonUser?.fields ?? [])
|
self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
|
||||||
self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:])
|
self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:])
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ extension ProfileViewModel {
|
||||||
self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
|
self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
|
||||||
self.protected.value = mastodonUser?.locked
|
self.protected.value = mastodonUser?.locked
|
||||||
self.suspended.value = mastodonUser?.suspended ?? false
|
self.suspended.value = mastodonUser?.suspended ?? false
|
||||||
self.fileds.value = mastodonUser?.fields ?? []
|
self.fields.value = mastodonUser?.fields ?? []
|
||||||
self.emojiDict.value = mastodonUser?.emojiDict ?? [:]
|
self.emojiDict.value = mastodonUser?.emojiDict ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import CoreDataStack
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
|
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
|
||||||
func followButtonDidPressed(clickedUser: MastodonUser)
|
func followButtonDidPressed(clickedUser: MastodonUser)
|
||||||
|
@ -42,8 +43,8 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
||||||
|
|
||||||
let displayNameLabel: UILabel = {
|
let displayNameLabel: ActiveLabel = {
|
||||||
let label = UILabel()
|
let label = ActiveLabel(style: .statusName)
|
||||||
label.textColor = .white
|
label.textColor = .white
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
label.font = .systemFont(ofSize: 18, weight: .semibold)
|
label.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||||
|
@ -164,7 +165,7 @@ extension SearchRecommendAccountsCollectionViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with mastodonUser: MastodonUser) {
|
func config(with mastodonUser: MastodonUser) {
|
||||||
displayNameLabel.text = mastodonUser.displayName.isEmpty ? mastodonUser.username : mastodonUser.displayName
|
displayNameLabel.configure(content: mastodonUser.displayNameWithFallback, emojiDict: mastodonUser.emojiDict)
|
||||||
acctLabel.text = "@" + mastodonUser.acct
|
acctLabel.text = "@" + mastodonUser.acct
|
||||||
avatarImageView.af.setImage(
|
avatarImageView.af.setImage(
|
||||||
withURL: URL(string: mastodonUser.avatar)!,
|
withURL: URL(string: mastodonUser.avatar)!,
|
||||||
|
|
|
@ -151,6 +151,12 @@ extension SearchViewController {
|
||||||
view.bringSubviewToFront(statusBar)
|
view.bringSubviewToFront(statusBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
viewModel.viewDidAppeared.send()
|
||||||
|
}
|
||||||
|
|
||||||
func setupSearchBar() {
|
func setupSearchBar() {
|
||||||
searchBar.delegate = self
|
searchBar.delegate = self
|
||||||
view.addSubview(searchBar)
|
view.addSubview(searchBar)
|
||||||
|
|
|
@ -23,6 +23,7 @@ final class SearchViewModel: NSObject {
|
||||||
|
|
||||||
let mastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
let mastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
||||||
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
||||||
|
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let searchText = CurrentValueSubject<String, Never>("")
|
let searchText = CurrentValueSubject<String, Never>("")
|
||||||
|
@ -145,9 +146,10 @@ final class SearchViewModel: NSObject {
|
||||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
requestRecommendHashTags()
|
viewDidAppeared
|
||||||
.receive(on: DispatchQueue.main)
|
.compactMap { _ in self.requestRecommendHashTags() }
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if !self.recommendHashTags.isEmpty {
|
if !self.recommendHashTags.isEmpty {
|
||||||
|
@ -160,8 +162,9 @@ final class SearchViewModel: NSObject {
|
||||||
} receiveValue: { _ in
|
} receiveValue: { _ in
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
viewDidAppeared
|
||||||
requestRecommendAccountsV2()
|
.compactMap { _ in self.requestRecommendAccountsV2() }
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if !self.recommendAccounts.isEmpty {
|
if !self.recommendAccounts.isEmpty {
|
||||||
|
@ -172,6 +175,7 @@ final class SearchViewModel: NSObject {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
recommendAccountsFallback
|
recommendAccountsFallback
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.requestRecommendAccounts()
|
self.requestRecommendAccounts()
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
final class DoubleTitleLabelNavigationBarTitleView: UIView {
|
final class DoubleTitleLabelNavigationBarTitleView: UIView {
|
||||||
|
|
||||||
let containerView = UIStackView()
|
let containerView = UIStackView()
|
||||||
|
|
||||||
let titleLabel: UILabel = {
|
let titleLabel: ActiveLabel = {
|
||||||
let label = UILabel()
|
let label = ActiveLabel(style: .default)
|
||||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
|
@ -58,8 +59,8 @@ extension DoubleTitleLabelNavigationBarTitleView {
|
||||||
containerView.addArrangedSubview(subtitleLabel)
|
containerView.addArrangedSubview(subtitleLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(title: String, subtitle: String?) {
|
func update(title: String, subtitle: String?, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
titleLabel.text = title
|
titleLabel.configure(content: title, emojiDict: emojiDict)
|
||||||
if let subtitle = subtitle {
|
if let subtitle = subtitle {
|
||||||
subtitleLabel.text = subtitle
|
subtitleLabel.text = subtitle
|
||||||
subtitleLabel.isHidden = false
|
subtitleLabel.isHidden = false
|
||||||
|
|
|
@ -14,6 +14,7 @@ import AlamofireImage
|
||||||
import FLAnimatedImage
|
import FLAnimatedImage
|
||||||
import MetaTextView
|
import MetaTextView
|
||||||
import Meta
|
import Meta
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// import LinkPresentation
|
// import LinkPresentation
|
||||||
|
@ -498,10 +499,20 @@ extension StatusView {
|
||||||
}
|
}
|
||||||
// TODO: a11y
|
// TODO: a11y
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateVisibility(visibility: String) {
|
func updateVisibility(visibility: Mastodon.Entity.Status.Visibility) {
|
||||||
guard let visibility = ComposeToolbarView.VisibilitySelectionType(rawValue: visibility) else { return }
|
switch visibility {
|
||||||
visibilityImageView.image = UIImage(systemName: visibility.imageNameForTimeline(), withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
|
case .public:
|
||||||
|
visibilityImageView.image = UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
|
||||||
|
case .private:
|
||||||
|
visibilityImageView.image = UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
|
||||||
|
case .unlisted:
|
||||||
|
visibilityImageView.image = UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
|
||||||
|
case .direct:
|
||||||
|
visibilityImageView.image = UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
|
||||||
|
case ._other:
|
||||||
|
visibilityImageView.image = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import CoreDataStack
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
|
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
|
||||||
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell)
|
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell)
|
||||||
|
@ -28,8 +29,8 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let titleLabel: UILabel = {
|
let titleLabel: ActiveLabel = {
|
||||||
let label = UILabel()
|
let label = ActiveLabel(style: .statusName)
|
||||||
label.textColor = Asset.Colors.brandBlue.color
|
label.textColor = Asset.Colors.brandBlue.color
|
||||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
label.lineBreakMode = .byTruncatingTail
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
@ -153,7 +154,7 @@ extension SuggestionAccountTableViewCell {
|
||||||
imageTransition: .crossDissolve(0.2)
|
imageTransition: .crossDissolve(0.2)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
titleLabel.configure(content: account.displayNameWithFallback, emojiDict: account.emojiDict)
|
||||||
subTitleLabel.text = account.acct
|
subTitleLabel.text = account.acct
|
||||||
button.isSelected = isSelected
|
button.isSelected = isSelected
|
||||||
button.publisher(for: .touchUpInside)
|
button.publisher(for: .touchUpInside)
|
||||||
|
|
|
@ -80,9 +80,13 @@ extension ThreadViewController {
|
||||||
|
|
||||||
viewModel.navigationBarTitle
|
viewModel.navigationBarTitle
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] title in
|
.sink { [weak self] tuple in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.titleView.update(title: title ?? L10n.Scene.Thread.backTitle, subtitle: nil)
|
guard let (title, emojiDict) = tuple else {
|
||||||
|
self.titleView.update(title: L10n.Scene.Thread.backTitle, subtitle: nil, emojiDict: [:])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.titleView.update(title: title, subtitle: nil, emojiDict: emojiDict)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ class ThreadViewModel {
|
||||||
let ancestorItems = CurrentValueSubject<[Item], Never>([])
|
let ancestorItems = CurrentValueSubject<[Item], Never>([])
|
||||||
let descendantNodes = CurrentValueSubject<[LeafNode], Never>([])
|
let descendantNodes = CurrentValueSubject<[LeafNode], Never>([])
|
||||||
let descendantItems = CurrentValueSubject<[Item], Never>([])
|
let descendantItems = CurrentValueSubject<[Item], Never>([])
|
||||||
let navigationBarTitle: CurrentValueSubject<String?, Never>
|
let navigationBarTitle: CurrentValueSubject<(String, MastodonStatusContent.EmojiDict)?, Never>
|
||||||
|
|
||||||
init(context: AppContext, optionalStatus: Status?) {
|
init(context: AppContext, optionalStatus: Status?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
@ -53,7 +53,7 @@ class ThreadViewModel {
|
||||||
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
|
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
|
||||||
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
|
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
|
||||||
self.navigationBarTitle = CurrentValueSubject(
|
self.navigationBarTitle = CurrentValueSubject(
|
||||||
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }
|
optionalStatus.flatMap { (L10n.Scene.Thread.title($0.author.displayNameWithFallback), $0.emojiDict) }
|
||||||
)
|
)
|
||||||
|
|
||||||
// bind fetcher domain
|
// bind fetcher domain
|
||||||
|
@ -85,7 +85,7 @@ class ThreadViewModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID)
|
self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID)
|
||||||
self.navigationBarTitle.value = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
|
self.navigationBarTitle.value = (L10n.Scene.Thread.title(status.author.displayNameWithFallback), status.author.emojiDict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
Loading…
Reference in New Issue