diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index eb7753337..ee5c44971 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -56,7 +56,7 @@ extension HashtagTimelineViewController { super.viewDidLoad() title = "#\(viewModel.hashtag)" - titleView.update(title: viewModel.hashtag, subtitle: nil) + titleView.update(title: viewModel.hashtag, subtitle: nil, emojiDict: [:]) navigationItem.titleView = titleView view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color @@ -143,7 +143,7 @@ extension HashtagTimelineViewController { private func updatePromptTitle() { var subtitle: String? defer { - titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle) + titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle, emojiDict: [:]) } guard let histories = viewModel.hashtagEntity.value?.history else { return diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 46b88796e..d34240a85 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -49,7 +49,7 @@ extension FavoriteViewController { view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color 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 view.addSubview(tableView) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 48470a241..94be3e6f5 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -13,6 +13,7 @@ import ActiveLabel import AlamofireImage import CropViewController import TwitterTextEditor +import MastodonMeta protocol ProfileHeaderViewControllerDelegate: AnyObject { func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) @@ -166,14 +167,27 @@ extension ProfileHeaderViewController { ) } .store(in: &disposeBag) - Publishers.CombineLatest3( - viewModel.isEditing.eraseToAnyPublisher(), - viewModel.displayProfileInfo.name.removeDuplicates().eraseToAnyPublisher(), - viewModel.editProfileInfo.name.removeDuplicates().eraseToAnyPublisher() + Publishers.CombineLatest4( + viewModel.isEditing, + viewModel.displayProfileInfo.name.removeDuplicates(), + viewModel.editProfileInfo.name.removeDuplicates(), + viewModel.emojiDict ) .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 } + 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 } .store(in: &disposeBag) @@ -412,7 +426,7 @@ extension ProfileHeaderViewController { profileHeaderView.avatarImageView.alpha = alpha profileHeaderView.editAvatarBackgroundView.alpha = alpha profileHeaderView.nameTextFieldBackgroundView.alpha = alpha - profileHeaderView.nameTextField.alpha = alpha + profileHeaderView.displayNameStackView.alpha = alpha profileHeaderView.usernameLabel.alpha = alpha } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index f9f2e98d4..abeac9855 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -10,6 +10,7 @@ import UIKit import ActiveLabel import TwitterTextEditor import FLAnimatedImage +import MetaTextView protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) @@ -111,7 +112,24 @@ final class ProfileHeaderView: UIView { view.layer.cornerRadius = 10 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 textField = UITextField() 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), ]) - let displayNameStackView = UIStackView() displayNameStackView.axis = .horizontal nameTextField.translatesAutoresizingMaskIntoConstraints = false displayNameStackView.addArrangedSubview(nameTextField) @@ -321,6 +338,16 @@ extension ProfileHeaderView { ]) displayNameStackView.bringSubviewToFront(nameTextField) 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(usernameLabel) @@ -436,6 +463,8 @@ extension ProfileHeaderView { switch state { case .normal: + nameMetaText.textView.alpha = 1 + nameTextField.alpha = 0 nameTextField.isEnabled = false bioActiveLabelContainer.isHidden = false bioTextEditorView.isHidden = true @@ -449,7 +478,9 @@ extension ProfileHeaderView { self.editAvatarBackgroundView.isHidden = true } case .editing: + nameMetaText.textView.alpha = 0 nameTextField.isEnabled = true + nameTextField.alpha = 1 bioActiveLabelContainer.isHidden = true bioTextEditorView.isHidden = false diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 476d03a9d..7b6a1db86 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -303,12 +303,13 @@ extension ProfileViewController { profileSegmentedViewController.pagingViewController.pagingDelegate = self // bind view model - Publishers.CombineLatest( - viewModel.name.eraseToAnyPublisher(), - viewModel.statusesCount.eraseToAnyPublisher() + Publishers.CombineLatest3( + viewModel.name, + viewModel.emojiDict, + viewModel.statusesCount ) .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 title = name, let statusesCount = statusesCount, let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { @@ -316,7 +317,7 @@ extension ProfileViewController { return } 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 } .store(in: &disposeBag) @@ -368,7 +369,7 @@ extension ProfileViewController { .receive(on: DispatchQueue.main) .assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name) .store(in: &disposeBag) - viewModel.fileds + viewModel.fields .removeDuplicates() .map { fields -> [ProfileFieldItem.FieldValue] in fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value) } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index ddd1ee291..45a2386be 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -39,7 +39,7 @@ class ProfileViewModel: NSObject { let statusesCount: CurrentValueSubject let followingCount: CurrentValueSubject let followersCount: CurrentValueSubject - let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never> + let fields: CurrentValueSubject<[Mastodon.Entity.Field], Never> let emojiDict: CurrentValueSubject // fulfill this before editing @@ -82,7 +82,7 @@ class ProfileViewModel: NSObject { self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) }) self.protected = CurrentValueSubject(mastodonUser?.locked) self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false) - self.fileds = CurrentValueSubject(mastodonUser?.fields ?? []) + self.fields = CurrentValueSubject(mastodonUser?.fields ?? []) self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:]) super.init() @@ -257,7 +257,7 @@ extension ProfileViewModel { self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) } self.protected.value = mastodonUser?.locked self.suspended.value = mastodonUser?.suspended ?? false - self.fileds.value = mastodonUser?.fields ?? [] + self.fields.value = mastodonUser?.fields ?? [] self.emojiDict.value = mastodonUser?.emojiDict ?? [:] } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index d76cb24bd..b69914dbe 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -10,6 +10,7 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import ActiveLabel protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { func followButtonDidPressed(clickedUser: MastodonUser) @@ -42,8 +43,8 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) - let displayNameLabel: UILabel = { - let label = UILabel() + let displayNameLabel: ActiveLabel = { + let label = ActiveLabel(style: .statusName) label.textColor = .white label.textAlignment = .center label.font = .systemFont(ofSize: 18, weight: .semibold) @@ -164,7 +165,7 @@ extension SearchRecommendAccountsCollectionViewCell { } 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 avatarImageView.af.setImage( withURL: URL(string: mastodonUser.avatar)!, diff --git a/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift b/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift index b136859a8..33ef86dd0 100644 --- a/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift +++ b/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift @@ -6,13 +6,14 @@ // import UIKit +import ActiveLabel final class DoubleTitleLabelNavigationBarTitleView: UIView { let containerView = UIStackView() - let titleLabel: UILabel = { - let label = UILabel() + let titleLabel: ActiveLabel = { + let label = ActiveLabel(style: .default) label.font = .systemFont(ofSize: 17, weight: .semibold) label.textColor = Asset.Colors.Label.primary.color label.textAlignment = .center @@ -58,8 +59,8 @@ extension DoubleTitleLabelNavigationBarTitleView { containerView.addArrangedSubview(subtitleLabel) } - func update(title: String, subtitle: String?) { - titleLabel.text = title + func update(title: String, subtitle: String?, emojiDict: MastodonStatusContent.EmojiDict) { + titleLabel.configure(content: title, emojiDict: emojiDict) if let subtitle = subtitle { subtitleLabel.text = subtitle subtitleLabel.isHidden = false diff --git a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift index db56d63ca..221f9a208 100644 --- a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift +++ b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift @@ -11,6 +11,7 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import ActiveLabel protocol SuggestionAccountTableViewCellDelegate: AnyObject { func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell) @@ -28,8 +29,8 @@ final class SuggestionAccountTableViewCell: UITableViewCell { return imageView }() - let titleLabel: UILabel = { - let label = UILabel() + let titleLabel: ActiveLabel = { + let label = ActiveLabel(style: .statusName) label.textColor = Asset.Colors.brandBlue.color label.font = .systemFont(ofSize: 17, weight: .semibold) label.lineBreakMode = .byTruncatingTail @@ -153,7 +154,7 @@ extension SuggestionAccountTableViewCell { 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 button.isSelected = isSelected button.publisher(for: .touchUpInside) diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index 2023d1b1d..02cf3c38d 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -80,9 +80,13 @@ extension ThreadViewController { viewModel.navigationBarTitle .receive(on: DispatchQueue.main) - .sink { [weak self] title in + .sink { [weak self] tuple in 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) } diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index febc34d17..0599a0654 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -45,7 +45,7 @@ class ThreadViewModel { let ancestorItems = CurrentValueSubject<[Item], Never>([]) let descendantNodes = CurrentValueSubject<[LeafNode], Never>([]) let descendantItems = CurrentValueSubject<[Item], Never>([]) - let navigationBarTitle: CurrentValueSubject + let navigationBarTitle: CurrentValueSubject<(String, MastodonStatusContent.EmojiDict)?, Never> init(context: AppContext, optionalStatus: Status?) { self.context = context @@ -53,7 +53,7 @@ class ThreadViewModel { self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) }) self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil) 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 @@ -85,7 +85,7 @@ class ThreadViewModel { return } 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)