Status word preference

This commit is contained in:
Justin Mazzocchi 2021-01-30 17:43:48 -08:00
parent f5aacf7624
commit 5f03ce7d8b
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
15 changed files with 163 additions and 71 deletions

View file

@ -573,9 +573,9 @@ public extension ContentDatabase {
return accountsPublisher.combineLatest(statusesPublisher)
.map { accounts, statuses in
[.init(items: accounts, titleLocalizedStringKey: "search.scope.accounts"),
.init(items: statuses, titleLocalizedStringKey: "search.scope.statuses"),
.init(items: hashtags, titleLocalizedStringKey: "search.scope.tags")]
[.init(items: accounts, searchScope: .accounts),
.init(items: statuses, searchScope: .statuses),
.init(items: hashtags, searchScope: .tags)]
.filter { !$0.items.isEmpty }
}
.removeDuplicates()

View file

@ -4,10 +4,10 @@ import Foundation
public struct CollectionSection: Hashable {
public let items: [CollectionItem]
public let titleLocalizedStringKey: String?
public let searchScope: SearchScope?
public init(items: [CollectionItem], titleLocalizedStringKey: String? = nil) {
public init(items: [CollectionItem], searchScope: SearchScope? = nil) {
self.items = items
self.titleLocalizedStringKey = titleLocalizedStringKey
self.searchScope = searchScope
}
}

View file

@ -6,8 +6,11 @@ import ViewModels
final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection.Identifier, CollectionItem> {
private let updateQueue =
DispatchQueue(label: "com.metabolist.metatext.collection-data-source.update-queue")
private let viewModel: CollectionViewModel
init(tableView: UITableView, viewModel: CollectionViewModel) {
self.viewModel = viewModel
init(tableView: UITableView, viewModelProvider: @escaping (IndexPath) -> CollectionItemViewModel) {
for cellClass in CollectionItem.cellClasses {
tableView.register(cellClass, forCellReuseIdentifier: String(describing: cellClass))
}
@ -17,7 +20,7 @@ final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection
withIdentifier: String(describing: item.cellClass),
for: indexPath)
switch (cell, viewModelProvider(indexPath)) {
switch (cell, viewModel.viewModel(indexPath: indexPath)) {
case let (statusCell as StatusTableViewCell, statusViewModel as StatusViewModel):
statusCell.viewModel = statusViewModel
case let (accountCell as AccountTableViewCell, accountViewModel as AccountViewModel):
@ -32,8 +35,9 @@ final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection
tagCell.viewModel = tagViewModel
case let (_, moreResultsViewModel as MoreResultsViewModel):
var configuration = cell.defaultContentConfiguration()
let statusWord = viewModel.identityContext.appPreferences.statusWord
configuration.text = moreResultsViewModel.scope.moreDescription
configuration.text = moreResultsViewModel.scope.moreDescription(statusWord: statusWord)
cell.contentConfiguration = configuration
cell.accessoryType = .disclosureIndicator
@ -58,8 +62,8 @@ final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection
let section = currentSnapshot.sectionIdentifiers[section]
if currentSnapshot.numberOfItems(inSection: section) > 0,
let localizedStringKey = section.titleLocalizedStringKey {
return NSLocalizedString(localizedStringKey, comment: "")
let searchScope = section.searchScope {
return searchScope.title(statusWord: viewModel.identityContext.appPreferences.statusWord)
}
return nil

View file

@ -6,7 +6,7 @@ import ViewModels
extension CollectionSection {
struct Identifier: Hashable {
let index: Int
let titleLocalizedStringKey: String?
let searchScope: SearchScope?
}
}
@ -17,7 +17,7 @@ extension Array where Element == CollectionSection {
for (index, section) in enumerated() {
let identifier = CollectionSection.Identifier(
index: index,
titleLocalizedStringKey: section.titleLocalizedStringKey)
searchScope: section.searchScope)
snapshot.appendSections([identifier])
snapshot.appendItems(section.items, toSection: identifier)
}

View file

@ -4,12 +4,22 @@ import Foundation
import ViewModels
extension ProfileCollection {
var title: String {
func title(statusWord: AppPreferences.StatusWord) -> String {
switch self {
case .statuses:
return NSLocalizedString("account.statuses", comment: "")
switch statusWord {
case .toot:
return NSLocalizedString("account.statuses.toot", comment: "")
case .post:
return NSLocalizedString("account.statuses.post", comment: "")
}
case .statusesAndReplies:
return NSLocalizedString("account.statuses-and-replies", comment: "")
switch statusWord {
case .toot:
return NSLocalizedString("account.statuses-and-replies.toot", comment: "")
default:
return NSLocalizedString("account.statuses-and-replies.post", comment: "")
}
case .media:
return NSLocalizedString("account.media", comment: "")
}

View file

@ -4,27 +4,38 @@ import Foundation
import ViewModels
extension SearchScope {
var title: String {
func title(statusWord: AppPreferences.StatusWord) -> String {
switch self {
case .all:
return NSLocalizedString("search.scope.all", comment: "")
case .accounts:
return NSLocalizedString("search.scope.accounts", comment: "")
case .statuses:
return NSLocalizedString("search.scope.statuses", comment: "")
switch statusWord {
case .toot:
return NSLocalizedString("search.scope.statuses.toot", comment: "")
case .post:
return NSLocalizedString("search.scope.statuses.post", comment: "")
}
case .tags:
return NSLocalizedString("search.scope.tags", comment: "")
}
}
var moreDescription: String? {
func moreDescription(statusWord: AppPreferences.StatusWord) -> String? {
switch self {
case .all:
return nil
case .accounts:
return NSLocalizedString("more-results.accounts", comment: "")
case .statuses:
return NSLocalizedString("more-results.statuses", comment: "")
switch statusWord {
case .toot:
return NSLocalizedString("more-results.statuses.toot", comment: "")
case .post:
return NSLocalizedString("more-results.statuses.post", comment: "")
}
case .tags:
return NSLocalizedString("more-results.tags", comment: "")
}

View file

@ -17,8 +17,10 @@
"account.hide-reblogs" = "Hide boosts";
"account.mute" = "Mute";
"account.request" = "Request";
"account.statuses" = "Posts";
"account.statuses-and-replies" = "Posts & Replies";
"account.statuses.post" = "Posts";
"account.statuses.toot" = "Toots";
"account.statuses-and-replies.post" = "Posts & Replies";
"account.statuses-and-replies.toot" = "Toots & Replies";
"account.media" = "Media";
"account.show-reblogs" = "Show boosts";
"account.unavailable" = "Profile unavailable";
@ -129,12 +131,9 @@
"preferences.media.autoplay.always" = "Always";
"preferences.media.autoplay.wifi" = "On Wi-Fi";
"preferences.media.autoplay.never" = "Never";
"preferences.posting-reading" = "Posting and Reading";
"preferences.posting" = "Posting";
"preferences.use-preferences-from-server" = "Use preferences from server";
"preferences.posting-default-visiblility" = "Default visibility";
"preferences.posting-default-sensitive" = "Mark content sensitive by default";
"preferences.reading" = "Reading";
"preferences.reading-expand-media" = "Expand media";
"preferences.expand-media.default" = "Hide sensitive";
"preferences.expand-media.show-all" = "Show all";
@ -156,6 +155,7 @@
"preferences.position.sync-position" = "Sync position with web and other devices";
"preferences.position.newest" = "Load newest";
"preferences.show-reblog-and-favorite-counts" = "Show reblog and favorite counts";
"preferences.status-word" = "Status word";
"filters.active" = "Active";
"filters.expired" = "Expired";
"filter.add-new" = "Add New Filter";
@ -165,7 +165,7 @@
"filter.expire-after" = "Expire after";
"filter.contexts" = "Filter contexts";
"filter.irreversible" = "Drop instead of hide";
"filter.irreversible-explanation" = "Filtered posts will disappear irreversibly, even if filter is later removed";
"filter.irreversible-explanation" = "Filtered statuses will disappear irreversibly, even if filter is later removed";
"filter.whole-word" = "Whole word";
"filter.whole-word-explanation" = "When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word";
"filter.save-changes" = "Save Changes";
@ -176,7 +176,8 @@
"filter.context.account" = "Profiles";
"filter.context.unknown" = "Unknown context";
"more-results.accounts" = "More people";
"more-results.statuses" = "More posts";
"more-results.statuses.post" = "More posts";
"more-results.statuses.toot" = "More toots";
"more-results.tags" = "More hashtags";
"notifications" = "Notifications";
"notifications.reblogged-your-status" = "%@ boosted your status";
@ -194,18 +195,22 @@
"report.forward-%@" = "Forward report to %@";
"search.scope.all" = "All";
"search.scope.accounts" = "People";
"search.scope.statuses" = "Posts";
"search.scope.statuses.post" = "Posts";
"search.scope.statuses.toot" = "Toots";
"search.scope.tags" = "Hashtags";
"share-extension-error.no-account-found" = "No account found";
"status.bookmark" = "Bookmark";
"status.content-warning-abbreviation" = "CW";
"status.delete" = "Delete";
"status.delete.confirm" = "Are you sure you want to delete this post?";
"status.delete.confirm.post" = "Are you sure you want to delete this post?";
"status.delete.confirm.toot" = "Are you sure you want to delete this toot?";
"status.delete-and-redraft" = "Delete & re-draft";
"status.delete-and-redraft.confirm" = "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.";
"status.delete-and-redraft.confirm.post" = "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.";
"status.delete-and-redraft.confirm.toot" = "Are you sure you want to delete this toot and re-draft it? Favorites and boosts will be lost, and replies to the original toot will be orphaned.";
"status.mute" = "Mute conversation";
"status.pin" = "Pin on profile";
"status.pinned-post" = "Pinned post";
"status.pinned.post" = "Pinned post";
"status.pinned.toot" = "Pinned toot";
"status.poll.vote" = "Vote";
"status.poll.time-left" = "%@ left";
"status.poll.refresh" = "Refresh";
@ -229,3 +234,4 @@
"timelines.home" = "Home";
"timelines.local" = "Local";
"timelines.federated" = "Federated";
"toot" = "Toot";

View file

@ -15,6 +15,13 @@ public struct AppPreferences {
}
public extension AppPreferences {
enum StatusWord: String, CaseIterable, Identifiable {
case toot
case post
public var id: String { rawValue }
}
enum AnimateAvatars: String, CaseIterable, Identifiable {
case everywhere
case profiles
@ -44,6 +51,18 @@ public extension AppPreferences {
set { self[.useSystemReduceMotionForMedia] = newValue }
}
var statusWord: StatusWord {
get {
if let rawValue = self[.statusWord] as String?,
let value = StatusWord(rawValue: rawValue) {
return value
}
return .toot
}
set { self[.statusWord] = newValue.rawValue }
}
var animateAvatars: AnimateAvatars {
get {
if let rawValue = self[.animateAvatars] as String?,
@ -140,23 +159,9 @@ public extension AppPreferences {
}
}
extension AppPreferences {
var updatedInstanceFilter: BloomFilter<String>? {
guard let data = self[.updatedFilter] as Data? else {
return nil
}
return try? JSONDecoder().decode(BloomFilter<String>.self, from: data)
}
func updateInstanceFilter( _ filter: BloomFilter<String>) {
userDefaults.set(try? JSONEncoder().encode(filter), forKey: Item.updatedFilter.rawValue)
}
}
private extension AppPreferences {
enum Item: String {
case updatedFilter
case statusWord
case useSystemReduceMotionForMedia
case animateAvatars
case animateHeaders

View file

@ -53,10 +53,16 @@ final class ExploreViewController: UICollectionViewController {
let searchController = UISearchController(searchResultsController: searchResultsController)
searchController.searchBar.scopeButtonTitles = SearchScope.allCases.map(\.title)
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController
viewModel.identityContext.$appPreferences.sink { appPreferences in
searchController.searchBar.scopeButtonTitles = SearchScope.allCases.map {
$0.title(statusWord: appPreferences.statusWord)
}
}
.store(in: &cancellables)
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
viewModel.$loading.sink { [weak self] in

View file

@ -15,7 +15,7 @@ final class NewStatusViewController: UIViewController {
private let stackView = UIStackView()
private let activityIndicatorView = UIActivityIndicatorView(style: .large)
private let postButton = UIBarButtonItem(
title: NSLocalizedString("post", comment: ""),
title: nil,
style: .done,
target: nil,
action: nil)
@ -40,6 +40,7 @@ final class NewStatusViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
// swiftlint:disable:next function_body_length
override func viewDidLoad() {
super.viewDidLoad()
@ -76,7 +77,16 @@ final class NewStatusViewController: UIViewController {
primaryAction: UIAction { [weak self] _ in self?.dismiss() })
navigationItem.rightBarButtonItem = postButton
postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
let postActionTitle: String
switch viewModel.identityContext.appPreferences.statusWord {
case .toot:
postActionTitle = NSLocalizedString("toot", comment: "")
case .post:
postActionTitle = NSLocalizedString("post", comment: "")
}
postButton.primaryAction = UIAction(title: postActionTitle) { [weak self] _ in
self?.viewModel.post()
}

View file

@ -25,10 +25,7 @@ final class ProfileViewController: TableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Initial size is to avoid unsatisfiable constraint warning
let accountHeaderView = AccountHeaderView(frame: .init(origin: .zero, size: .init(width: 300, height: 300)))
accountHeaderView.viewModel = viewModel
let accountHeaderView = AccountHeaderView(viewModel: viewModel)
viewModel.$accountViewModel
.receive(on: DispatchQueue.main)

View file

@ -24,7 +24,7 @@ class TableViewController: UITableViewController {
private weak var parentNavigationController: UINavigationController?
private lazy var dataSource: TableViewDataSource = {
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
.init(tableView: tableView, viewModel: viewModel)
}()
init(viewModel: CollectionViewModel,
@ -441,11 +441,23 @@ private extension TableViewController {
}
func confirmDelete(statusViewModel: StatusViewModel, redraft: Bool) {
let deleteAndRedraftConfirmMessage: String
let deleteConfirmMessage: String
switch viewModel.identityContext.appPreferences.statusWord {
case .toot:
deleteAndRedraftConfirmMessage = NSLocalizedString("status.delete-and-redraft.confirm.toot", comment: "")
deleteConfirmMessage = NSLocalizedString("status.delete.confirm.toot", comment: "")
case .post:
deleteAndRedraftConfirmMessage = NSLocalizedString("status.delete-and-redraft.confirm.post", comment: "")
deleteConfirmMessage = NSLocalizedString("status.delete.confirm.post", comment: "")
}
let alertController = UIAlertController(
title: nil,
message: redraft
? NSLocalizedString("status.delete-and-redraft.confirm", comment: "")
: NSLocalizedString("status.delete.confirm", comment: ""),
? deleteAndRedraftConfirmMessage
: deleteConfirmMessage,
preferredStyle: .alert)
let deleteAction = UIAlertAction(

View file

@ -62,6 +62,12 @@ struct PreferencesView: View {
.disabled(viewModel.preferences.useServerPostingReadingPreferences)
}
Section(header: Text("preferences.app")) {
Picker("preferences.status-word",
selection: $identityContext.appPreferences.statusWord) {
ForEach(AppPreferences.StatusWord.allCases) { option in
Text(option.localizedStringKey).tag(option)
}
}
if accessibilityReduceMotion {
Toggle("preferences.media.use-system-reduce-motion",
isOn: $identityContext.appPreferences.useSystemReduceMotionForMedia)
@ -119,6 +125,17 @@ private extension PreferencesView {
}
}
extension AppPreferences.StatusWord {
var localizedStringKey: LocalizedStringKey {
switch self {
case .toot:
return "toot"
case .post:
return "post"
}
}
}
extension AppPreferences.AnimateAvatars {
var localizedStringKey: LocalizedStringKey {
switch self {

View file

@ -26,9 +26,9 @@ final class AccountHeaderView: UIView {
let segmentedControl = UISegmentedControl()
let unavailableLabel = UILabel()
var viewModel: ProfileViewModel? {
var viewModel: ProfileViewModel {
didSet {
if let accountViewModel = viewModel?.accountViewModel {
if let accountViewModel = viewModel.accountViewModel {
headerImageView.kf.setImage(with: accountViewModel.headerURL) { [weak self] in
if case let .success(result) = $0, result.image.size != Self.missingHeaderImageSize {
self?.headerButton.isEnabled = true
@ -127,8 +127,11 @@ final class AccountHeaderView: UIView {
}
}
override init(frame: CGRect) {
super.init(frame: frame)
init(viewModel: ProfileViewModel) {
self.viewModel = viewModel
// Initial size is to avoid unsatisfiable constraint warning
super.init(frame: .init(origin: .zero, size: .init(width: 300, height: 300)))
initialSetup()
}
@ -158,7 +161,7 @@ extension AccountHeaderView: UITextViewDelegate {
interaction: UITextItemInteraction) -> Bool {
switch interaction {
case .invokeDefaultAction:
viewModel?.accountViewModel?.urlSelected(URL)
viewModel.accountViewModel?.urlSelected(URL)
return false
case .preview: return false
case .presentActions: return false
@ -189,7 +192,7 @@ private extension AccountHeaderView {
headerButton.translatesAutoresizingMaskIntoConstraints = false
headerButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
headerButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentHeader() }, for: .touchUpInside)
headerButton.addAction(UIAction { [weak self] _ in self?.viewModel.presentHeader() }, for: .touchUpInside)
headerButton.isEnabled = false
let avatarBackgroundViewDimension = Self.avatarDimension + .compactSpacing * 2
@ -210,7 +213,7 @@ private extension AccountHeaderView {
avatarButton.translatesAutoresizingMaskIntoConstraints = false
avatarButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
avatarButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentAvatar() }, for: .touchUpInside)
avatarButton.addAction(UIAction { [weak self] _ in self?.viewModel.presentAvatar() }, for: .touchUpInside)
addSubview(relationshipButtonsStackView)
relationshipButtonsStackView.translatesAutoresizingMaskIntoConstraints = false
@ -230,7 +233,7 @@ private extension AccountHeaderView {
withConfiguration: UIImage.SymbolConfiguration(scale: .small)),
for: .normal)
followButton.addAction(
UIAction { [weak self] _ in self?.viewModel?.accountViewModel?.follow() },
UIAction { [weak self] _ in self?.viewModel.accountViewModel?.follow() },
for: .touchUpInside)
unfollowButton.setImage(
@ -241,7 +244,7 @@ private extension AccountHeaderView {
unfollowButton.setTitle(NSLocalizedString("account.following", comment: ""), for: .normal)
unfollowButton.showsMenuAsPrimaryAction = true
unfollowButton.menu = UIMenu(children: [UIDeferredMenuElement { [weak self] completion in
guard let accountViewModel = self?.viewModel?.accountViewModel else { return }
guard let accountViewModel = self?.viewModel.accountViewModel else { return }
let unfollowAction = UIAction(
title: NSLocalizedString("account.unfollow", comment: ""),
@ -299,20 +302,22 @@ private extension AccountHeaderView {
followStackView.distribution = .fillEqually
followingButton.addAction(
UIAction { [weak self] _ in self?.viewModel?.accountViewModel?.followingSelected() },
UIAction { [weak self] _ in self?.viewModel.accountViewModel?.followingSelected() },
for: .touchUpInside)
followStackView.addArrangedSubview(followingButton)
followersButton.addAction(
UIAction { [weak self] _ in self?.viewModel?.accountViewModel?.followersSelected() },
UIAction { [weak self] _ in self?.viewModel.accountViewModel?.followersSelected() },
for: .touchUpInside)
followStackView.addArrangedSubview(followersButton)
let statusWord = viewModel.identityContext.appPreferences.statusWord
for (index, collection) in ProfileCollection.allCases.enumerated() {
segmentedControl.insertSegment(
action: UIAction(title: collection.title) { [weak self] _ in
self?.viewModel?.collection = collection
self?.viewModel?.request(maxId: nil, minId: nil, search: nil)
action: UIAction(title: collection.title(statusWord: statusWord)) { [weak self] _ in
self?.viewModel.collection = collection
self?.viewModel.request(maxId: nil, minId: nil, search: nil)
},
at: index,
animated: false)

View file

@ -354,7 +354,16 @@ private extension StatusView {
infoLabel.isHidden = false
infoIcon.isHidden = false
} else if viewModel.configuration.isPinned {
infoLabel.text = NSLocalizedString("status.pinned-post", comment: "")
let pinnedText: String
switch viewModel.identityContext.appPreferences.statusWord {
case .toot:
pinnedText = NSLocalizedString("status.pinned.toot", comment: "")
case .post:
pinnedText = NSLocalizedString("status.pinned.post", comment: "")
}
infoLabel.text = pinnedText
infoIcon.image = UIImage(
systemName: "pin",
withConfiguration: UIImage.SymbolConfiguration(scale: .small))