Merge tag '0.6.0' into develop

no message
This commit is contained in:
CMK 2021-06-15 11:57:58 +08:00
commit 389d0971cd
39 changed files with 305 additions and 480 deletions

View File

@ -8,5 +8,5 @@
import Foundation import Foundation
public enum AppName { public enum AppName {
public static let groupID = "group.org.joinmastodon.mastodon-temp" public static let groupID = "group.org.joinmastodon.app"
} }

View File

@ -13,7 +13,7 @@ import Keys
public final class AppSecret { public final class AppSecret {
public static let keychain = Keychain(service: "org.joinmastodon.Mastodon.keychain", accessGroup: AppName.groupID) public static let keychain = Keychain(service: "org.joinmastodon.app.keychain", accessGroup: AppName.groupID)
static let notificationPrivateKeyName = "notification-private-key-base64" static let notificationPrivateKeyName = "notification-private-key-base64"
static let notificationAuthName = "notification-auth-base64" static let notificationAuthName = "notification-auth-base64"

View File

@ -181,11 +181,9 @@
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; };
DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; };
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; };
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; };
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; };
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */; }; DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */; };
@ -753,10 +751,8 @@
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = "<group>"; }; BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = "<group>"; };
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; }; D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = "<group>"; };
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; }; DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = "<group>"; };
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCollectionView.swift; sourceTree = "<group>"; }; DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCollectionView.swift; sourceTree = "<group>"; };
@ -1618,7 +1614,6 @@
DB68A03825E900CC00CFDF14 /* Share */, DB68A03825E900CC00CFDF14 /* Share */,
0FAA0FDD25E0B5700017CCDE /* Welcome */, 0FAA0FDD25E0B5700017CCDE /* Welcome */,
0FAA102525E1125D0017CCDE /* PickServer */, 0FAA102525E1125D0017CCDE /* PickServer */,
DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */,
DBE0821A25CD382900FD6BBD /* Register */, DBE0821A25CD382900FD6BBD /* Register */,
DB72602125E36A2500235243 /* ServerRules */, DB72602125E36A2500235243 /* ServerRules */,
2D364F7025E66D5B00204FDC /* ResendEmail */, 2D364F7025E66D5B00204FDC /* ResendEmail */,
@ -1627,16 +1622,6 @@
path = Onboarding; path = Onboarding;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */ = {
isa = PBXGroup;
children = (
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */,
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */,
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */,
);
path = PinBasedAuthentication;
sourceTree = "<group>";
};
DB084B5125CBC56300F898ED /* CoreDataStack */ = { DB084B5125CBC56300F898ED /* CoreDataStack */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1921,6 +1906,7 @@
children = ( children = (
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */,
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */, DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */,
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */,
); );
path = Share; path = Share;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3043,6 +3029,7 @@
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */, 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */,
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */, 5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */,
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */, DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */,
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
@ -3149,7 +3136,6 @@
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */, DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
@ -3220,7 +3206,6 @@
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */,
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */, DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */,
@ -3280,7 +3265,6 @@
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */, DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */, DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */,
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */, DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
@ -3630,16 +3614,16 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.0; MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -3657,16 +3641,16 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.0; MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -3680,7 +3664,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonTests/Info.plist; INFOPLIST_FILE = MastodonTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -3701,7 +3685,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonTests/Info.plist; INFOPLIST_FILE = MastodonTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -3721,7 +3705,7 @@
baseConfigurationReference = 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */; baseConfigurationReference = 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonUITests/Info.plist; INFOPLIST_FILE = MastodonUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -3741,7 +3725,7 @@
baseConfigurationReference = BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */; baseConfigurationReference = BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonUITests/Info.plist; INFOPLIST_FILE = MastodonUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -3765,7 +3749,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -3776,7 +3760,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.AppShared; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -3796,7 +3780,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -3807,7 +3791,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.AppShared; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -3824,7 +3808,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -3853,7 +3837,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -3879,7 +3863,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = CoreDataStackTests/Info.plist; INFOPLIST_FILE = CoreDataStackTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -3899,7 +3883,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = CoreDataStackTests/Info.plist; INFOPLIST_FILE = CoreDataStackTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -3920,16 +3904,16 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 0.5.0; MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -3943,16 +3927,16 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 0.5.0; MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -7,7 +7,7 @@
<key>AppShared.xcscheme_^#shared#^_</key> <key>AppShared.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>18</integer> <integer>14</integer>
</dict> </dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -27,12 +27,12 @@
<key>Mastodon.xcscheme_^#shared#^_</key> <key>Mastodon.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>1</integer> <integer>13</integer>
</dict> </dict>
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>16</integer> <integer>12</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -18,7 +18,7 @@ final class SafariActivity: UIActivity {
} }
override var activityType: UIActivity.ActivityType? { override var activityType: UIActivity.ActivityType? {
return UIActivity.ActivityType("org.joinmastodon.Mastodon.safari-activity") return UIActivity.ActivityType("org.joinmastodon.app.safari-activity")
} }
override var activityTitle: String? { override var activityTitle: String? {

View File

@ -42,7 +42,6 @@ extension SceneCoordinator {
// onboarding // onboarding
case welcome case welcome
case mastodonPickServer(viewMode: MastodonPickServerViewModel) case mastodonPickServer(viewMode: MastodonPickServerViewModel)
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
case mastodonRegister(viewModel: MastodonRegisterViewModel) case mastodonRegister(viewModel: MastodonRegisterViewModel)
case mastodonServerRules(viewModel: MastodonServerRulesViewModel) case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
@ -78,6 +77,7 @@ extension SceneCoordinator {
case safari(url: URL) case safari(url: URL)
case alertController(alertController: UIAlertController) case alertController(alertController: UIAlertController)
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
#if DEBUG #if DEBUG
case publicTimeline case publicTimeline
#endif #endif
@ -86,7 +86,6 @@ extension SceneCoordinator {
switch self { switch self {
case .welcome, case .welcome,
.mastodonPickServer, .mastodonPickServer,
.mastodonPinBasedAuthentication,
.mastodonRegister, .mastodonRegister,
.mastodonServerRules, .mastodonServerRules,
.mastodonConfirmEmail, .mastodonConfirmEmail,
@ -217,10 +216,6 @@ private extension SceneCoordinator {
let _viewController = MastodonPickServerViewController() let _viewController = MastodonPickServerViewController()
_viewController.viewModel = viewModel _viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .mastodonPinBasedAuthentication(let viewModel):
let _viewController = MastodonPinBasedAuthenticationViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonRegister(let viewModel): case .mastodonRegister(let viewModel):
let _viewController = MastodonRegisterViewController() let _viewController = MastodonRegisterViewController()
_viewController.viewModel = viewModel _viewController.viewModel = viewModel

View File

@ -141,8 +141,8 @@ extension ComposeStatusSection {
attribute.contentWarningContent.value = text attribute.contentWarningContent.value = text
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag) ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag)
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
return cell return cell
case .attachment(let attachmentService): case .attachment(let attachmentService):
@ -228,7 +228,7 @@ extension ComposeStatusSection {
.assign(to: \.value, on: attribute.option) .assign(to: \.value, on: attribute.option)
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
return cell return cell
case .pollOptionAppendEntry: case .pollOptionAppendEntry:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
@ -295,7 +295,7 @@ protocol CustomEmojiReplaceableTextInput: AnyObject {
var isFirstResponder: Bool { get } var isFirstResponder: Bool { get }
} }
class CustomEmojiReplacableTextInputReference { class CustomEmojiReplaceableTextInputReference {
weak var value: CustomEmojiReplaceableTextInput? weak var value: CustomEmojiReplaceableTextInput?
init(value: CustomEmojiReplaceableTextInput? = nil) { init(value: CustomEmojiReplaceableTextInput? = nil) {
@ -320,7 +320,7 @@ extension ComposeStatusSection {
static func configureCustomEmojiPicker( static func configureCustomEmojiPicker(
viewModel: CustomEmojiPickerInputViewModel?, viewModel: CustomEmojiPickerInputViewModel?,
customEmojiReplacableTextInput: CustomEmojiReplaceableTextInput, customEmojiReplaceableTextInput: CustomEmojiReplaceableTextInput,
disposeBag: inout Set<AnyCancellable> disposeBag: inout Set<AnyCancellable>
) { ) {
guard let viewModel = viewModel else { return } guard let viewModel = viewModel else { return }
@ -328,9 +328,9 @@ extension ComposeStatusSection {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak viewModel] isCustomEmojiComposing in .sink { [weak viewModel] isCustomEmojiComposing in
guard let viewModel = viewModel else { return } guard let viewModel = viewModel else { return }
customEmojiReplacableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil customEmojiReplaceableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil
customEmojiReplacableTextInput.reloadInputViews() customEmojiReplaceableTextInput.reloadInputViews()
viewModel.append(customEmojiReplacableTextInput: customEmojiReplacableTextInput) viewModel.append(customEmojiReplaceableTextInput: customEmojiReplaceableTextInput)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }

View File

@ -6,7 +6,7 @@
<string>development</string> <string>development</string>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.org.joinmastodon.mastodon-temp</string> <string>group.org.joinmastodon.app</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -61,7 +61,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
var systemKeyboardHeight: CGFloat = .zero { var systemKeyboardHeight: CGFloat = .zero {
didSet { didSet {
// note: some system AutoLayout warning here // note: some system AutoLayout warning here
customEmojiPickerInputView.frame.size.height = systemKeyboardHeight != .zero ? systemKeyboardHeight : 300 let height = max(300, systemKeyboardHeight)
customEmojiPickerInputView.frame.size.height = height
} }
} }
@ -730,7 +731,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
guard let emoji = customEmojiViewModel.emoji(shortcode: name) else { continue } guard let emoji = customEmojiViewModel.emoji(shortcode: name) else { continue }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
// set emoji token invisiable (without upper bounce space) // set emoji token invisible (without upper bounce space)
var attributes = [NSAttributedString.Key: Any]() var attributes = [NSAttributedString.Key: Any]()
attributes[.font] = UIFont.systemFont(ofSize: 0.01) attributes[.font] = UIFont.systemFont(ofSize: 0.01)
attributedString.addAttributes(attributes, range: match.range) attributedString.addAttributes(attributes, range: match.range)
@ -812,15 +813,15 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
extension ComposeViewController: TextEditorViewChangeObserver { extension ComposeViewController: TextEditorViewChangeObserver {
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) { func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
guard var autoCompeletion = ComposeViewController.scanAutoCompleteInfo(textEditorView: textEditorView) else { guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textEditorView: textEditorView) else {
viewModel.autoCompleteInfo.value = nil viewModel.autoCompleteInfo.value = nil
return return
} }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompeletion.toHighlightEndString), String(autoCompeletion.toCursorString)) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
// get layout text bounding rect // get layout text bounding rect
var glyphRange = NSRange() var glyphRange = NSRange()
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange) textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
let textContainer = textEditorView.layoutManager.textContainers[0] let textContainer = textEditorView.layoutManager.textContainers[0]
let textBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) let textBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
@ -838,13 +839,13 @@ extension ComposeViewController: TextEditorViewChangeObserver {
viewModel.autoCompleteRetryLayoutTimes.value = 0 viewModel.autoCompleteRetryLayoutTimes.value = 0
// get symbol bounding rect // get symbol bounding rect
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.symbolRange, in: textEditorView.text), actualGlyphRange: &glyphRange) textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
let symbolBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) let symbolBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// set bounding rect and trigger layout // set bounding rect and trigger layout
autoCompeletion.textBoundingRect = textBoundingRect autoCompletion.textBoundingRect = textBoundingRect
autoCompeletion.symbolBoundingRect = symbolBoundingRect autoCompletion.symbolBoundingRect = symbolBoundingRect
viewModel.autoCompleteInfo.value = autoCompeletion viewModel.autoCompleteInfo.value = autoCompletion
} }
struct AutoCompleteInfo { struct AutoCompleteInfo {
@ -864,12 +865,14 @@ extension ComposeViewController: TextEditorViewChangeObserver {
private static func scanAutoCompleteInfo(textEditorView: TextEditorView) -> AutoCompleteInfo? { private static func scanAutoCompleteInfo(textEditorView: TextEditorView) -> AutoCompleteInfo? {
let text = textEditorView.text let text = textEditorView.text
let cursorLocation = textEditorView.selectedRange.location
let cursorIndex = text.index(text.startIndex, offsetBy: cursorLocation) guard textEditorView.selectedRange.location > 0, !text.isEmpty,
guard cursorLocation > 0, !text.isEmpty else { return nil } let selectedRange = Range(textEditorView.selectedRange, in: text) else {
return nil
let _highlighStartIndex: String.Index? = { }
var index = text.index(text.startIndex, offsetBy: cursorLocation - 1) let cursorIndex = selectedRange.upperBound
let _highlightStartIndex: String.Index? = {
var index = text.index(before: cursorIndex)
while index > text.startIndex { while index > text.startIndex {
let char = text[index] let char = text[index]
if char == "@" || char == "#" || char == ":" { if char == "@" || char == "#" || char == ":" {
@ -886,18 +889,18 @@ extension ComposeViewController: TextEditorViewChangeObserver {
} }
}() }()
guard let highlighStartIndex = _highlighStartIndex else { return nil } guard let highlightStartIndex = _highlightStartIndex else { return nil }
let scanRange = NSRange(highlighStartIndex..<text.endIndex, in: text) let scanRange = NSRange(highlightStartIndex..<text.endIndex, in: text)
guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil } guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
let matchRange = match.range(at: 0) guard let matchRange = Range(match.range(at: 0), in: text) else { return nil }
let matchStartIndex = text.index(text.startIndex, offsetBy: matchRange.location) let matchStartIndex = matchRange.lowerBound
let matchEndIndex = text.index(matchStartIndex, offsetBy: matchRange.length) let matchEndIndex = matchRange.upperBound
guard matchStartIndex == highlighStartIndex, matchEndIndex >= cursorIndex else { return nil } guard matchStartIndex == highlightStartIndex, matchEndIndex >= cursorIndex else { return nil }
let symbolRange = highlighStartIndex..<text.index(after: highlighStartIndex) let symbolRange = highlightStartIndex..<text.index(after: highlightStartIndex)
let symbolString = text[symbolRange] let symbolString = text[symbolRange]
let toCursorRange = highlighStartIndex..<cursorIndex let toCursorRange = highlightStartIndex..<cursorIndex
let toCursorString = text[toCursorRange] let toCursorString = text[toCursorRange]
let toHighlightEndRange = matchStartIndex..<matchEndIndex let toHighlightEndRange = matchStartIndex..<matchEndIndex
let toHighlightEndString = text[toHighlightEndRange] let toHighlightEndString = text[toHighlightEndRange]
@ -1015,7 +1018,7 @@ extension ComposeViewController: UICollectionViewDelegate {
let emoji = attribute.emoji let emoji = attribute.emoji
let textEditorView = self.textEditorView() let textEditorView = self.textEditorView()
// retrive active text input and insert emoji // retrieve active text input and insert emoji
// the leading and trailing space is REQUIRED to fix `UITextStorage` layout issue // the leading and trailing space is REQUIRED to fix `UITextStorage` layout issue
let reference = viewModel.customEmojiPickerInputViewModel.insertText(" :\(emoji.shortcode): ") let reference = viewModel.customEmojiPickerInputViewModel.insertText(" :\(emoji.shortcode): ")
@ -1028,6 +1031,9 @@ extension ComposeViewController: UICollectionViewDelegate {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.collectionViewLayout.invalidateLayout()
// make click sound
UIDevice.current.playInputClick()
} }
} }
} else { } else {
@ -1068,7 +1074,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate {
let service = MastodonAttachmentService( let service = MastodonAttachmentService(
context: context, context: context,
pickerResult: result, pickerResult: result,
initalAuthenticationBox: viewModel.activeAuthenticationBox.value initialAuthenticationBox: viewModel.activeAuthenticationBox.value
) )
return service return service
} }
@ -1087,7 +1093,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationC
let attachmentService = MastodonAttachmentService( let attachmentService = MastodonAttachmentService(
context: context, context: context,
image: image, image: image,
initalAuthenticationBox: viewModel.activeAuthenticationBox.value initialAuthenticationBox: viewModel.activeAuthenticationBox.value
) )
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
} }
@ -1106,7 +1112,7 @@ extension ComposeViewController: UIDocumentPickerDelegate {
let attachmentService = MastodonAttachmentService( let attachmentService = MastodonAttachmentService(
context: context, context: context,
documentURL: url, documentURL: url,
initalAuthenticationBox: viewModel.activeAuthenticationBox.value initialAuthenticationBox: viewModel.activeAuthenticationBox.value
) )
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
} }

View File

@ -218,6 +218,12 @@ extension ComposeToolbarView {
} }
private func updateToolbarButtonUserInterfaceStyle() { private func updateToolbarButtonUserInterfaceStyle() {
// reset emoji
let emojiButtonImage = Asset.Human.faceSmilingAdaptive.image
.af.imageScaled(to: CGSize(width: 20, height: 20))
.withRenderingMode(.alwaysTemplate)
emojiButton.setImage(emojiButtonImage, for: .normal)
switch traitCollection.userInterfaceStyle { switch traitCollection.userInterfaceStyle {
case .light: case .light:
mediaButton.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal) mediaButton.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)

View File

@ -84,3 +84,9 @@ extension CustomEmojiPickerInputView {
return layout return layout
} }
} }
extension CustomEmojiPickerInputView: UIInputViewAudioFeedback {
var enableInputClicksWhenVisible: Bool {
return true
}
}

View File

@ -12,7 +12,7 @@ final class CustomEmojiPickerInputViewModel {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
private var customEmojiReplacableTextInputReferences: [CustomEmojiReplacableTextInputReference] = [] private var customEmojiReplaceableTextInputReferences: [CustomEmojiReplaceableTextInputReference] = []
// input // input
weak var customEmojiPickerInputView: CustomEmojiPickerInputView? weak var customEmojiPickerInputView: CustomEmojiPickerInputView?
@ -25,27 +25,27 @@ final class CustomEmojiPickerInputViewModel {
extension CustomEmojiPickerInputViewModel { extension CustomEmojiPickerInputViewModel {
private func removeEmptyReferences() { private func removeEmptyReferences() {
customEmojiReplacableTextInputReferences.removeAll(where: { element in customEmojiReplaceableTextInputReferences.removeAll(where: { element in
element.value == nil element.value == nil
}) })
} }
func append(customEmojiReplacableTextInput textInput: CustomEmojiReplaceableTextInput) { func append(customEmojiReplaceableTextInput textInput: CustomEmojiReplaceableTextInput) {
removeEmptyReferences() removeEmptyReferences()
let isContains = customEmojiReplacableTextInputReferences.contains(where: { element in let isContains = customEmojiReplaceableTextInputReferences.contains(where: { element in
element.value === textInput element.value === textInput
}) })
guard !isContains else { guard !isContains else {
return return
} }
customEmojiReplacableTextInputReferences.append(CustomEmojiReplacableTextInputReference(value: textInput)) customEmojiReplaceableTextInputReferences.append(CustomEmojiReplaceableTextInputReference(value: textInput))
} }
func insertText(_ text: String) -> CustomEmojiReplacableTextInputReference? { func insertText(_ text: String) -> CustomEmojiReplaceableTextInputReference? {
removeEmptyReferences() removeEmptyReferences()
for reference in customEmojiReplacableTextInputReferences { for reference in customEmojiReplaceableTextInputReferences {
guard reference.value?.isFirstResponder == true else { continue } guard reference.value?.isFirstResponder == true else { continue }
reference.value?.insertText(text) reference.value?.insertText(text)
return reference return reference

View File

@ -9,6 +9,7 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import GameController import GameController
import AuthenticationServices
final class MastodonPickServerViewController: UIViewController, NeedsDependency { final class MastodonPickServerViewController: UIViewController, NeedsDependency {
@ -19,6 +20,11 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: MastodonPickServerViewModel! var viewModel: MastodonPickServerViewModel!
private(set) lazy var authenticationViewModel = AuthenticationViewModel(
context: context,
coordinator: coordinator,
isAuthenticationExist: false
)
private var expandServerDomainSet = Set<String>() private var expandServerDomainSet = Set<String>()
@ -50,6 +56,8 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
}() }()
var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint! var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint!
var mastodonAuthenticationController: MastodonAuthenticationController?
deinit { deinit {
tableViewObservation = nil tableViewObservation = nil
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
@ -182,23 +190,26 @@ extension MastodonPickServerViewController {
.assign(to: \.isEnabled, on: nextStepButton) .assign(to: \.isEnabled, on: nextStepButton)
.store(in: &disposeBag) .store(in: &disposeBag)
viewModel.error Publishers.Merge(
.compactMap { $0 } viewModel.error,
.receive(on: DispatchQueue.main) authenticationViewModel.error
.sink { [weak self] error in )
guard let self = self else { return } .compactMap { $0 }
let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert) .receive(on: DispatchQueue.main)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) .sink { [weak self] error in
alertController.addAction(okAction) guard let self = self else { return }
self.coordinator.present( let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert)
scene: .alertController(alertController: alertController), let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
from: nil, alertController.addAction(okAction)
transition: .alertController(animated: true, completion: nil) self.coordinator.present(
) scene: .alertController(alertController: alertController),
} from: nil,
.store(in: &disposeBag) transition: .alertController(animated: true, completion: nil)
)
}
.store(in: &disposeBag)
viewModel authenticationViewModel
.authenticated .authenticated
.flatMap { [weak self] (domain, user) -> AnyPublisher<Result<Bool, Error>, Never> in .flatMap { [weak self] (domain, user) -> AnyPublisher<Result<Bool, Error>, Never> in
guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() } guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() }
@ -217,7 +228,7 @@ extension MastodonPickServerViewController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
viewModel.isAuthenticating authenticationViewModel.isAuthenticating
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] isAuthenticating in .sink { [weak self] isAuthenticating in
guard let self = self else { return } guard let self = self else { return }
@ -273,11 +284,15 @@ extension MastodonPickServerViewController {
private func doSignIn() { private func doSignIn() {
guard let server = viewModel.selectedServer.value else { return } guard let server = viewModel.selectedServer.value else { return }
viewModel.isAuthenticating.send(true) authenticationViewModel.isAuthenticating.send(true)
context.apiService.createApplication(domain: server.domain) context.apiService.createApplication(domain: server.domain)
.tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
let application = response.value let application = response.value
guard let info = MastodonPickServerViewModel.AuthenticateInfo(domain: server.domain, application: application) else { guard let info = AuthenticationViewModel.AuthenticateInfo(
domain: server.domain,
application: application,
redirectURI: response.value.redirectURI ?? MastodonAuthenticationController.callbackURL
) else {
throw APIService.APIError.explicit(.badResponse) throw APIService.APIError.explicit(.badResponse)
} }
return info return info
@ -285,7 +300,7 @@ extension MastodonPickServerViewController {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] completion in .sink { [weak self] completion in
guard let self = self else { return } guard let self = self else { return }
self.viewModel.isAuthenticating.send(false) self.authenticationViewModel.isAuthenticating.send(false)
switch completion { switch completion {
case .failure(let error): case .failure(let error):
@ -296,15 +311,19 @@ extension MastodonPickServerViewController {
} }
} receiveValue: { [weak self] info in } receiveValue: { [weak self] info in
guard let self = self else { return } guard let self = self else { return }
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL) let authenticationController = MastodonAuthenticationController(
self.viewModel.authenticate( context: self.context,
info: info, authenticateURL: info.authorizeURL
pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher
) )
self.viewModel.mastodonPinBasedAuthenticationViewController = self.coordinator.present(
scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel), self.mastodonAuthenticationController = authenticationController
from: nil, authenticationController.authenticationSession?.prefersEphemeralWebBrowserSession = true
transition: .modal(animated: true, completion: nil) authenticationController.authenticationSession?.presentationContextProvider = self
authenticationController.authenticationSession?.start()
self.authenticationViewModel.authenticate(
info: info,
pinCodePublisher: authenticationController.pinCodePublisher
) )
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -313,7 +332,7 @@ extension MastodonPickServerViewController {
private func doSignUp() { private func doSignUp() {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let server = viewModel.selectedServer.value else { return } guard let server = viewModel.selectedServer.value else { return }
viewModel.isAuthenticating.send(true) authenticationViewModel.isAuthenticating.send(true)
context.apiService.instance(domain: server.domain) context.apiService.instance(domain: server.domain)
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in .compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
@ -328,7 +347,10 @@ extension MastodonPickServerViewController {
.switchToLatest() .switchToLatest()
.tryMap { response -> MastodonPickServerViewModel.SignUpResponseSecond in .tryMap { response -> MastodonPickServerViewModel.SignUpResponseSecond in
let application = response.application.value let application = response.application.value
guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: server.domain, application: application) else { guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(
domain: server.domain,
application: application
) else {
throw APIService.APIError.explicit(.badResponse) throw APIService.APIError.explicit(.badResponse)
} }
return MastodonPickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) return MastodonPickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo)
@ -340,7 +362,8 @@ extension MastodonPickServerViewController {
return self.context.apiService.applicationAccessToken( return self.context.apiService.applicationAccessToken(
domain: server.domain, domain: server.domain,
clientID: authenticateInfo.clientID, clientID: authenticateInfo.clientID,
clientSecret: authenticateInfo.clientSecret clientSecret: authenticateInfo.clientSecret,
redirectURI: authenticateInfo.redirectURI
) )
.map { MastodonPickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } .map { MastodonPickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -349,7 +372,7 @@ extension MastodonPickServerViewController {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] completion in .sink { [weak self] completion in
guard let self = self else { return } guard let self = self else { return }
self.viewModel.isAuthenticating.send(false) self.authenticationViewModel.isAuthenticating.send(false)
switch completion { switch completion {
case .failure(let error): case .failure(let error):
@ -519,3 +542,10 @@ extension MastodonPickServerViewController: PickServerCellDelegate {
// MARK: - OnboardingViewControllerAppearance // MARK: - OnboardingViewControllerAppearance
extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } extension MastodonPickServerViewController: OnboardingViewControllerAppearance { }
// MARK: - ASWebAuthenticationPresentationContextProviding
extension MastodonPickServerViewController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return view.window!
}
}

View File

@ -58,15 +58,11 @@ class MastodonPickServerViewModel: NSObject {
let filteredIndexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) let filteredIndexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
let selectedServer = CurrentValueSubject<Mastodon.Entity.Server?, Never>(nil) let selectedServer = CurrentValueSubject<Mastodon.Entity.Server?, Never>(nil)
let error = PassthroughSubject<Error, Never>() let error = CurrentValueSubject<Error?, Never>(nil)
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
let isLoadingIndexedServers = CurrentValueSubject<Bool, Never>(false) let isLoadingIndexedServers = CurrentValueSubject<Bool, Never>(false)
let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none) let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none)
var mastodonPinBasedAuthenticationViewController: UIViewController?
init(context: AppContext, mode: PickServerMode) { init(context: AppContext, mode: PickServerMode) {
self.context = context self.context = context
self.mode = mode self.mode = mode
@ -233,156 +229,6 @@ extension MastodonPickServerViewModel {
} }
} }
} }
// MARK: - SignIn methods & structs
extension MastodonPickServerViewModel {
enum AuthenticationError: Error, LocalizedError {
case badCredentials
case registrationClosed
var errorDescription: String? {
switch self {
case .badCredentials: return "Bad Credentials"
case .registrationClosed: return "Registration Closed"
}
}
var failureReason: String? {
switch self {
case .badCredentials: return "Credentials invalid."
case .registrationClosed: return "Server disallow registration."
}
}
var helpAnchor: String? {
switch self {
case .badCredentials: return "Please try again."
case .registrationClosed: return "Please try another domain."
}
}
}
struct AuthenticateInfo {
let domain: String
let clientID: String
let clientSecret: String
let authorizeURL: URL
init?(domain: String, application: Mastodon.Entity.Application) {
self.domain = domain
guard let clientID = application.clientID,
let clientSecret = application.clientSecret else { return nil }
self.clientID = clientID
self.clientSecret = clientSecret
self.authorizeURL = {
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
return url
}()
}
}
func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject<String, Never>) {
pinCodePublisher
.handleEvents(receiveOutput: { [weak self] _ in
guard let self = self else { return }
// self.isAuthenticating.value = true
self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil)
self.mastodonPinBasedAuthenticationViewController = nil
})
.compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in
guard let self = self else { return nil }
return self.context.apiService
.userAccessToken(
domain: info.domain,
clientID: info.clientID,
clientSecret: info.clientSecret,
code: code
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
let token = response.value
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in success. Token: %s", ((#file as NSString).lastPathComponent), #line, #function, token.accessToken)
return Self.verifyAndSaveAuthentication(
context: self.context,
info: info,
userToken: token
)
}
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
switch completion {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: swap user access token swap fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
// self.isAuthenticating.value = false
self.error.send(error)
case .finished:
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
let account = response.value
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s sign in success", ((#file as NSString).lastPathComponent), #line, #function, account.username)
self.authenticated.send((domain: info.domain, account: account))
}
.store(in: &self.disposeBag)
}
static func verifyAndSaveAuthentication(
context: AppContext,
info: AuthenticateInfo,
userToken: Mastodon.Entity.Token
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
let managedObjectContext = context.backgroundManagedObjectContext
return context.apiService.accountVerifyCredentials(
domain: info.domain,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
let account = response.value
let mastodonUserRequest = MastodonUser.sortedFetchRequest
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
mastodonUserRequest.fetchLimit = 1
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher()
}
let property = MastodonAuthentication.Property(
domain: info.domain,
userID: mastodonUser.id,
username: mastodonUser.username,
appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken,
clientID: info.clientID,
clientSecret: info.clientSecret
)
return managedObjectContext.performChanges {
_ = APIService.CoreData.createOrMergeMastodonAuthentication(
into: managedObjectContext,
for: mastodonUser,
in: info.domain,
property: property,
networkDate: response.networkDate
)
}
.tryMap { result in
switch result {
case .failure(let error): throw error
case .success: return response
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
// MARK: - SignUp methods & structs // MARK: - SignUp methods & structs
extension MastodonPickServerViewModel { extension MastodonPickServerViewModel {

View File

@ -52,6 +52,8 @@ class PickServerSearchCell: UITableViewCell {
textField.clearButtonMode = .whileEditing textField.clearButtonMode = .whileEditing
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.returnKeyType = .done
textField.keyboardType = .URL
return textField return textField
}() }()
@ -78,6 +80,7 @@ extension PickServerSearchCell {
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
searchTextField.delegate = self
contentView.addSubview(bgView) contentView.addSubview(bgView)
contentView.addSubview(textFieldBgView) contentView.addSubview(textFieldBgView)
@ -107,3 +110,12 @@ extension PickServerSearchCell {
delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text) delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text)
} }
} }
// MARK: - UITextFieldDelegate
extension PickServerSearchCell: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return false
}
}

View File

@ -1,73 +0,0 @@
//
// MastodonPinBasedAuthenticationViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/1/29.
//
import os.log
import UIKit
import Combine
import WebKit
final class MastodonPinBasedAuthenticationViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: MastodonPinBasedAuthenticationViewModel!
let webView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
let webView = WKWebView(frame: .zero, configuration: configuration)
return webView
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
// cleanup cookie
let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore
httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
httpCookieStore.delete(cookie, completionHandler: nil)
}
}
}
}
extension MastodonPinBasedAuthenticationViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Authentication"
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(MastodonPinBasedAuthenticationViewController.cancelBarButtonItemPressed(_:)))
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
let request = URLRequest(url: viewModel.authenticateURL)
webView.navigationDelegate = viewModel.navigationDelegate
webView.load(request)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: authenticate via: %s", ((#file as NSString).lastPathComponent), #line, #function, viewModel.authenticateURL.debugDescription)
}
}
extension MastodonPinBasedAuthenticationViewController {
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
}

View File

@ -1,40 +0,0 @@
//
// MastodonPinBasedAuthenticationViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/1/29.
//
import os.log
import Foundation
import Combine
import WebKit
final class MastodonPinBasedAuthenticationViewModel {
// input
let authenticateURL: URL
// output
let pinCodePublisher = PassthroughSubject<String, Never>()
private var navigationDelegateShim: MastodonPinBasedAuthenticationViewModelNavigationDelegateShim?
init(authenticateURL: URL) {
self.authenticateURL = authenticateURL
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension MastodonPinBasedAuthenticationViewModel {
var navigationDelegate: WKNavigationDelegate {
let navigationDelegateShim = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim(viewModel: self)
self.navigationDelegateShim = navigationDelegateShim
return navigationDelegateShim
}
}

View File

@ -1,41 +0,0 @@
//
// MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021/1/29.
//
import os.log
import Foundation
import WebKit
final class MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: NSObject {
weak var viewModel: MastodonPinBasedAuthenticationViewModel?
init(viewModel: MastodonPinBasedAuthenticationViewModel) {
self.viewModel = viewModel
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - WKNavigationDelegate
extension MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let url = webView.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
let code = codeQueryItem.value else {
return
}
viewModel?.pinCodePublisher.send(code)
}
}

View File

@ -31,9 +31,7 @@ final class AuthenticationViewModel {
let isIdle = CurrentValueSubject<Bool, Never>(true) let isIdle = CurrentValueSubject<Bool, Never>(true)
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
let error = CurrentValueSubject<Error?, Never>(nil) let error = CurrentValueSubject<Error?, Never>(nil)
var mastodonPinBasedAuthenticationViewController: UIViewController?
init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) { init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) {
self.context = context self.context = context
self.coordinator = coordinator self.coordinator = coordinator
@ -118,18 +116,24 @@ extension AuthenticationViewModel {
let clientID: String let clientID: String
let clientSecret: String let clientSecret: String
let authorizeURL: URL let authorizeURL: URL
let redirectURI: String
init?(domain: String, application: Mastodon.Entity.Application) { init?(
domain: String,
application: Mastodon.Entity.Application,
redirectURI: String = MastodonAuthenticationController.callbackURL
) {
self.domain = domain self.domain = domain
guard let clientID = application.clientID, guard let clientID = application.clientID,
let clientSecret = application.clientSecret else { return nil } let clientSecret = application.clientSecret else { return nil }
self.clientID = clientID self.clientID = clientID
self.clientSecret = clientSecret self.clientSecret = clientSecret
self.authorizeURL = { self.authorizeURL = {
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID) let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID, redirectURI: redirectURI)
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
return url return url
}() }()
self.redirectURI = redirectURI
} }
} }
@ -138,8 +142,6 @@ extension AuthenticationViewModel {
.handleEvents(receiveOutput: { [weak self] _ in .handleEvents(receiveOutput: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.isAuthenticating.value = true self.isAuthenticating.value = true
self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil)
self.mastodonPinBasedAuthenticationViewController = nil
}) })
.compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in .compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in
guard let self = self else { return nil } guard let self = self else { return nil }
@ -148,6 +150,7 @@ extension AuthenticationViewModel {
domain: info.domain, domain: info.domain,
clientID: info.clientID, clientID: info.clientID,
clientSecret: info.clientSecret, clientSecret: info.clientSecret,
redirectURI: info.redirectURI,
code: code code: code
) )
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in .flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in

View File

@ -0,0 +1,75 @@
//
// MastodonAuthenticationController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-6-4.
//
import os.log
import UIKit
import Combine
import AuthenticationServices
final class MastodonAuthenticationController {
static let callbackURLScheme = "mastodon"
static let callbackURL = "mastodon://joinmastodon.org/oauth"
var disposeBag = Set<AnyCancellable>()
// input
var context: AppContext!
let authenticateURL: URL
var authenticationSession: ASWebAuthenticationSession?
// output
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
let error = CurrentValueSubject<Error?, Never>(nil)
let pinCodePublisher = PassthroughSubject<String, Never>()
init(
context: AppContext,
authenticateURL: URL
) {
self.context = context
self.authenticateURL = authenticateURL
authentication()
}
}
extension MastodonAuthenticationController {
private func authentication() {
authenticationSession = ASWebAuthenticationSession(
url: authenticateURL,
callbackURLScheme: MastodonAuthenticationController.callbackURLScheme
) { [weak self] callback, error in
guard let self = self else { return }
os_log("%{public}s[%{public}ld], %{public}s: callback: %s, error: %s", ((#file as NSString).lastPathComponent), #line, #function, callback?.debugDescription ?? "<nil>", error.debugDescription)
if let error = error {
if let error = error as? ASWebAuthenticationSessionError {
if error.errorCode == ASWebAuthenticationSessionError.canceledLogin.rawValue {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user cancel authentication", ((#file as NSString).lastPathComponent), #line, #function)
self.isAuthenticating.value = false
return
}
}
self.isAuthenticating.value = false
self.error.value = error
return
}
guard let url = callback,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
let code = codeQueryItem.value else {
return
}
self.pinCodePublisher.send(code)
}
}
}

View File

@ -94,7 +94,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
tableView.translatesAutoresizingMaskIntoConstraints = false tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self tableView.delegate = self
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color tableView.backgroundColor = .clear
tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsAppearanceTableViewCell.self)) tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsAppearanceTableViewCell.self))
tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsToggleTableViewCell.self)) tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsToggleTableViewCell.self))
@ -185,7 +185,14 @@ class SettingsViewController: UIViewController, NeedsDependency {
} }
private func setupView() { private func setupView() {
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color view.backgroundColor = UIColor(dynamicProvider: { traitCollection in
switch traitCollection.userInterfaceLevel {
case .elevated where traitCollection.userInterfaceStyle == .dark:
return Asset.Colors.Background.systemElevatedBackground.color
default:
return Asset.Colors.Background.secondarySystemBackground.color
}
})
setupNavigation() setupNavigation()
view.addSubview(tableView) view.addSubview(tableView)

View File

@ -176,7 +176,7 @@ class SettingsAppearanceTableViewCell: UITableViewCell {
// MARK: Private methods // MARK: Private methods
private func setupUI() { private func setupUI() {
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color backgroundColor = .clear
selectionStyle = .none selectionStyle = .none
contentView.addSubview(stackView) contentView.addSubview(stackView)

View File

@ -39,7 +39,8 @@ class SettingsSectionHeader: UIView {
init(frame: CGRect, customView: UIView? = nil) { init(frame: CGRect, customView: UIView? = nil) {
super.init(frame: frame) super.init(frame: frame)
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color backgroundColor = .clear
stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(titleLabel)
if let view = customView { if let view = customView {
stackView.addArrangedSubview(view) stackView.addArrangedSubview(view)

View File

@ -46,7 +46,7 @@ struct MosaicMeta {
let blurhash: String? let blurhash: String?
let altText: String? let altText: String?
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent) let workingQueue = DispatchQueue(label: "org.joinmastodon.app.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent)
func blurhashImagePublisher() -> AnyPublisher<UIImage?, Never> { func blurhashImagePublisher() -> AnyPublisher<UIImage?, Never> {
return Future { promise in return Future { promise in

View File

@ -14,7 +14,7 @@ import UIKit
final class VideoPlayerViewModel { final class VideoPlayerViewModel {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.video-playback-service.appWillPlayVideo") static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.video-playback-service.appWillPlayVideo")
// input // input
let previewImageURL: URL? let previewImageURL: URL?
let videoURL: URL let videoURL: URL

View File

@ -20,7 +20,11 @@ extension APIService {
#endif #endif
func createApplication(domain: String) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> { func createApplication(domain: String) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> {
let query = Mastodon.API.App.CreateQuery(clientName: APIService.clientName, website: nil) let query = Mastodon.API.App.CreateQuery(
clientName: APIService.clientName,
redirectURIs: MastodonAuthenticationController.callbackURL,
website: nil
)
return Mastodon.API.App.create( return Mastodon.API.App.create(
session: session, session: session,
domain: domain, domain: domain,

View File

@ -17,11 +17,13 @@ extension APIService {
domain: String, domain: String,
clientID: String, clientID: String,
clientSecret: String, clientSecret: String,
redirectURI: String,
code: String code: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
let query = Mastodon.API.OAuth.AccessTokenQuery( let query = Mastodon.API.OAuth.AccessTokenQuery(
clientID: clientID, clientID: clientID,
clientSecret: clientSecret, clientSecret: clientSecret,
redirectURI: redirectURI,
code: code, code: code,
grantType: "authorization_code" grantType: "authorization_code"
) )
@ -35,11 +37,13 @@ extension APIService {
func applicationAccessToken( func applicationAccessToken(
domain: String, domain: String,
clientID: String, clientID: String,
clientSecret: String clientSecret: String,
redirectURI: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
let query = Mastodon.API.OAuth.AccessTokenQuery( let query = Mastodon.API.OAuth.AccessTokenQuery(
clientID: clientID, clientID: clientID,
clientSecret: clientSecret, clientSecret: clientSecret,
redirectURI: redirectURI,
code: nil, code: nil,
grantType: "client_credentials" grantType: "client_credentials"
) )

View File

@ -14,7 +14,7 @@ import os.log
final class AudioPlaybackService: NSObject { final class AudioPlaybackService: NSObject {
static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.audio-playback-service.appWillPlayAudio") static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.audio-playback-service.appWillPlayAudio")
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()

View File

@ -15,7 +15,7 @@ final class EmojiService {
weak var apiService: APIService? weak var apiService: APIService?
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.EmojiService.working-queue") let workingQueue = DispatchQueue(label: "org.joinmastodon.app.EmojiService.working-queue")
private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:] private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:]
init(apiService: APIService) { init(apiService: APIService) {

View File

@ -52,10 +52,10 @@ final class MastodonAttachmentService {
init( init(
context: AppContext, context: AppContext,
pickerResult: PHPickerResult, pickerResult: PHPickerResult,
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
) { ) {
self.context = context self.context = context
self.authenticationBox = initalAuthenticationBox self.authenticationBox = initialAuthenticationBox
// end init // end init
setupServiceObserver() setupServiceObserver()
@ -90,10 +90,10 @@ final class MastodonAttachmentService {
init( init(
context: AppContext, context: AppContext,
image: UIImage, image: UIImage,
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
) { ) {
self.context = context self.context = context
self.authenticationBox = initalAuthenticationBox self.authenticationBox = initialAuthenticationBox
// end init // end init
setupServiceObserver() setupServiceObserver()
@ -105,10 +105,10 @@ final class MastodonAttachmentService {
init( init(
context: AppContext, context: AppContext,
documentURL: URL, documentURL: URL,
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
) { ) {
self.context = context self.context = context
self.authenticationBox = initalAuthenticationBox self.authenticationBox = initialAuthenticationBox
// end init // end init
setupServiceObserver() setupServiceObserver()
@ -213,7 +213,7 @@ extension MastodonAttachmentService: Equatable, Hashable {
extension MastodonAttachmentService { extension MastodonAttachmentService {
private static func createWorkingQueue() -> DispatchQueue { private static func createWorkingQueue() -> DispatchQueue {
return DispatchQueue(label: "org.joinmastodon.Mastodon.MastodonAttachmentService.\(UUID().uuidString)") return DispatchQueue(label: "org.joinmastodon.app.MastodonAttachmentService.\(UUID().uuidString)")
} }
static func loadAttachment(url: URL) -> AnyPublisher<Mastodon.Query.MediaAttachment, Error> { static func loadAttachment(url: URL) -> AnyPublisher<Mastodon.Query.MediaAttachment, Error> {

View File

@ -17,7 +17,7 @@ final class NotificationService {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue") let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue")
// input // input
weak var apiService: APIService? weak var apiService: APIService?

View File

@ -16,7 +16,7 @@ final class StatusPrefetchingService {
typealias TaskID = String typealias TaskID = String
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.StatusPrefetchingService.working-queue") let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPrefetchingService.working-queue")
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:] private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]

View File

@ -16,7 +16,7 @@ final class StatusPublishService {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.StatusPublishService.working-queue") let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPublishService.working-queue")
// input // input
var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models

View File

@ -14,7 +14,7 @@ import os.log
final class VideoPlaybackService { final class VideoPlaybackService {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.VideoPlaybackService.working-queue") let workingQueue = DispatchQueue(label: "org.joinmastodon.app.VideoPlaybackService.working-queue")
private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:] private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:]
// only for video kind // only for video kind

View File

@ -111,7 +111,7 @@ extension AppContext {
return formatter return formatter
}() }()
private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.AppContext.purgeCacheWorkingQueue") private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.app.AppContext.purgeCacheWorkingQueue")
func purgeCache() -> AnyPublisher<ByteCount, Never> { func purgeCache() -> AnyPublisher<ByteCount, Never> {
Publishers.MergeMany([ Publishers.MergeMany([

View File

@ -103,7 +103,7 @@ extension Mastodon.API.App {
public init( public init(
clientName: String, clientName: String,
redirectURIs: String = "urn:ietf:wg:oauth:2.0:oob", redirectURIs: String,
scopes: String? = "read write follow push", scopes: String? = "read write follow push",
website: String? website: String?
) { ) {

View File

@ -139,7 +139,7 @@ extension Mastodon.API.OAuth {
forceLogin: String? = nil, forceLogin: String? = nil,
responseType: String = "code", responseType: String = "code",
clientID: String, clientID: String,
redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", redirectURI: String,
scope: String? = "read write follow push" scope: String? = "read write follow push"
) { ) {
self.forceLogin = forceLogin self.forceLogin = forceLogin
@ -166,7 +166,7 @@ extension Mastodon.API.OAuth {
public init( public init(
clientID: String, clientID: String,
clientSecret: String, clientSecret: String,
redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", redirectURI: String,
scope: String? = "read write follow push", scope: String? = "read write follow push",
code: String?, code: String?,
grantType: String grantType: String

View File

@ -26,7 +26,7 @@ final class SerialStream: NSObject {
private var buffer: UnsafeMutablePointer<UInt8> private var buffer: UnsafeMutablePointer<UInt8>
private var canWrite = false private var canWrite = false
private let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.SerialStream.\(UUID().uuidString)") private let workingQueue = DispatchQueue(label: "org.joinmastodon.app.SerialStream.\(UUID().uuidString)")
// bound pair stream // bound pair stream
private(set) lazy var boundStreams: Streams = { private(set) lazy var boundStreams: Streams = {

View File

@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.org.joinmastodon.mastodon-temp</string> <string>group.org.joinmastodon.app</string>
</array> </array>
</dict> </dict>
</plist> </plist>