Merge remote-tracking branch 'upstream/develop' into compose-a11y

This commit is contained in:
Jed Fox 2022-11-08 13:42:03 -05:00
commit eef012678b
No known key found for this signature in database
GPG Key ID: 0B61D18EA54B47E1
31 changed files with 565 additions and 101 deletions

View File

@ -20,7 +20,6 @@ ARTIFACT_PATH=${RESULT_PATH:-${BUILD_DIR}/Artifacts}
RESULT_BUNDLE_PATH="${ARTIFACT_PATH}/${SCHEME}.xcresult"
ARCHIVE_PATH=${ARCHIVE_PATH:-${BUILD_DIR}/Archives/${SCHEME}.xcarchive}
DERIVED_DATA_PATH=${DERIVED_DATA_PATH:-${BUILD_DIR}/DerivedData}
CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0}
EXPORT_OPTIONS_FILE=".github/support/ExportOptions.plist"
WORK_DIR=$(pwd)
@ -31,7 +30,13 @@ rm -rf "${RESULT_BUNDLE_PATH}"
rm -rf "${API_PRIVATE_KEYS_PATH}"
mkdir -p "${API_PRIVATE_KEYS_PATH}"
echo -n "${ENV_API_PRIVATE_KEY}" | base64 --decode > "${API_KEY_FILE}"
echo -n "${ENV_API_PRIVATE_KEY_BASE64}" | base64 --decode > "${API_KEY_FILE}"
BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number $ENV_APP_ID --issuer-id $ENV_ISSUER_ID --key-id $ENV_API_KEY_ID --private-key @file:$API_KEY_FILE)
BUILD_NUMBER=$((BUILD_NUMBER+1))
CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0}
agvtool new-version -all $CURRENT_PROJECT_VERSION
xcrun xcodebuild clean \
-workspace "${WORKSPACE}" \

View File

@ -4,5 +4,7 @@
<dict>
<key>method</key>
<string>app-store</string>
<key>manageAppVersionAndBuildNumber</key>
<true/>
</dict>
</plist>

View File

@ -20,6 +20,14 @@ jobs:
NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }}
run: exec ./.github/scripts/setup.sh
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: |
pip3 install codemagic-cli-tools
- run: |
codemagic-cli-tools --version || true
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1 # https://github.com/Apple-Actions/import-codesign-certs
with:
@ -37,9 +45,11 @@ jobs:
- name: build
env:
ENV_APP_ID: ${{ secrets.APP_ID }}
ENV_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
ENV_API_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
ENV_API_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }}
ENV_API_PRIVATE_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
ENV_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }}
run: exec ./.github/scripts/build-release.sh
- name: Upload TestFlight Build

View File

@ -1,30 +1,30 @@
# Contributing
- File the issue for bug report and feature request
- File an issue to report a bug or feature request
- Translate the project in our [Crowdin](https://crowdin.com/project/mastodon-for-ios) project
- Make the Pull Request to contribute
## Bug Report
File the issue about the bug. Make sure you are installing the latest version app from TestFlight or App Store.
File an issue about the bug or feature request. Make sure you are installing the latest version of the app from TestFlight or App Store.
## Translation
[![Crowdin](https://badges.crowdin.net/mastodon-for-ios/localized.svg)](https://crowdin.com/project/mastodon-for-ios)
The translation will update regularly. Please request language if not listed via issue.
The translation will update regularly. Please request the language if it is not listed via an issue.
## Pull Request
You can make a pull request directly with small block code changes for bugfix or feature implementations. Before making a pull request with hundred lines of changes to this repository, please first discuss the change you wish to make via issue.
You can create a pull request directly with small block code changes for bugfix or feature implementations. Before making a pull request with hundred lines of changes to this repository, please first discuss the change you wish to make via an issue.
Also, there are lots of existing feature request issues that could be a good-first-issue discussing place.
Follow the git-flow pattern to make your pull request.
1. Ensure you are checkout on the `develop` branch.
2. Write your codes and test them on **iPad and iPhone**.
3. Merge the `develop` into your branch then make a Pull Request. Please merge the branch and resolve any conflicts when the `develop` updates. **Do not force push your codes.**
4. Make sure the permission for your folk is open to the reviewer. Code style fix, conflict resolution, and other changes may be committed by the reviewer directly.
1. Ensure you have started a new branch based on the `develop` branch.
2. Write your changes and test them on **iPad and iPhone**.
3. Merge the `develop` branch into your branch then make a Pull Request. Please merge the branch and resolve any conflicts if `develop` updates. **Do not force push your commits.**
4. Make sure the permission for your fork is open to the reviewer. Code style fix, conflict resolution, and other changes may be committed by the reviewer directly.
5. Request a code review and wait for approval. The PR will be merged when it is approved.
## Documentation
The documents for this app is list under the [Documentation](../Documentation/) folder. We are also welcome contributions for documentation.
The documentation for this app is listed under the [Documentation](../Documentation/) folder. We are also welcoming contributions for documentation.

View File

@ -100,6 +100,7 @@ GEM
PLATFORMS
arm64-darwin-21
x86_64-darwin-21
DEPENDENCIES
arkana

View File

@ -180,7 +180,9 @@
"unmute": "Unmute",
"unmute_user": "Unmute %s",
"muted": "Muted",
"edit_info": "Edit Info"
"edit_info": "Edit Info",
"show_reblogs": "Show Reblogs",
"hide_reblogs": "Hide Reblogs"
},
"timeline": {
"filtered": "Filtered",
@ -457,6 +459,14 @@
"confirm_unblock_user": {
"title": "Unblock Account",
"message": "Confirm to unblock %s"
},
"confirm_show_reblogs": {
"title": "Show Reblogs",
"message": "Confirm to show reblogs"
},
"confirm_hide_reblogs": {
"title": "Hide Reblogs",
"message": "Confirm to hide reblogs"
}
},
"accessibility": {

View File

@ -3771,6 +3771,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -3834,6 +3835,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -3866,7 +3868,6 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@ -3874,6 +3875,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.4.7;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3895,7 +3897,6 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@ -3903,6 +3904,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.4.7;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4030,6 +4032,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -4067,7 +4070,6 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@ -4075,6 +4077,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.4.7;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4133,7 +4136,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4155,7 +4157,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4178,7 +4179,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4201,7 +4201,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4224,7 +4223,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4247,7 +4245,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4270,7 +4267,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4323,6 +4319,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -4356,7 +4353,6 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@ -4364,6 +4360,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.4.7;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4421,7 +4418,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4443,7 +4439,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4466,7 +4461,6 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4490,7 +4484,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -4513,7 +4506,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (

View File

@ -189,15 +189,6 @@
"version" : "0.1.4"
}
},
{
"identity" : "swiftyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version" : "5.0.1"
}
},
{
"identity" : "tabbarpager",
"kind" : "remoteSourceControl",

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.7</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@ -30,7 +30,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>147</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>

View File

@ -132,3 +132,14 @@ extension DataSourceFacade {
}
} // end func
}
extension DataSourceFacade {
static func responseToShowHideReblogAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
_ = try await dependency.context.apiService.toggleShowReblogs(
for: user,
authenticationBox: dependency.authContext.mastodonAuthenticationBox)
}
}

View File

@ -205,6 +205,45 @@ extension DataSourceFacade {
menuContext: MenuContext
) async throws {
switch action {
case .hideReblogs(let actionContext):
let title = actionContext.showReblogs ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.title
let message = actionContext.showReblogs ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.message : L10n.Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.message
let alertController = UIAlertController(
title: title,
message: message,
preferredStyle: .alert
)
let actionTitle = actionContext.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs
let showHideReblogsAction = UIAlertAction(
title: actionTitle,
style: .destructive
) { [weak dependency] _ in
guard let dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
try await DataSourceFacade.responseToShowHideReblogAction(
dependency: dependency,
user: user
)
}
}
alertController.addAction(showHideReblogsAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true)
case .muteUser(let actionContext):
let alertController = UIAlertController(
title: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.title,
@ -230,9 +269,9 @@ extension DataSourceFacade {
} // end Task
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true, completion: nil)
dependency.present(alertController, animated: true)
case .blockUser(let actionContext):
let alertController = UIAlertController(
title: actionContext.isBlocking ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.title,
@ -258,9 +297,9 @@ extension DataSourceFacade {
} // end Task
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true, completion: nil)
dependency.present(alertController, animated: true)
case .reportUser:
Task {
guard let user = menuContext.author else { return }
@ -349,9 +388,9 @@ extension DataSourceFacade {
} // end Task
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true, completion: nil)
dependency.present(alertController, animated: true)
}
} // end func
@ -371,3 +410,4 @@ extension DataSourceFacade {
}
}

View File

@ -14,6 +14,12 @@ protocol ScrollViewContainer: UIViewController {
extension ScrollViewContainer {
func scrollToTop(animated: Bool) {
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
scrollView.scrollToTop(animated: animated)
}
}
extension UIScrollView {
func scrollToTop(animated: Bool) {
scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
}
}

View File

@ -131,6 +131,13 @@ extension DiscoveryViewController: ScrollViewContainer {
var scrollView: UIScrollView {
return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView()
}
func scrollToTop(animated: Bool) {
if scrollView.contentOffset.y <= 0 {
scrollToPage(.first, animated: animated)
} else {
scrollView.scrollToTop(animated: animated)
}
}
}
extension DiscoveryViewController {

View File

@ -376,13 +376,22 @@ extension ProfileViewController {
}
let name = user.displayNameWithFallback
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
var menuActions: [MastodonMenu.Action] = [
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
.reportUser(.init(name: name)),
.shareUser(.init(name: name)),
]
if let me = self.viewModel?.me, me.following.contains(user) {
let showReblogs = me.showingReblogsBy.contains(user)
let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs)
menuActions.insert(.hideReblogs(context), at: 1)
}
let menu = MastodonMenu.setupMenu(
actions: [
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
.reportUser(.init(name: name)),
.shareUser(.init(name: name)),
],
actions: menuActions,
delegate: self
)
return menu
@ -397,7 +406,9 @@ extension ProfileViewController {
}
} receiveValue: { [weak self] menu in
guard let self = self else { return }
self.moreMenuBarButtonItem.menu = menu
OperationQueue.main.addOperation {
self.moreMenuBarButtonItem.menu = menu
}
}
.store(in: &disposeBag)
}
@ -740,7 +751,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
alertController.addAction(okAction)
self.coordinator.present(
_ = self.coordinator.present(
scene: .alertController(alertController: alertController),
from: nil,
transition: .alertController(animated: true, completion: nil)
@ -763,11 +774,11 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
break
case .follow, .request, .pending, .following:
guard let user = viewModel.user else { return }
let reocrd = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
Task {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: reocrd
user: record
)
}
case .muting:
@ -816,10 +827,8 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
case .blocked:
case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating:
break
default:
assertionFailure()
}
}

View File

@ -88,6 +88,7 @@ extension SidebarViewModel {
cell.setNeedsUpdateConfiguration()
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.title
cell.accessibilityTraits.insert(.button)
self.$currentTab
.receive(on: DispatchQueue.main)
@ -141,6 +142,7 @@ extension SidebarViewModel {
cell.setNeedsUpdateConfiguration()
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.title
cell.accessibilityTraits.insert(.button)
}
// header

View File

@ -190,6 +190,16 @@ extension SearchViewController: UISearchControllerDelegate {
}
}
// MARK: - ScrollViewContainer
extension SearchViewController: ScrollViewContainer {
var scrollView: UIScrollView {
discoveryViewController?.scrollView ?? UIScrollView()
}
func scrollToTop(animated: Bool) {
discoveryViewController?.scrollToTop(animated: animated)
}
}
// MARK: - UICollectionViewDelegate
//extension SearchViewController: UICollectionViewDelegate {
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.7</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>147</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>

View File

@ -40,7 +40,6 @@ let package = Package(
.package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"),
.package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.0.0"),
.package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
.package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"),
.package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "2.2.5"),
.package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"),
@ -103,7 +102,6 @@ let package = Package(
.target(
name: "MastodonSDK",
dependencies: [
.product(name: "SwiftyJSON", package: "SwiftyJSON"),
.product(name: "NIOHTTP1", package: "swift-nio"),
]
),

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>CoreData 3.xcdatamodel</string>
<string>CoreData 4.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G115" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="vapidKey" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
</entity>
<entity name="DomainBlock" representedClassName="CoreDataStack.DomainBlock" syncable="YES">
<attribute name="blockedDomain" attributeType="String"/>
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="userID"/>
<constraint value="domain"/>
<constraint value="blockedDomain"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Emoji" representedClassName="CoreDataStack.Emoji" syncable="YES">
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="shortcode" attributeType="String"/>
<attribute name="staticURL" attributeType="String"/>
<attribute name="url" attributeType="String"/>
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
</entity>
<entity name="Feed" representedClassName="CoreDataStack.Feed" syncable="YES">
<attribute name="acctRaw" optional="YES" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasMore" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isLoadingMore" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="kindRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
</entity>
<entity name="Instance" representedClassName="CoreDataStack.Instance" syncable="YES">
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userAccessToken" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
</entity>
<entity name="MastodonUser" representedClassName="CoreDataStack.MastodonUser" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="String"/>
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="displayName" attributeType="String"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="fields" optional="YES" attributeType="Binary"/>
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" attributeType="String"/>
<attribute name="headerStatic" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="blocking" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
<relationship name="blockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blocking" inverseEntity="MastodonUser"/>
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="bookmarkedBy" inverseEntity="Status"/>
<relationship name="domainBlocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlockingBy" inverseEntity="MastodonUser"/>
<relationship name="domainBlockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
<relationship name="endorsed" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsedBy" inverseEntity="MastodonUser"/>
<relationship name="endorsedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsed" inverseEntity="MastodonUser"/>
<relationship name="favourite" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
<relationship name="following" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
<relationship name="followingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
<relationship name="followRequested" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
<relationship name="followRequestedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequested" inverseEntity="MastodonUser"/>
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
<relationship name="muted" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
<relationship name="muting" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
<relationship name="mutingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Nullify" destinationEntity="Notification" inverseName="account" inverseEntity="Notification"/>
<relationship name="pinnedStatus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="pinnedBy" inverseEntity="Status"/>
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
<relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/>
<relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
<relationship name="votePollOptions" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
<relationship name="votePolls" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
</entity>
<entity name="Notification" representedClassName="CoreDataStack.Notification" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="followRequestState" optional="YES" attributeType="Binary"/>
<attribute name="id" attributeType="String"/>
<attribute name="transientFollowRequestState" optional="YES" transient="YES" attributeType="Binary"/>
<attribute name="typeRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
</entity>
<entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<attribute name="isVoting" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="options" toMany="YES" deletionRule="Cascade" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="poll" inverseEntity="Status"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
</entity>
<entity name="PollOption" representedClassName="CoreDataStack.PollOption" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isSelected" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
</entity>
<entity name="PrivateNote" representedClassName="CoreDataStack.PrivateNote" syncable="YES">
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
</entity>
<entity name="SearchHistory" representedClassName="CoreDataStack.SearchHistory" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String" defaultValueString=""/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
</entity>
<entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredStaticEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredUsingDefaultBrowser" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
</entity>
<entity name="Status" representedClassName="CoreDataStack.Status" syncable="YES">
<attribute name="attachments" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="isSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="mentions" optional="YES" attributeType="Binary"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" optional="YES" attributeType="Integer 64" usesScalarValueType="NO"/>
<attribute name="revealedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="visibilityRaw" optional="YES" attributeType="String" elementID="visibility"/>
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
<relationship name="bookmarkedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
<relationship name="favouritedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="status" inverseEntity="Feed"/>
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Cascade" destinationEntity="Notification" inverseName="status" inverseEntity="Notification"/>
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedStatus" inverseEntity="MastodonUser"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="status" inverseEntity="Poll"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
<relationship name="reblogFrom" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
</entity>
<entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="endpoint" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="policyRaw" attributeType="String"/>
<attribute name="serverKey" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userToken" optional="YES" attributeType="String"/>
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
</entity>
<entity name="SubscriptionAlerts" representedClassName="CoreDataStack.SubscriptionAlerts" syncable="YES">
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
</entity>
<entity name="Tag" representedClassName="CoreDataStack.Tag" syncable="YES">
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="histories" optional="YES" attributeType="Binary"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" attributeType="String"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
</entity>
</model>

View File

@ -89,7 +89,8 @@ final public class MastodonUser: NSManagedObject {
@NSManaged public private(set) var endorsedBy: Set<MastodonUser>
@NSManaged public private(set) var domainBlocking: Set<MastodonUser>
@NSManaged public private(set) var domainBlockingBy: Set<MastodonUser>
@NSManaged public private(set) var showingReblogs: Set<MastodonUser>
@NSManaged public private(set) var showingReblogsBy: Set<MastodonUser>
}
extension MastodonUser {
@ -521,6 +522,7 @@ extension MastodonUser: AutoUpdatableObject {
}
}
}
public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) {
if isDomainBlocking {
if !self.domainBlockingBy.contains(mastodonUser) {
@ -533,4 +535,15 @@ extension MastodonUser: AutoUpdatableObject {
}
}
public func update(isShowingReblogs: Bool, by mastodonUser: MastodonUser) {
if isShowingReblogs {
if !self.showingReblogsBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).add(mastodonUser)
}
} else {
if self.showingReblogsBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).remove(mastodonUser)
}
}
}
}

View File

@ -157,5 +157,6 @@ extension Persistence.MastodonUser {
user.update(isBlocking: relationship.blocking, by: me)
relationship.domainBlocking.flatMap { user.update(isDomainBlocking: $0, by: me) }
relationship.blockedBy.flatMap { me.update(isBlocking: $0, by: user) }
relationship.showingReblogs.flatMap { me.update(isShowingReblogs: $0, by: user) }
}
}

View File

@ -13,7 +13,7 @@ import CommonOSLog
import MastodonSDK
extension APIService {
private struct MastodonFollowContext {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
@ -121,5 +121,56 @@ extension APIService {
let response = try result.get()
return response
}
public func toggleShowReblogs(
for user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
else { throw APIError.implicit(.badRequest) }
let me = authentication.user
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
let oldShowReblogs = me.showingReblogsBy.contains(user)
let newShowReblogs = (oldShowReblogs == false)
do {
let response = try await Mastodon.API.Account.follow(
session: session,
domain: authenticationBox.domain,
accountID: user.id,
followQueryType: .follow(query: .init(reblogs: newShowReblogs)),
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
} catch {
result = .failure(error)
}
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
switch result {
case .success(let response):
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isShowingReblogs: oldShowReblogs, by: me)
}
}
return try result.get()
}
}

View File

@ -198,6 +198,8 @@ public enum L10n {
public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Follow")
/// Following
public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Following")
/// Hide Reblogs
public static let hideReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.HideReblogs")
/// Mute
public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Mute")
/// Muted
@ -210,6 +212,8 @@ public enum L10n {
public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Pending")
/// Request
public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Request")
/// Show Reblogs
public static let showReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.ShowReblogs")
/// Unblock
public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock")
/// Unblock %@
@ -709,6 +713,12 @@ public enum L10n {
/// Block Account
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title")
}
public enum ConfirmHideReblogs {
/// Confirm to hide reblogs
public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message")
/// Hide reblogs
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title")
}
public enum ConfirmMuteUser {
/// Confirm to mute %@
public static func message(_ p1: Any) -> String {
@ -717,6 +727,12 @@ public enum L10n {
/// Mute Account
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title")
}
public enum ConfirmShowReblogs {
/// Confirm to show reblogs
public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message")
/// Show Reblogs
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title")
}
public enum ConfirmUnblockUser {
/// Confirm to unblock %@
public static func message(_ p1: Any) -> String {

View File

@ -77,6 +77,9 @@ Please check your internet connection.";
"Common.Controls.Friendship.UnblockUser" = "Unblock %@";
"Common.Controls.Friendship.Unmute" = "Unmute";
"Common.Controls.Friendship.UnmuteUser" = "Unmute %@";
"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings";
"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites";
@ -262,6 +265,10 @@ uploaded to Mastodon.";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Unblock Account";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account";
"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide reblogs";
"Scene.Profile.SegmentedControl.About" = "About";
"Scene.Profile.SegmentedControl.Media" = "Media";
"Scene.Profile.SegmentedControl.Posts" = "Posts";

View File

@ -45,11 +45,23 @@ extension MastodonMenu {
case reportUser(ReportUserActionContext)
case shareUser(ShareUserActionContext)
case bookmarkStatus(BookmarkStatusActionContext)
case hideReblogs(HideReblogsActionContext)
case shareStatus
case deleteStatus
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
switch self {
case .hideReblogs(let context):
let title = context.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs
let reblogAction = BuiltAction(
title: title,
image: UIImage(systemName: "arrow.2.squarepath")
) { [weak delegate] in
guard let delegate = delegate else { return }
delegate.menuAction(self)
}
return reblogAction
case .muteUser(let context):
let muteAction = BuiltAction(
title: context.isMuting ? L10n.Common.Controls.Friendship.unmuteUser(context.name) : L10n.Common.Controls.Friendship.muteUser(context.name),
@ -205,5 +217,12 @@ extension MastodonMenu {
self.name = name
}
}
public struct HideReblogsActionContext {
public let showReblogs: Bool
public init(showReblogs: Bool) {
self.showReblogs = showReblogs
}
}
}

View File

@ -12,6 +12,7 @@ import MastodonLocalization
import CoreDataStack
public enum RelationshipAction: Int, CaseIterable {
case showReblogs
case isMyself
case followingBy
case blockingBy
@ -27,7 +28,7 @@ public enum RelationshipAction: Int, CaseIterable {
case edit
case editing
case updating
public var option: RelationshipActionOptionSet {
return RelationshipActionOptionSet(rawValue: 1 << rawValue)
}
@ -57,7 +58,7 @@ public struct RelationshipActionOptionSet: OptionSet {
public static let edit = RelationshipAction.edit.option
public static let editing = RelationshipAction.editing.option
public static let updating = RelationshipAction.updating.option
public static let showReblogs = RelationshipAction.showReblogs.option
public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating]
public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
@ -75,24 +76,24 @@ public struct RelationshipActionOptionSet: OptionSet {
return " "
}
switch highPriorityAction {
case .isMyself: return ""
case .followingBy: return " "
case .blockingBy: return " "
case .none: return " "
case .follow: return L10n.Common.Controls.Friendship.follow
case .request: return L10n.Common.Controls.Friendship.request
case .pending: return L10n.Common.Controls.Friendship.pending
case .following: return L10n.Common.Controls.Friendship.following
case .muting: return L10n.Common.Controls.Friendship.muted
case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user (deprecated)
case .blocking: return L10n.Common.Controls.Friendship.blocked
case .suspended: return L10n.Common.Controls.Friendship.follow
case .edit: return L10n.Common.Controls.Friendship.editInfo
case .editing: return L10n.Common.Controls.Actions.done
case .updating: return " "
case .isMyself: return ""
case .followingBy: return " "
case .blockingBy: return " "
case .none: return " "
case .follow: return L10n.Common.Controls.Friendship.follow
case .request: return L10n.Common.Controls.Friendship.request
case .pending: return L10n.Common.Controls.Friendship.pending
case .following: return L10n.Common.Controls.Friendship.following
case .muting: return L10n.Common.Controls.Friendship.muted
case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user (deprecated)
case .blocking: return L10n.Common.Controls.Friendship.blocked
case .suspended: return L10n.Common.Controls.Friendship.follow
case .edit: return L10n.Common.Controls.Friendship.editInfo
case .editing: return L10n.Common.Controls.Actions.done
case .updating: return " "
case .showReblogs: return " "
}
}
}
public final class RelationshipViewModel {
@ -114,6 +115,7 @@ public final class RelationshipViewModel {
@Published public var isFollowing = false
@Published public var isFollowingBy = false
@Published public var isMuting = false
@Published public var showReblogs = false
@Published public var isBlocking = false
@Published public var isBlockingBy = false
@Published public var isSuspended = false
@ -184,6 +186,7 @@ extension RelationshipViewModel {
self.isBlockingBy = optionSet.contains(.blockingBy)
self.isBlocking = optionSet.contains(.blocking)
self.isSuspended = optionSet.contains(.suspended)
self.showReblogs = optionSet.contains(.showReblogs)
self.optionSet = optionSet
}
@ -196,6 +199,7 @@ extension RelationshipViewModel {
isBlockingBy = false
isBlocking = false
optionSet = nil
showReblogs = false
}
}
@ -214,7 +218,8 @@ extension RelationshipViewModel {
let isMuting = user.mutingBy.contains(me)
let isBlockingBy = me.blockingBy.contains(user)
let isBlocking = user.blockingBy.contains(me)
let isShowingReblogs = me.showingReblogsBy.contains(user)
var optionSet: RelationshipActionOptionSet = [.follow]
if isMyself {
@ -252,7 +257,11 @@ extension RelationshipViewModel {
if user.suspended {
optionSet.insert(.suspended)
}
if isShowingReblogs {
optionSet.insert(.showReblogs)
}
return optionSet
}
}

View File

@ -15,8 +15,8 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.7</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>147</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -15,8 +15,8 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.7</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>147</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.7</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>147</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -17,23 +17,23 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.7</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>147</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>4</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>