diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict
new file mode 100644
index 000000000..b002a41c3
--- /dev/null
+++ b/Localization/Localizable.stringsdict
@@ -0,0 +1,138 @@
+
+
+
+
+ plural.count.metric_formatted.post
+
+ NSStringLocalizedFormatKey
+ %@ %#@post_count@
+ post_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ posts
+ one
+ post
+ few
+ posts
+ many
+ posts
+ other
+ posts
+
+
+ plural.count.favorite
+
+ NSStringLocalizedFormatKey
+ %#@favorite_count@
+ favorite_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 favorites
+ one
+ 1 favorite
+ few
+ %ld favorites
+ many
+ %ld favorites
+ other
+ %ld favorites
+
+
+ plural.count.reblog
+
+ NSStringLocalizedFormatKey
+ %#@reblog_count@
+ reblog_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 reblogs
+ one
+ 1 reblog
+ few
+ %ld reblogs
+ many
+ %ld reblogs
+ other
+ %ld reblogs
+
+
+ plural.count.vote
+
+ NSStringLocalizedFormatKey
+ %#@vote_count@
+ vote_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 votes
+ one
+ 1 vote
+ few
+ %ld votes
+ many
+ %ld votes
+ other
+ %ld votes
+
+
+ plural.count.voter
+
+ NSStringLocalizedFormatKey
+ %#@voter_count@
+ voter_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 voters
+ one
+ 1 voter
+ few
+ %ld voters
+ many
+ %ld voters
+ other
+ %ld voters
+
+
+ plural.people_talking
+
+ NSStringLocalizedFormatKey
+ %#@count_people_talking@
+ count_people_talking
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 people talking
+ one
+ 1 people talking
+ few
+ %ld people talking
+ many
+ %ld people talking
+ other
+ %ld people talking
+
+
+
+
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 025678de1..271016b95 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -280,6 +280,7 @@
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; };
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; };
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; };
+ DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DB552D4E26BBD10C00E481F6 /* OrderedCollections */; };
DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */; };
DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; };
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
@@ -1311,6 +1312,7 @@
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
+ DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DB01E23326A98F0900C3965B /* MastodonMeta in Frameworks */,
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
@@ -3081,6 +3083,7 @@
DBC6462A26A1738900B0E31B /* MastodonUI */,
DB01E23226A98F0900C3965B /* MastodonMeta */,
DB01E23426A98F0900C3965B /* MetaTextKit */,
+ DB552D4E26BBD10C00E481F6 /* OrderedCollections */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@@ -3328,6 +3331,7 @@
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */,
+ DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@@ -4482,7 +4486,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4490,7 +4494,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -4509,7 +4513,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4517,7 +4521,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -4772,7 +4776,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4780,7 +4784,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4796,7 +4800,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4804,7 +4808,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4820,7 +4824,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4828,7 +4832,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4844,7 +4848,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4852,7 +4856,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4868,7 +4872,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4876,7 +4880,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4892,7 +4896,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4900,7 +4904,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4916,7 +4920,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4924,7 +4928,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -4940,7 +4944,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4948,7 +4952,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5030,7 +5034,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -5038,7 +5042,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -5144,7 +5148,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5152,7 +5156,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5264,7 +5268,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -5272,7 +5276,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -5378,7 +5382,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5386,7 +5390,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5432,7 +5436,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5440,7 +5444,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5455,7 +5459,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 52;
+ CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5463,7 +5467,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 1.0.2;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5644,6 +5648,14 @@
minimumVersion = 4.1.0;
};
};
+ DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/apple/swift-collections.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 0.0.5;
+ };
+ };
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git";
@@ -5752,6 +5764,11 @@
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage;
};
+ DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;
+ productName = OrderedCollections;
+ };
DB68050F2637D0F800430867 /* KeychainAccess */ = {
isa = XCSwiftPackageProductDependency;
package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 4bf0e25a9..10ea3fcff 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
AppShared.xcscheme_^#shared#^_
orderHint
- 24
+ 27
CoreDataStack.xcscheme_^#shared#^_
@@ -42,7 +42,7 @@
MastodonIntent.xcscheme_^#shared#^_
orderHint
- 28
+ 24
MastodonIntents.xcscheme_^#shared#^_
diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
index c6a46992b..67c613bff 100644
--- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -136,6 +136,15 @@
"version": "5.11.1"
}
},
+ {
+ "package": "swift-collections",
+ "repositoryURL": "https://github.com/apple/swift-collections.git",
+ "state": {
+ "branch": null,
+ "revision": "0959ba76a1d4a98fd11163aa83fd49c25b93bfae",
+ "version": "0.0.5"
+ }
+ },
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
diff --git a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift
index 64019e580..b894f818d 100644
--- a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift
+++ b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift
@@ -10,6 +10,9 @@ import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
+import MetaTextKit
+import MastodonMeta
+import Combine
enum RecommendAccountSection: Equatable, Hashable {
case main
@@ -18,18 +21,118 @@ enum RecommendAccountSection: Equatable, Hashable {
extension RecommendAccountSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
+ dependency: NeedsDependency,
delegate: SearchRecommendAccountsCollectionViewCellDelegate,
managedObjectContext: NSManagedObjectContext
) -> UICollectionViewDiffableDataSource {
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
- let user = managedObjectContext.object(with: objectID) as! MastodonUser
+ managedObjectContext.performAndWait {
+ let user = managedObjectContext.object(with: objectID) as! MastodonUser
+ configure(cell: cell, user: user, dependency: dependency)
+ }
cell.delegate = delegate
- cell.config(with: user)
return cell
}
}
+ static func configure(
+ cell: SearchRecommendAccountsCollectionViewCell,
+ user: MastodonUser,
+ dependency: NeedsDependency
+ ) {
+ configureContent(cell: cell, user: user)
+
+ if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user {
+ configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
+ }
+
+ Publishers.CombineLatest(
+ ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error },
+ dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self)
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { _ in
+ // do nothing
+ } receiveValue: { [weak cell] change, authentication in
+ guard let cell = cell else { return }
+ guard case .update(let object) = change.changeType,
+ let user = object as? MastodonUser else { return }
+ guard let currentMastodonUser = authentication?.user else { return }
+
+ configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
+ }
+ .store(in: &cell.disposeBag)
+
+ }
+
+ static func configureContent(
+ cell: SearchRecommendAccountsCollectionViewCell,
+ user: MastodonUser
+ ) {
+ do {
+ let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
+ let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
+ cell.displayNameLabel.configure(content: metaContent)
+ } catch {
+ let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
+ cell.displayNameLabel.configure(content: metaContent)
+ }
+ cell.acctLabel.text = "@" + user.acct
+ cell.avatarImageView.af.setImage(
+ withURL: user.avatarImageURLWithFallback(domain: user.domain),
+ placeholderImage: UIImage.placeholder(color: .systemFill),
+ imageTransition: .crossDissolve(0.2)
+ )
+ cell.headerImageView.af.setImage(
+ withURL: URL(string: user.header)!,
+ placeholderImage: UIImage.placeholder(color: .systemFill),
+ imageTransition: .crossDissolve(0.2)
+ ) { [weak cell] _ in
+ // guard let cell = cell else { return }
+ }
+ }
+
+ static func configureFollowButton(
+ with mastodonUser: MastodonUser,
+ currentMastodonUser: MastodonUser,
+ followButton: HighlightDimmableButton
+ ) {
+ let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
+ followButton.setTitle(relationshipActionSet.title, for: .normal)
+ }
+
+ static func relationShipActionSet(
+ mastodonUser: MastodonUser,
+ currentMastodonUser: MastodonUser
+ ) -> ProfileViewModel.RelationshipActionOptionSet {
+ var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
+ let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
+ if isFollowing {
+ relationshipActionSet.insert(.following)
+ }
+
+ let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
+ if isPending {
+ relationshipActionSet.insert(.pending)
+ }
+
+ let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
+ if isBlocking {
+ relationshipActionSet.insert(.blocking)
+ }
+
+ let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
+ if isBlockedBy {
+ relationshipActionSet.insert(.blocked)
+ }
+ return relationshipActionSet
+ }
+
+}
+
+extension RecommendAccountSection {
+
static func tableViewDiffableDataSource(
for tableView: UITableView,
managedObjectContext: NSManagedObjectContext,
diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
index e115151ea..4c34c6749 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
@@ -260,7 +260,7 @@ extension StatusProviderFacade {
// haptic feedback generator
let generator = UIImpactFeedbackGenerator(style: .light)
- let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
+ let responseFeedbackGenerator = UINotificationFeedbackGenerator()
status
.compactMap { status -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
@@ -284,13 +284,13 @@ extension StatusProviderFacade {
.eraseToAnyPublisher()
.switchToLatest()
.receive(on: DispatchQueue.main)
- .handleEvents { _ in
+ .handleEvents(receiveSubscription: { _ in
generator.prepare()
- responseFeedbackGenerator.prepare()
- } receiveOutput: { _, favoriteKind in
+ }, receiveOutput: { _, favoriteKind in
generator.impactOccurred()
os_log("%{public}s[%{public}ld], %{public}s: [Like] update local status like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
- } receiveCompletion: { completion in
+ }, receiveCompletion: { completion in
+ responseFeedbackGenerator.prepare()
switch completion {
case .failure:
// TODO: handle error
@@ -298,7 +298,7 @@ extension StatusProviderFacade {
case .finished:
break
}
- }
+ })
.map { statusID, favoriteKind in
return context.apiService.favorite(
statusID: statusID,
@@ -309,14 +309,13 @@ extension StatusProviderFacade {
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak provider] completion in
- guard let provider = provider else { return }
- if provider.view.window != nil {
- responseFeedbackGenerator.impactOccurred()
- }
+ guard let _ = provider else { return }
switch completion {
case .failure(let error):
+ responseFeedbackGenerator.notificationOccurred(.error)
os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
+ responseFeedbackGenerator.notificationOccurred(.success)
os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request success", ((#file as NSString).lastPathComponent), #line, #function)
}
} receiveValue: { response in
@@ -370,7 +369,7 @@ extension StatusProviderFacade {
// haptic feedback generator
let generator = UIImpactFeedbackGenerator(style: .light)
- let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
+ let responseFeedbackGenerator = UINotificationFeedbackGenerator()
status
.compactMap { status -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
@@ -394,10 +393,9 @@ extension StatusProviderFacade {
.eraseToAnyPublisher()
.switchToLatest()
.receive(on: DispatchQueue.main)
- .handleEvents { _ in
+ .handleEvents(receiveSubscription: { _ in
generator.prepare()
- responseFeedbackGenerator.prepare()
- } receiveOutput: { _, reblogKind in
+ }, receiveOutput: { _, reblogKind in
generator.impactOccurred()
switch reblogKind {
case .reblog:
@@ -405,7 +403,8 @@ extension StatusProviderFacade {
case .undoReblog:
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
}
- } receiveCompletion: { completion in
+ }, receiveCompletion: { completion in
+ responseFeedbackGenerator.prepare()
switch completion {
case .failure:
// TODO: handle error
@@ -413,7 +412,7 @@ extension StatusProviderFacade {
case .finished:
break
}
- }
+ })
.map { statusID, reblogKind in
return context.apiService.reblog(
statusID: statusID,
@@ -424,14 +423,13 @@ extension StatusProviderFacade {
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak provider] completion in
- guard let provider = provider else { return }
- if provider.view.window != nil {
- responseFeedbackGenerator.impactOccurred()
- }
+ guard let _ = provider else { return }
switch completion {
case .failure(let error):
+ responseFeedbackGenerator.notificationOccurred(.error)
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
+ responseFeedbackGenerator.notificationOccurred(.success)
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request success", ((#file as NSString).lastPathComponent), #line, #function)
}
} receiveValue: { response in
diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift
index 1cb9b5085..0c4910d17 100644
--- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift
+++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift
@@ -55,13 +55,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
} receiveValue: { [weak self] response in
guard let _ = self else { return }
stateMachine.enter(Idle.self)
-
- // ignore approval required servers
- var servers = response.value
- if viewModel.mode == .signUp {
- servers = servers.filter { !$0.approvalRequired }
- }
- viewModel.indexedServers.value = servers
+ viewModel.indexedServers.value = response.value
}
.store(in: &viewModel.disposeBag)
}
diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift
index ef9275b0d..7a6480118 100644
--- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift
+++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift
@@ -11,6 +11,7 @@ import Combine
import GameplayKit
import MastodonSDK
import CoreDataStack
+import OrderedCollections
class MastodonPickServerViewModel: NSObject {
@@ -167,7 +168,44 @@ extension MastodonPickServerViewModel {
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates()
)
.map { indexedServers, selectCategoryItem, searchText -> [Mastodon.Entity.Server] in
- // Filter the indexed servers from joinmastodon.org
+ // ignore approval required servers when sign-up
+ var indexedServers = indexedServers
+ if self.mode == .signUp {
+ indexedServers = indexedServers.filter { !$0.approvalRequired }
+ }
+
+ // group by language user preferred language first. Then sort by `totalUsers`
+ var languageToServersMapping = OrderedDictionary()
+ for language in Locale.preferredLanguages {
+ let local = Locale(identifier: language)
+ guard let languageCode = local.languageCode else { continue }
+ // skip if key duplicate
+ guard !languageToServersMapping.keys.contains(languageCode) else { continue }
+ // append to dict
+ languageToServersMapping[languageCode] = indexedServers
+ .filter { $0.language.lowercased() == languageCode.lowercased() }
+ .sorted(by: { $0.totalUsers > $1.totalUsers })
+ }
+ // sort remains servers by `totalUsers`
+ let remainsServers = indexedServers
+ .filter { server in
+ return !languageToServersMapping.contains { _, servers in servers.contains(server) }
+ }
+ .sorted(by: { $0.totalUsers > $1.totalUsers })
+
+ var _indexedServers: [Mastodon.Entity.Server] = []
+ for key in languageToServersMapping.keys {
+ _indexedServers.append(contentsOf: languageToServersMapping[key] ?? [])
+ }
+ _indexedServers.append(contentsOf: remainsServers)
+
+ if _indexedServers.count == indexedServers.count {
+ indexedServers = _indexedServers
+ } else {
+ assertionFailure("should not change dataset size")
+ }
+
+ // Filter the indexed servers by category or search text
switch selectCategoryItem {
case .all:
return MastodonPickServerViewModel.filterServers(servers: indexedServers, category: nil, searchText: searchText)
diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift
index 41cf8d38b..ee2471878 100644
--- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift
+++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift
@@ -87,7 +87,7 @@ class PickServerCell: UITableViewCell {
}()
let expandButton: UIButton = {
- let button = UIButton(type: .custom)
+ let button = HitTestExpandedButton(type: .custom)
button.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
index 216c81685..716b62307 100644
--- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
+++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
@@ -195,23 +195,34 @@ extension ProfileHeaderViewController {
}
.store(in: &disposeBag)
- Publishers.CombineLatest4(
+ let profileNote = Publishers.CombineLatest3(
viewModel.isEditing.removeDuplicates(),
viewModel.displayProfileInfo.note.removeDuplicates(),
- viewModel.editProfileInfo.note.removeDuplicates(),
+ viewModel.editProfileInfoDidInitialized
+ )
+ .map { isEditing, displayNote, _ -> String? in
+ if isEditing {
+ return self.viewModel.editProfileInfo.note.value
+ } else {
+ return displayNote
+ }
+ }
+ .eraseToAnyPublisher()
+
+ Publishers.CombineLatest3(
+ viewModel.isEditing.removeDuplicates(),
+ profileNote.removeDuplicates(),
viewModel.emojiMeta.removeDuplicates()
)
.receive(on: DispatchQueue.main)
- .sink { [weak self] isEditing, note, editingNote, emojiMeta in
+ .sink { [weak self] isEditing, note, emojiMeta in
guard let self = self else { return }
-
+
self.profileHeaderView.bioMetaText.textView.isEditable = isEditing
if isEditing {
- if self.profileHeaderView.bioMetaText.backedString != note {
- let metaContent = PlaintextMetaContent(string: editingNote ?? "")
- self.profileHeaderView.bioMetaText.configure(content: metaContent)
- }
+ let metaContent = PlaintextMetaContent(string: note ?? "")
+ self.profileHeaderView.bioMetaText.configure(content: metaContent)
} else {
let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
do {
@@ -224,6 +235,7 @@ extension ProfileHeaderViewController {
}
}
.store(in: &disposeBag)
+
profileHeaderView.bioMetaText.delegate = self
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
@@ -461,9 +473,15 @@ extension ProfileHeaderViewController {
extension ProfileHeaderViewController: MetaTextDelegate {
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString)
- assert(metaText.textView === profileHeaderView.bioMetaText.textView)
- if metaText.textView === profileHeaderView.bioMetaText.textView {
+
+ switch metaText {
+ case profileHeaderView.bioMetaText:
+ guard viewModel.isEditing.value else { break }
viewModel.editProfileInfo.note.value = metaText.backedString
+ let metaContent = PlaintextMetaContent(string: metaText.backedString)
+ return metaContent
+ default:
+ assertionFailure()
}
return nil
diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift
index c98758581..e8405b6ad 100644
--- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift
+++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift
@@ -31,6 +31,7 @@ final class ProfileHeaderViewModel {
// output
let displayProfileInfo = ProfileInfo()
let editProfileInfo = ProfileInfo()
+ let editProfileInfoDidInitialized = CurrentValueSubject(Void()) // needs trigger initial event
let isTitleViewDisplaying = CurrentValueSubject(false)
var fieldDiffableDataSource: UICollectionViewDiffableDataSource!
@@ -52,6 +53,7 @@ final class ProfileHeaderViewModel {
self.editProfileInfo.fields.value = account?.source?.fields?.compactMap { field in
ProfileFieldItem.FieldValue(name: field.name, value: field.value)
} ?? []
+ self.editProfileInfoDidInitialized.send()
}
.store(in: &disposeBag)
diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift
index cd1d196e4..365c1ee72 100644
--- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift
+++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift
@@ -5,6 +5,7 @@
// Created by sxiaojian on 2021/4/1.
//
+import os.log
import Combine
import CoreDataStack
import Foundation
@@ -14,12 +15,12 @@ import MetaTextKit
import MastodonMeta
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
- func followButtonDidPressed(clickedUser: MastodonUser)
-
- func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton)
+ func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton)
}
class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
+
+ let logger = Logger(subsystem: "SearchRecommendAccountsCollectionViewCell", category: "UI")
var disposeBag = Set()
weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate?
@@ -72,7 +73,6 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
super.prepareForReuse()
headerImageView.af.cancelImageRequest()
avatarImageView.af.cancelImageRequest()
- visualEffectView.removeFromSuperview()
disposeBag.removeAll()
}
@@ -117,6 +117,15 @@ extension SearchRecommendAccountsCollectionViewCell {
headerImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
+ headerImageView.addSubview(visualEffectView)
+ visualEffectView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ visualEffectView.topAnchor.constraint(equalTo: headerImageView.topAnchor),
+ visualEffectView.leadingAnchor.constraint(equalTo: headerImageView.leadingAnchor),
+ visualEffectView.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor),
+ visualEffectView.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor)
+ ])
+
let containerStackView = UIStackView()
containerStackView.axis = .vertical
containerStackView.distribution = .fill
@@ -156,48 +165,16 @@ extension SearchRecommendAccountsCollectionViewCell {
followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24)
])
containerStackView.addArrangedSubview(followButton)
+
+ followButton.addTarget(self, action: #selector(SearchRecommendAccountsCollectionViewCell.followButtonDidPressed(_:)), for: .touchUpInside)
}
- func config(with mastodonUser: MastodonUser) {
- do {
- let mastodonContent = MastodonContent(content: mastodonUser.displayNameWithFallback, emojis: mastodonUser.emojiMeta)
- let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
- displayNameLabel.configure(content: metaContent)
- } catch {
- let metaContent = PlaintextMetaContent(string: mastodonUser.displayNameWithFallback)
- displayNameLabel.configure(content: metaContent)
- }
- acctLabel.text = "@" + mastodonUser.acct
- avatarImageView.af.setImage(
- withURL: URL(string: mastodonUser.avatar)!,
- placeholderImage: UIImage.placeholder(color: .systemFill),
- imageTransition: .crossDissolve(0.2)
- )
- headerImageView.af.setImage(
- withURL: URL(string: mastodonUser.header)!,
- placeholderImage: UIImage.placeholder(color: .systemFill),
- imageTransition: .crossDissolve(0.2)
- ) { [weak self] _ in
- guard let self = self else { return }
- self.headerImageView.addSubview(self.visualEffectView)
- self.visualEffectView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- self.visualEffectView.topAnchor.constraint(equalTo: self.headerImageView.topAnchor),
- self.visualEffectView.leadingAnchor.constraint(equalTo: self.headerImageView.leadingAnchor),
- self.visualEffectView.trailingAnchor.constraint(equalTo: self.headerImageView.trailingAnchor),
- self.visualEffectView.bottomAnchor.constraint(equalTo: self.headerImageView.bottomAnchor)
- ])
- }
- delegate?.configFollowButton(with: mastodonUser, followButton: followButton)
- followButton.publisher(for: .touchUpInside)
- .sink { [weak self] _ in
- self?.followButtonDidPressed(mastodonUser: mastodonUser)
- }
- .store(in: &disposeBag)
- }
-
- func followButtonDidPressed(mastodonUser: MastodonUser) {
- delegate?.followButtonDidPressed(clickedUser: mastodonUser)
+}
+
+extension SearchRecommendAccountsCollectionViewCell {
+ @objc private func followButtonDidPressed(_ sender: UIButton) {
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
+ delegate?.searchRecommendAccountsCollectionViewCell(self, followButtonDidPressed: sender)
}
}
diff --git a/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift
index c345336df..386b0af18 100644
--- a/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift
+++ b/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift
@@ -26,16 +26,30 @@ extension SearchViewController: UserProvider {
}
extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate {
- func followButtonDidPressed(clickedUser: MastodonUser) {
+ func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton) {
+ guard let diffableDataSource = viewModel.accountDiffableDataSource else { return }
+ guard let indexPath = accountsCollectionView.indexPath(for: cell),
+ let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
+
+ context.managedObjectContext.performAndWait {
+ guard let user = try? context.managedObjectContext.existingObject(with: item) as? MastodonUser else { return }
+ self.toggleFriendship(for: user)
+ }
+ }
+
+ func toggleFriendship(for mastodonUser: MastodonUser) {
guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
return
}
- guard let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) else { return }
+ guard let relationshipAction = RecommendAccountSection.relationShipActionSet(
+ mastodonUser: mastodonUser,
+ currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions)
+ else { return }
switch relationshipAction {
case .none:
break
case .follow, .following:
- UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: clickedUser)
+ UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: mastodonUser)
.sink { _ in
// error handling
} receiveValue: { _ in
@@ -45,7 +59,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
case .pending:
break
case .muting:
- let name = clickedUser.displayNameWithFallback
+ let name = mastodonUser.displayNameWithFallback
let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
@@ -53,7 +67,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
)
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
guard let self = self else { return }
- UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: clickedUser)
+ UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: mastodonUser)
.sink { _ in
// do nothing
} receiveValue: { _ in
@@ -66,7 +80,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
case .blocking:
- let name = clickedUser.displayNameWithFallback
+ let name = mastodonUser.displayNameWithFallback
let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name),
@@ -74,7 +88,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
)
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
guard let self = self else { return }
- UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: clickedUser)
+ UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: mastodonUser)
.sink { _ in
// do nothing
} receiveValue: { _ in
@@ -93,50 +107,4 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
}
}
- func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) {
- guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
- return
- }
- _configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser, followButton: followButton)
- ManagedObjectObserver.observe(object: currentMastodonUser)
- .sink { _ in
-
- } receiveValue: { change in
- guard case .update(let object) = change.changeType,
- let newUser = object as? MastodonUser else { return }
- self._configFollowButton(with: mastodonUser, currentMastodonUser: newUser, followButton: followButton)
- }
- .store(in: &disposeBag)
- }
-}
-
-extension SearchViewController {
- func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser, followButton: HighlightDimmableButton) {
- let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
- followButton.setTitle(relationshipActionSet.title, for: .normal)
- }
-
- func relationShipActionSet(mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) -> ProfileViewModel.RelationshipActionOptionSet {
- var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
- let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
- if isFollowing {
- relationshipActionSet.insert(.following)
- }
-
- let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
- if isPending {
- relationshipActionSet.insert(.pending)
- }
-
- let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
- if isBlocking {
- relationshipActionSet.insert(.blocking)
- }
-
- let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
- if isBlockedBy {
- relationshipActionSet.insert(.blocked)
- }
- return relationshipActionSet
- }
}
diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift
index 7c5003703..a3d84cd6a 100644
--- a/Mastodon/Scene/Search/Search/SearchViewController.swift
+++ b/Mastodon/Scene/Search/Search/SearchViewController.swift
@@ -40,7 +40,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
var searchTransitionController = SearchTransitionController()
var disposeBag = Set()
- private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator)
+ private(set) lazy var viewModel = SearchViewModel(context: context)
// recommend
let scrollView: UIScrollView = {
@@ -167,7 +167,12 @@ extension SearchViewController {
private func setupDataSource() {
viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView)
- viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, delegate: self, managedObjectContext: context.managedObjectContext)
+ viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(
+ for: accountsCollectionView,
+ dependency: self,
+ delegate: self,
+ managedObjectContext: context.managedObjectContext
+ )
}
}
diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift
index 4929ccca4..1c2456091 100644
--- a/Mastodon/Scene/Search/Search/SearchViewModel.swift
+++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift
@@ -19,24 +19,25 @@ final class SearchViewModel: NSObject {
// input
let context: AppContext
- weak var coordinator: SceneCoordinator!
-
- let currentMastodonUser = CurrentValueSubject(nil)
let viewDidAppeared = PassthroughSubject()
// output
+ let currentMastodonUser = CurrentValueSubject(nil)
- // var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject()
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource?
var accountDiffableDataSource: UICollectionViewDiffableDataSource?
- init(context: AppContext, coordinator: SceneCoordinator) {
- self.coordinator = coordinator
+ init(context: AppContext) {
self.context = context
super.init()
+
+ context.authenticationService.activeMastodonAuthentication
+ .map { $0?.user }
+ .assign(to: \.value, on: currentMastodonUser)
+ .store(in: &disposeBag)
Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox,
diff --git a/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift
index 6c67580f9..91ec0128c 100644
--- a/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift
+++ b/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift
@@ -64,18 +64,15 @@ extension SearchRecommendCollectionHeader {
])
let horizontalStackView = UIStackView()
+ horizontalStackView.spacing = 8
horizontalStackView.axis = .horizontal
horizontalStackView.alignment = .center
- horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
horizontalStackView.distribution = .fill
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
horizontalStackView.addArrangedSubview(titleLabel)
- seeAllButton.translatesAutoresizingMaskIntoConstraints = false
horizontalStackView.addArrangedSubview(seeAllButton)
containerStackView.addArrangedSubview(horizontalStackView)
- descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(descriptionLabel)
}
diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift
index 8336e852e..a62230441 100644
--- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift
+++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift
@@ -207,6 +207,7 @@ extension MosaicImageViewContainer {
func setupImageViews(count: Int, maxSize: CGSize) -> [ConfigurableMosaic] {
reset()
+ let count = min(4, max(0, count))
guard count > 1 else {
return []
}
diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift
index 794f9c761..f771f8bb2 100644
--- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift
+++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift
@@ -100,6 +100,8 @@ extension ActionToolbarContainer {
button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding)
}
+ // add more expand for menu button
+ moreButton.expandEdgeInsets = UIEdgeInsets(top: -10, left: -20, bottom: -10, right: -20)
let replyImage = UIImage(systemName: "arrowshape.turn.up.left.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .ultraLight))!.withRenderingMode(.alwaysTemplate)
let reblogImage = UIImage(systemName: "arrow.2.squarepath", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate)
diff --git a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift
index 64bde2e66..af8978c30 100644
--- a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift
+++ b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift
@@ -119,10 +119,10 @@ final class VideoPlayerViewModel {
case .unknown, .buffering, .readyToPlay:
break
case .playing:
- try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
+ try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
case .paused, .stopped, .failed:
- try? AVAudioSession.sharedInstance().setCategory(.ambient)
+ try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) // reset to default
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
}
diff --git a/Mastodon/Service/AudioPlaybackService.swift b/Mastodon/Service/AudioPlaybackService.swift
index 6fdac4bf3..5abb30c6c 100644
--- a/Mastodon/Service/AudioPlaybackService.swift
+++ b/Mastodon/Service/AudioPlaybackService.swift
@@ -39,10 +39,10 @@ final class AudioPlaybackService: NSObject {
case .unknown, .buffering, .readyToPlay:
break
case .playing:
- try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
+ try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
case .paused, .stopped, .failed:
- try? AVAudioSession.sharedInstance().setCategory(.ambient)
+ try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) // reset to default
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
}
diff --git a/Mastodon/Service/VideoPlaybackService.swift b/Mastodon/Service/VideoPlaybackService.swift
index e0ac5e6ff..f1e289926 100644
--- a/Mastodon/Service/VideoPlaybackService.swift
+++ b/Mastodon/Service/VideoPlaybackService.swift
@@ -40,7 +40,6 @@ extension VideoPlaybackService {
} else {
if latestPlayingVideoPlayerViewModel === playerViewModel {
latestPlayingVideoPlayerViewModel = nil
-// try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default)
}
}
}
diff --git a/README.md b/README.md
index d8f7a9c6e..e31c4879b 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,7 @@ The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay)
- [Nuke](https://github.com/kean/Nuke)
- [Pageboy](https://github.com/uias/Pageboy#the-basics)
- [SDWebImage](https://github.com/SDWebImage/SDWebImage)
+- [swift-collections](https://github.com/apple/swift-collections)
- [swift-nio](https://github.com/apple/swift-nio)
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
diff --git a/crowdin.yml b/crowdin.yml
index 4b5310c73..3612e371e 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -27,6 +27,11 @@ files: [
"translation" : "/Localization/StringsConvertor/input/%locale_with_underscore%/%original_file_name%",
"update_option" : "update_as_unapproved",
},
+ {
+ "source" : "/Localization/Localizable.stringsdict",
+ "translation" : "/Localization/StringsConvertor/input/%locale_with_underscore%/%original_file_name%",
+ "update_option" : "update_as_unapproved",
+ },
{
"source" : "/MastodonIntent/en.lproj/Intents.strings",
"translation" : "/Localization/StringsConvertor/Intents/input/%locale_with_underscore%/%original_file_name%",