From 027fec1cc9d10960a2c4dac009a98988abbd95b7 Mon Sep 17 00:00:00 2001 From: jk234ert Date: Wed, 24 Feb 2021 22:47:42 +0800 Subject: [PATCH] feat: implement pick server view search cell & server list cell --- Mastodon.xcodeproj/project.pbxproj | 8 + Mastodon/Generated/Strings.swift | 12 + .../Resources/en.lproj/Localizable.strings | 6 + .../PickServer/PickServerViewController.swift | 16 +- .../PickServer/PickServerViewModel.swift | 131 ++++++- .../TableViewCell/PickServerCell.swift | 322 ++++++++++++++++++ .../TableViewCell/PickServerSearchCell.swift | 101 ++++++ .../View/PickServerCategoryView.swift | 14 +- .../Entity/Mastodon+Entity+Server.swift | 15 + 9 files changed, 596 insertions(+), 29 deletions(-) create mode 100644 Mastodon/Scene/PickServer/TableViewCell/PickServerCell.swift create mode 100644 Mastodon/Scene/PickServer/TableViewCell/PickServerSearchCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 795aa80a6..5c833cd3f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; }; 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; }; 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; }; + 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */; }; + 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; }; @@ -214,6 +216,8 @@ 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = ""; }; 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = ""; }; 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = ""; }; + 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = ""; }; + 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; @@ -438,6 +442,8 @@ children = ( 0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */, 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, + 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */, + 0FB3D33725E6401400AAD544 /* PickServerCell.swift */, ); path = TableViewCell; sourceTree = ""; @@ -1370,6 +1376,7 @@ 0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, + 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, @@ -1417,6 +1424,7 @@ DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, + 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 4c53c7faa..81b3c8c35 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -111,6 +111,10 @@ internal enum L10n { /// Pick a Server,\nany server. internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title") internal enum Button { + /// See less + internal static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess") + /// See More + internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore") internal enum Category { /// All internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All") @@ -120,6 +124,14 @@ internal enum L10n { /// Find a server or join your own... internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder") } + internal enum Label { + /// CATEGORY + internal static let category = L10n.tr("Localizable", "Scene.ServerPicker.Label.Category") + /// LANGUAGE + internal static let language = L10n.tr("Localizable", "Scene.ServerPicker.Label.Language") + /// USERS + internal static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users") + } } internal enum ServerRules { /// By continuing, you're subject to the terms of service and privacy policy for %@. diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index a0e04cffb..6a6c6477d 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -33,6 +33,12 @@ "Scene.ServerPicker.Title" = "Pick a Server, any server."; "Scene.ServerPicker.Button.Category.All" = "All"; +"Scene.ServerPicker.Button.SeeLess" = "See less"; +"Scene.ServerPicker.Button.SeeMore" = "See More"; +"Scene.ServerPicker.Label.Language" = "LANGUAGE"; +"Scene.ServerPicker.Label.Users" = "USERS"; +"Scene.ServerPicker.Label.Category" = "CATEGORY"; + "Scene.ServerRules.Button.Confirm" = "I Agree"; "Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; diff --git a/Mastodon/Scene/PickServer/PickServerViewController.swift b/Mastodon/Scene/PickServer/PickServerViewController.swift index a2a4a4078..5a2188ea7 100644 --- a/Mastodon/Scene/PickServer/PickServerViewController.swift +++ b/Mastodon/Scene/PickServer/PickServerViewController.swift @@ -32,7 +32,8 @@ final class PickServerViewController: UIViewController, NeedsDependency { let tableView = ControlContainableTableView() tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self)) tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self)) -// tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self)) + tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear @@ -83,7 +84,20 @@ extension PickServerViewController { nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) } + viewModel.tableView = tableView tableView.delegate = viewModel tableView.dataSource = viewModel + + viewModel.searchedServers + .receive(on: DispatchQueue.main) + .sink { completion in + print("22") + } receiveValue: { [weak self] servers in + self?.tableView.reloadSections(IndexSet(integer: 3), with: .automatic) + } + .store(in: &disposeBag) + + + viewModel.fetchAllServers() } } diff --git a/Mastodon/Scene/PickServer/PickServerViewModel.swift b/Mastodon/Scene/PickServer/PickServerViewModel.swift index 3215462ab..e194047c1 100644 --- a/Mastodon/Scene/PickServer/PickServerViewModel.swift +++ b/Mastodon/Scene/PickServer/PickServerViewModel.swift @@ -18,6 +18,7 @@ class PickServerViewModel: NSObject { enum Section: CaseIterable { case title case categories + case search case serverList } @@ -32,23 +33,24 @@ class PickServerViewModel: NSObject { case .All: return L10n.Scene.ServerPicker.Button.Category.all case .Some(let masCategory): + // TODO: Use emoji as placeholders switch masCategory.category { case .academia: - return "AC" + return "📚" case .activism: - return "AT" + return "✊" case .food: - return "F" + return "🍕" case .furry: - return "FU" + return "🦁" case .games: - return "G" + return "🕹" case .general: return "GE" case .journalism: - return "JO" + return "📰" case .lgbt: - return "LG" + return "🏳️‍🌈" case .regional: return "📍" case .art: @@ -58,7 +60,7 @@ class PickServerViewModel: NSObject { case .tech: return "📱" case ._other: - return "UN" + return "❓" } } } @@ -72,11 +74,15 @@ class PickServerViewModel: NSObject { let searchText = CurrentValueSubject(nil) - let allServers = CurrentValueSubject<[Mastodon.Entity.Instance], Error>([]) - let searchedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Error>([]) + let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) + let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) let nextButtonEnable = CurrentValueSubject(false) + private var disposeBag = Set() + + weak var tableView: UITableView? + init(context: AppContext, mode: PickServerMode) { self.context = context self.mode = mode @@ -89,20 +95,89 @@ class PickServerViewModel: NSObject { let masCategories = context.apiService.stubCategories() categories.append(.All) categories.append(contentsOf: masCategories.map { Category.Some($0) }) + + Publishers.CombineLatest3( + selectCategoryIndex, + searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), + allServers + ) + .flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<[Mastodon.Entity.Server], Error> in + guard let self = self else { return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher() } + + // 1. Search from the servers recorded in joinmastodon.org + let searchedServersFromAPI = self.searchServersFromAPI(category: self.categories[selectCategoryIndex], searchText: searchText, allServers: allServers) + if !searchedServersFromAPI.isEmpty { + // If found servers, just return + return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher() + } + // 2. No server found in the recorded list, check if searchText is a valid mastodon server domain + if let toSearchText = searchText, !toSearchText.isEmpty { + return self.context.apiService.instance(domain: toSearchText) + .map { return [Mastodon.Entity.Server(instance: $0.value)] }.eraseToAnyPublisher() + } + return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher() + } + .sink { completion in + print("1") + } receiveValue: { [weak self] servers in + self?.searchedServers.send(servers) + } + .store(in: &disposeBag) + + + } + + func fetchAllServers() { + context.apiService.servers(language: nil, category: nil) + .receive(on: DispatchQueue.main) + .sink { error in + print("11") + } receiveValue: { [weak self] result in + self?.allServers.send(result.value) + } + .store(in: &disposeBag) + + } + + private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] { + return allServers + // 1. Filter the category + .filter { + switch category { + case .All: + return true + case .Some(let masCategory): + return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame + } + } + // 2. Filter the searchText + .filter { + if let searchText = searchText { + return $0.domain.contains(searchText) + } else { + return true + } + } } } extension PickServerViewModel: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if section == 0 { + let category = Section.allCases[section] + switch category { + case .title: return 20 - } - else if section == 1 { + case .categories: + // Since category view has a blur shadow effect, its height need to be large than the actual height, + // Thus we reduce the section header's height by 10, and make the category cell height 60+20(10 inset for top and bottom) return 10 - } - else { + case .search: + // Same reason as above return 10 + case .serverList: + // Header with 1 height as the separator + return 1 } } @@ -121,7 +196,8 @@ extension PickServerViewModel: UITableViewDataSource { let section = Self.Section.allCases[section] switch section { case .title, - .categories: + .categories, + .search: return 1 case .serverList: return searchedServers.value.count @@ -140,8 +216,15 @@ extension PickServerViewModel: UITableViewDataSource { cell.dataSource = self cell.delegate = self return cell + case .search: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell + cell.delegate = self + return cell case .serverList: - return UITableViewCell(style: .default, reuseIdentifier: "1") + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell + cell.server = searchedServers.value[indexPath.row] + cell.delegate = self + return cell } } } @@ -163,3 +246,17 @@ extension PickServerViewModel: PickServerCategoriesDataSource, PickServerCategor selectCategoryIndex.send(index) } } + +extension PickServerViewModel: PickServerSearchCellDelegate { + func pickServerSearchCell(didChange searchText: String?) { + self.searchText.send(searchText) + } +} + +extension PickServerViewModel: PickServerCellDelegate { + func pickServerCell(modeChange updates: (() -> Void)) { + tableView?.beginUpdates() + tableView?.performBatchUpdates(updates, completion: nil) + tableView?.endUpdates() + } +} diff --git a/Mastodon/Scene/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/PickServer/TableViewCell/PickServerCell.swift new file mode 100644 index 000000000..96ca78daf --- /dev/null +++ b/Mastodon/Scene/PickServer/TableViewCell/PickServerCell.swift @@ -0,0 +1,322 @@ +// +// PickServerCell.swift +// Mastodon +// +// Created by BradGao on 2021/2/24. +// + +import UIKit +import MastodonSDK +import Kingfisher + +protocol PickServerCellDelegate: class { + func pickServerCell(modeChange updates: (() -> Void)) +} + +class PickServerCell: UITableViewCell { + + weak var delegate: PickServerCellDelegate? + + enum Mode { + case collapse + case expand + } + + private var bgView: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.lightWhite.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private var domainLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .headline) + label.textColor = Asset.Colors.lightDarkGray.color + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var checkbox: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private var descriptionLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .subheadline) + label.numberOfLines = 0 + label.textColor = Asset.Colors.lightDarkGray.color + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var thumbImageView: UIImageView = { + let imageView = UIImageView() + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private var infoStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .fill + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private var expandBox: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private var expandButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal) + button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected) + button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal) + button.titleLabel?.font = .preferredFont(forTextStyle: .footnote) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private var seperator: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.lightBackground.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private var langValueLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightDarkGray.color + label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var usersValueLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightDarkGray.color + label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var categoryValueLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightDarkGray.color + label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var langTitleLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightDarkGray.color + label.font = .preferredFont(forTextStyle: .caption2) + label.text = L10n.Scene.ServerPicker.Label.language + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var usersTitleLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightDarkGray.color + label.font = .preferredFont(forTextStyle: .caption2) + label.text = L10n.Scene.ServerPicker.Label.users + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var categoryTitleLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightDarkGray.color + label.font = .preferredFont(forTextStyle: .caption2) + label.text = L10n.Scene.ServerPicker.Label.category + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var collapseConstraints: [NSLayoutConstraint] = [] + private var expandConstraints: [NSLayoutConstraint] = [] + + var mode: PickServerCell.Mode = .collapse { + didSet { + updateMode() + } + } + + var server: Mastodon.Entity.Server? { + didSet { + updateServerInfo() + } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } +} + +// MARK: - Methods to configure appearance +extension PickServerCell { + private func _init() { + selectionStyle = .none + backgroundColor = .clear + + contentView.addSubview(bgView) + contentView.addSubview(domainLabel) + contentView.addSubview(checkbox) + contentView.addSubview(descriptionLabel) + contentView.addSubview(seperator) + + contentView.addSubview(expandButton) + + // Always add the expandbox which contains elements only visible in expand mode + contentView.addSubview(expandBox) + expandBox.addSubview(thumbImageView) + expandBox.addSubview(infoStackView) + expandBox.isHidden = true + + let verticalInfoStackViewLang = makeVerticalInfoStackView(arrangedView: langValueLabel, langTitleLabel) + let verticalInfoStackViewUsers = makeVerticalInfoStackView(arrangedView: usersValueLabel, usersTitleLabel) + let verticalInfoStackViewCategory = makeVerticalInfoStackView(arrangedView: categoryValueLabel, categoryTitleLabel) + infoStackView.addArrangedSubview(verticalInfoStackViewLang) + infoStackView.addArrangedSubview(verticalInfoStackViewUsers) + infoStackView.addArrangedSubview(verticalInfoStackViewCategory) + + let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12) + collapseConstraints.append(expandButtonTopConstraintInCollapse) + + let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8) + expandConstraints.append(expandButtonTopConstraintInExpand) + + NSLayoutConstraint.activate([ + // Set background view + bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + bgView.topAnchor.constraint(equalTo: contentView.topAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: bgView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: bgView.bottomAnchor, constant: 1), + + // Set bottom separator + seperator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: seperator.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: seperator.bottomAnchor), + seperator.heightAnchor.constraint(equalToConstant: 1), + + domainLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16), + domainLabel.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 16), + + checkbox.widthAnchor.constraint(equalToConstant: 23), + checkbox.heightAnchor.constraint(equalToConstant: 22), + bgView.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 16), + checkbox.leadingAnchor.constraint(equalTo: domainLabel.trailingAnchor, constant: 16), + + descriptionLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16), + descriptionLabel.topAnchor.constraint(equalTo: domainLabel.firstBaselineAnchor, constant: 8), + bgView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor, constant: 16), + + // Set expandBox constraints + expandBox.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16), + bgView.trailingAnchor.constraint(equalTo: expandBox.trailingAnchor, constant: 16), + expandBox.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8), + expandBox.bottomAnchor.constraint(equalTo: infoStackView.bottomAnchor), + + thumbImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor), + expandBox.trailingAnchor.constraint(equalTo: thumbImageView.trailingAnchor), + thumbImageView.topAnchor.constraint(equalTo: expandBox.topAnchor), + thumbImageView.heightAnchor.constraint(equalTo: thumbImageView.widthAnchor, multiplier: 151.0 / 303.0), + + infoStackView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor), + expandBox.trailingAnchor.constraint(equalTo: infoStackView.trailingAnchor), + infoStackView.topAnchor.constraint(equalTo: thumbImageView.bottomAnchor, constant: 16), + + expandButton.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16), + bgView.trailingAnchor.constraint(equalTo: expandButton.trailingAnchor, constant: 16), + bgView.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor, constant: 8), + ]) + + NSLayoutConstraint.activate(collapseConstraints) + + expandButton.addTarget(self, action: #selector(expandButtonDidClicked(_:)), for: .touchUpInside) + + } + + private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .equalCentering + stackView.spacing = 2 + arrangedView.forEach { stackView.addArrangedSubview($0) } + return stackView + } + + private func updateMode() { + switch mode { + case .collapse: + expandBox.isHidden = true + NSLayoutConstraint.deactivate(expandConstraints) + NSLayoutConstraint.activate(collapseConstraints) + case .expand: + expandBox.isHidden = false + NSLayoutConstraint.activate(expandConstraints) + NSLayoutConstraint.deactivate(collapseConstraints) + } + } + + @objc + private func expandButtonDidClicked(_ sender: UIButton) { + delegate?.pickServerCell(modeChange: { + let newMode: Mode = mode == .collapse ? .expand : .collapse + self.mode = newMode + }) + } +} + +// MARK: - Methods to update data +extension PickServerCell { + private func updateServerInfo() { + guard let serverInfo = server else { return } + domainLabel.text = serverInfo.domain + descriptionLabel.text = serverInfo.description + let processor = RoundCornerImageProcessor(cornerRadius: 3) + thumbImageView.kf.indicatorType = .activity + thumbImageView.kf.setImage(with: URL(string: serverInfo.proxiedThumbnail ?? "")!, placeholder: UIImage.placeholder(color: .yellow), options: [ + .processor(processor), + .scaleFactor(UIScreen.main.scale), + .transition(.fade(1)) + ]) + langValueLabel.text = serverInfo.language.uppercased() + usersValueLabel.text = "\(serverInfo.totalUsers)" + categoryValueLabel.text = serverInfo.category.uppercased() + } +} diff --git a/Mastodon/Scene/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/PickServer/TableViewCell/PickServerSearchCell.swift new file mode 100644 index 000000000..9065a086d --- /dev/null +++ b/Mastodon/Scene/PickServer/TableViewCell/PickServerSearchCell.swift @@ -0,0 +1,101 @@ +// +// PickServerSearchCell.swift +// Mastodon +// +// Created by BradGao on 2021/2/24. +// + +import UIKit + +protocol PickServerSearchCellDelegate: class { + func pickServerSearchCell(didChange searchText: String?) +} + +class PickServerSearchCell: UITableViewCell { + + weak var delegate: PickServerSearchCellDelegate? + + private var bgView: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.lightWhite.color + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.maskedCorners = [ + .layerMinXMinYCorner, + .layerMaxXMinYCorner + ] + view.layer.cornerCurve = .continuous + view.layer.cornerRadius = 10 + return view + }() + + private var textFieldBgView: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.lightBackground.color.withAlphaComponent(0.6) + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.masksToBounds = true + view.layer.cornerRadius = 6 + view.layer.cornerCurve = .continuous + return view + }() + + private var searchTextField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.font = .preferredFont(forTextStyle: .headline) + textField.tintColor = Asset.Colors.lightDarkGray.color + textField.textColor = Asset.Colors.lightDarkGray.color + textField.adjustsFontForContentSizeCategory = true + textField.attributedPlaceholder = + NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder, + attributes: [.font: UIFont.preferredFont(forTextStyle: .headline), + .foregroundColor: Asset.Colors.lightSecondaryText.color.withAlphaComponent(0.6)]) + textField.clearButtonMode = .whileEditing + return textField + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } +} + +extension PickServerSearchCell { + private func _init() { + self.selectionStyle = .none + backgroundColor = .clear + + searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + + contentView.addSubview(bgView) + contentView.addSubview(textFieldBgView) + contentView.addSubview(searchTextField) + + NSLayoutConstraint.activate([ + bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + bgView.topAnchor.constraint(equalTo: contentView.topAnchor), + bgView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + + textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14), + textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12), + bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14), + bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13), + + searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11), + searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4), + textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11), + textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4), + ]) + } +} + +extension PickServerSearchCell { + @objc func textFieldDidChange(_ textField: UITextField) { + delegate?.pickServerSearchCell(didChange: textField.text) + } +} diff --git a/Mastodon/Scene/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/PickServer/View/PickServerCategoryView.swift index 6fbabdf8f..c159f9bf3 100644 --- a/Mastodon/Scene/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/PickServer/View/PickServerCategoryView.swift @@ -54,20 +54,12 @@ class PickServerCategoryView: UIView { extension PickServerCategoryView { private func configure() { -// bgShadowView.backgroundColor = nil -// addSubview(bgShadowView) -// bgShadowView.addSubview(bgView) addSubview(bgView) addSubview(titleLabel) - bgView.backgroundColor = .white + bgView.backgroundColor = Asset.Colors.lightWhite.color NSLayoutConstraint.activate([ -// bgShadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor), -// bgShadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor), -// bgShadowView.topAnchor.constraint(equalTo: self.topAnchor), -// bgShadowView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor), bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor), bgView.topAnchor.constraint(equalTo: self.topAnchor), @@ -94,10 +86,10 @@ extension PickServerCategoryView { bgView.backgroundColor = Asset.Colors.lightBrandBlue.color bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0) if case .All = category { - titleLabel.textColor = .white + titleLabel.textColor = Asset.Colors.lightWhite.color } } else { - bgView.backgroundColor = .white + bgView.backgroundColor = Asset.Colors.lightWhite.color bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0) if case .All = category { titleLabel.textColor = Asset.Colors.lightBackground.color diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift index 2ff335d2d..f13cae687 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift @@ -37,6 +37,21 @@ extension Mastodon.Entity { case language case category } + + public init(instance: Instance) { + self.domain = instance.title + self.version = "\(instance.version)" + self.description = instance.description + self.language = instance.languages?.first ?? "" + self.languages = instance.languages ?? [] + self.region = "Unknown" // TODO: how to handle properties not in an instance + self.categories = [] + self.category = "Unknown" + self.proxiedThumbnail = instance.thumbnail + self.totalUsers = instance.statistics?.userCount ?? 0 + self.lastWeekUsers = 0 + self.approvalRequired = instance.approvalRequired ?? false + } } }