Merge branch 'release/0.6.2'
This commit is contained in:
commit
61a3c08a69
|
@ -207,6 +207,7 @@
|
||||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
|
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
|
||||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
|
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
|
||||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; };
|
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; };
|
||||||
|
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */; };
|
||||||
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
|
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||||
|
@ -414,6 +415,8 @@
|
||||||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
||||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; };
|
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; };
|
||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||||
|
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
|
||||||
|
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; };
|
||||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
|
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
|
||||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||||
|
@ -779,6 +782,7 @@
|
||||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
|
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
|
||||||
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||||
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
|
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderImageCacheService.swift; sourceTree = "<group>"; };
|
||||||
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -984,6 +988,7 @@
|
||||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
||||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
||||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
|
||||||
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
|
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
|
||||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1045,6 +1050,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */,
|
||||||
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
||||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||||
|
@ -1350,6 +1356,8 @@
|
||||||
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
||||||
DB6D9F6226357848008423CD /* SettingService.swift */,
|
DB6D9F6226357848008423CD /* SettingService.swift */,
|
||||||
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
|
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
|
||||||
|
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||||
|
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||||
);
|
);
|
||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2497,6 +2505,7 @@
|
||||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||||
DBB525072611EAC0002F1F29 /* Tabman */,
|
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||||
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
||||||
|
DBAEDE5E267A0B1500D25FF5 /* Nuke */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -2686,6 +2695,7 @@
|
||||||
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
||||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||||
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||||
|
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -2995,6 +3005,7 @@
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||||
|
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
|
||||||
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
||||||
|
@ -3100,6 +3111,7 @@
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||||
|
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */,
|
||||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */,
|
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */,
|
||||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||||
|
@ -3614,7 +3626,7 @@
|
||||||
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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||||
|
@ -3622,7 +3634,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.6.1;
|
MARKETING_VERSION = 0.6.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -3641,7 +3653,7 @@
|
||||||
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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||||
|
@ -3649,7 +3661,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.6.1;
|
MARKETING_VERSION = 0.6.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -3904,7 +3916,7 @@
|
||||||
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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -3912,7 +3924,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.6.1;
|
MARKETING_VERSION = 0.6.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -3927,7 +3939,7 @@
|
||||||
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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -3935,7 +3947,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.6.1;
|
MARKETING_VERSION = 0.6.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -4027,7 +4039,7 @@
|
||||||
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
|
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 5.0.2;
|
version = 5.0.3;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
|
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
|
||||||
|
@ -4102,6 +4114,14 @@
|
||||||
minimumVersion = 1.4.1;
|
minimumVersion = 1.4.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/kean/Nuke.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 10.3.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = {
|
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/uias/Tabman";
|
repositoryURL = "https://github.com/uias/Tabman";
|
||||||
|
@ -4185,6 +4205,11 @@
|
||||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||||
productName = "UITextView+Placeholder";
|
productName = "UITextView+Placeholder";
|
||||||
};
|
};
|
||||||
|
DBAEDE5E267A0B1500D25FF5 /* Nuke */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||||
|
productName = Nuke;
|
||||||
|
};
|
||||||
DBB525072611EAC0002F1F29 /* Tabman */ = {
|
DBB525072611EAC0002F1F29 /* Tabman */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */;
|
package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */;
|
||||||
|
|
|
@ -27,12 +27,12 @@
|
||||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>13</integer>
|
<integer>12</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>12</integer>
|
<integer>16</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "2132a7bf8da2bea74bb49b0b815950ea4d8f6a7e",
|
"revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672",
|
||||||
"version": "5.0.2"
|
"version": "5.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -73,6 +73,15 @@
|
||||||
"version": "6.2.1"
|
"version": "6.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "Nuke",
|
||||||
|
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "69ae6d5b8c4b898450432f94bd35f863d3830cfc",
|
||||||
|
"version": "10.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "Pageboy",
|
"package": "Pageboy",
|
||||||
"repositoryURL": "https://github.com/uias/Pageboy",
|
"repositoryURL": "https://github.com/uias/Pageboy",
|
||||||
|
|
|
@ -115,11 +115,11 @@ extension PickServerSection {
|
||||||
guard let proxiedThumbnail = server.proxiedThumbnail,
|
guard let proxiedThumbnail = server.proxiedThumbnail,
|
||||||
let url = URL(string: proxiedThumbnail) else {
|
let url = URL(string: proxiedThumbnail) else {
|
||||||
cell.thumbnailImageView.image = placeholderImage
|
cell.thumbnailImageView.image = placeholderImage
|
||||||
cell.thumbnailActivityIdicator.stopAnimating()
|
cell.thumbnailActivityIndicator.stopAnimating()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cell.thumbnailImageView.isHidden = false
|
cell.thumbnailImageView.isHidden = false
|
||||||
cell.thumbnailActivityIdicator.startAnimating()
|
cell.thumbnailActivityIndicator.startAnimating()
|
||||||
|
|
||||||
cell.thumbnailImageView.af.setImage(
|
cell.thumbnailImageView.af.setImage(
|
||||||
withURL: url,
|
withURL: url,
|
||||||
|
@ -129,7 +129,7 @@ extension PickServerSection {
|
||||||
completion: { [weak cell] response in
|
completion: { [weak cell] response in
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case .success, .failure:
|
case .success, .failure:
|
||||||
cell?.thumbnailActivityIdicator.stopAnimating()
|
cell?.thumbnailActivityIndicator.stopAnimating()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import CoreDataStack
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVKit
|
import AVKit
|
||||||
|
import Nuke
|
||||||
|
|
||||||
protocol StatusCell: DisposeBagCollectable {
|
protocol StatusCell: DisposeBagCollectable {
|
||||||
var statusView: StatusView { get }
|
var statusView: StatusView { get }
|
||||||
|
@ -84,7 +85,7 @@ extension StatusSection {
|
||||||
case .root:
|
case .root:
|
||||||
StatusSection.configureThreadMeta(cell: cell, status: status)
|
StatusSection.configureThreadMeta(cell: cell, status: status)
|
||||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { change in
|
||||||
|
@ -160,9 +161,9 @@ extension StatusSection {
|
||||||
requestUserID: String,
|
requestUserID: String,
|
||||||
statusItemAttribute: Item.StatusAttribute
|
statusItemAttribute: Item.StatusAttribute
|
||||||
) {
|
) {
|
||||||
// safely cancel the listenser when deleted
|
// safely cancel the listener when deleted
|
||||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { [weak cell] change in
|
} receiveValue: { [weak cell] change in
|
||||||
|
@ -174,11 +175,10 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
|
|
||||||
// set header
|
// set header
|
||||||
StatusSection.configureHeader(cell: cell, status: status)
|
StatusSection.configureHeader(cell: cell, status: status)
|
||||||
ManagedObjectObserver.observe(object: status)
|
ManagedObjectObserver.observe(object: status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { [weak cell] change in
|
} receiveValue: { [weak cell] change in
|
||||||
|
@ -221,7 +221,7 @@ extension StatusSection {
|
||||||
cell.statusView.updateVisibility(visibility: visibility)
|
cell.statusView.updateVisibility(visibility: visibility)
|
||||||
|
|
||||||
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak cell] isHidden in
|
.sink { [weak cell] isHidden in
|
||||||
cell?.statusView.visibilityImageView.isHidden = !isHidden
|
cell?.statusView.visibilityImageView.isHidden = !isHidden
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@ extension StatusSection {
|
||||||
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
||||||
|
|
||||||
// set image
|
// set image
|
||||||
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
let mosaicImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
||||||
let imageViewMaxSize: CGSize = {
|
let imageViewMaxSize: CGSize = {
|
||||||
let maxWidth: CGFloat = {
|
let maxWidth: CGFloat = {
|
||||||
// use timelinePostView width as container width
|
// use timelinePostView width as container width
|
||||||
|
@ -246,61 +246,78 @@ extension StatusSection {
|
||||||
return containerWidth
|
return containerWidth
|
||||||
}()
|
}()
|
||||||
let scale: CGFloat = {
|
let scale: CGFloat = {
|
||||||
switch mosiacImageViewModel.metas.count {
|
switch mosaicImageViewModel.metas.count {
|
||||||
case 1: return 1.3
|
case 1: return 1.3
|
||||||
default: return 0.7
|
default: return 0.7
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return CGSize(width: maxWidth, height: maxWidth * scale)
|
return CGSize(width: maxWidth, height: maxWidth * scale)
|
||||||
}()
|
}()
|
||||||
let blurhashImageCache = dependency.context.documentStore.blurhashImageCache
|
|
||||||
let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = {
|
let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = {
|
||||||
if mosiacImageViewModel.metas.count == 1 {
|
if mosaicImageViewModel.metas.count == 1 {
|
||||||
let meta = mosiacImageViewModel.metas[0]
|
let meta = mosaicImageViewModel.metas[0]
|
||||||
let mosaic = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
|
let mosaic = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
|
||||||
return [mosaic]
|
return [mosaic]
|
||||||
} else {
|
} else {
|
||||||
let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
|
let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosaicImageViewModel.metas.count, maxSize: imageViewMaxSize)
|
||||||
return mosaics
|
return mosaics
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
for (i, mosiac) in mosaics.enumerated() {
|
for (i, mosaic) in mosaics.enumerated() {
|
||||||
let (imageView, blurhashOverlayImageView) = mosiac
|
let imageView = mosaic.imageView
|
||||||
let meta = mosiacImageViewModel.metas[i]
|
let blurhashOverlayImageView = mosaic.blurhashOverlayImageView
|
||||||
let blurhashImageDataKey = meta.url.absoluteString as NSString
|
let meta = mosaicImageViewModel.metas[i]
|
||||||
if let blurhashImageData = blurhashImageCache.object(forKey: meta.url.absoluteString as NSString),
|
|
||||||
let image = UIImage(data: blurhashImageData as Data) {
|
|
||||||
blurhashOverlayImageView.image = image
|
|
||||||
} else {
|
|
||||||
meta.blurhashImagePublisher()
|
meta.blurhashImagePublisher()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak blurhashImageCache] image in
|
.sink { image in
|
||||||
guard let blurhashImageCache = blurhashImageCache else { return }
|
|
||||||
blurhashOverlayImageView.image = image
|
blurhashOverlayImageView.image = image
|
||||||
image?.pngData().flatMap {
|
|
||||||
blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
|
||||||
imageView.af.setImage(
|
let imageSize = CGSize(
|
||||||
withURL: meta.url,
|
width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale,
|
||||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale
|
||||||
imageTransition: .crossDissolve(0.2)
|
)
|
||||||
) { response in
|
let request = ImageRequest(
|
||||||
switch response.result {
|
url: meta.url,
|
||||||
case .success:
|
processors: [
|
||||||
statusItemAttribute.isImageLoaded.value = true
|
ImageProcessors.Resize(size: imageSize, contentMode: .aspectFill)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
let options = ImageLoadingOptions(
|
||||||
|
transition: .fadeIn(duration: 0.2)
|
||||||
|
)
|
||||||
|
Nuke.loadImage(
|
||||||
|
with: request,
|
||||||
|
options: options,
|
||||||
|
into: imageView
|
||||||
|
) { result in
|
||||||
|
switch result {
|
||||||
case .failure:
|
case .failure:
|
||||||
break
|
break
|
||||||
|
case .success:
|
||||||
|
statusItemAttribute.isImageLoaded.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//imageView.af.setImage(
|
||||||
|
// withURL: meta.url,
|
||||||
|
// placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
// imageTransition: .crossDissolve(0.2)
|
||||||
|
//) { response in
|
||||||
|
// switch response.result {
|
||||||
|
// case .success:
|
||||||
|
// statusItemAttribute.isImageLoaded.value = true
|
||||||
|
// case .failure:
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
//}
|
||||||
imageView.accessibilityLabel = meta.altText
|
imageView.accessibilityLabel = meta.altText
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
statusItemAttribute.isImageLoaded,
|
statusItemAttribute.isImageLoaded,
|
||||||
statusItemAttribute.isRevealing
|
statusItemAttribute.isRevealing
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak cell] isImageLoaded, isMediaRevealing in
|
.sink { [weak cell] isImageLoaded, isMediaRevealing in
|
||||||
guard let cell = cell else { return }
|
guard let cell = cell else { return }
|
||||||
guard isImageLoaded else {
|
guard isImageLoaded else {
|
||||||
|
@ -322,7 +339,7 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
cell.statusView.statusMosaicImageViewContainer.isHidden = mosaicImageViewModel.metas.isEmpty
|
||||||
|
|
||||||
// set audio
|
// set audio
|
||||||
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
|
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
|
||||||
|
@ -408,7 +425,7 @@ extension StatusSection {
|
||||||
)
|
)
|
||||||
// observe model change
|
// observe model change
|
||||||
ManagedObjectObserver.observe(object: status)
|
ManagedObjectObserver.observe(object: status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { [weak dependency, weak cell] change in
|
} receiveValue: { [weak dependency, weak cell] change in
|
||||||
|
@ -479,7 +496,7 @@ extension StatusSection {
|
||||||
|
|
||||||
// observe model change
|
// observe model change
|
||||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { [weak dependency, weak cell] change in
|
} receiveValue: { [weak dependency, weak cell] change in
|
||||||
|
@ -700,7 +717,7 @@ extension StatusSection {
|
||||||
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
||||||
.assertNoFailure()
|
.assertNoFailure()
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak dependency, weak cell] _, change in
|
.sink { [weak dependency, weak cell] _, change in
|
||||||
guard let cell = cell else { return }
|
guard let cell = cell else { return }
|
||||||
guard let dependency = dependency else { return }
|
guard let dependency = dependency else { return }
|
||||||
|
|
|
@ -63,6 +63,7 @@ extension ActiveLabel {
|
||||||
extension ActiveLabel {
|
extension ActiveLabel {
|
||||||
/// status content
|
/// status content
|
||||||
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
|
attributedText = nil
|
||||||
activeEntities.removeAll()
|
activeEntities.removeAll()
|
||||||
|
|
||||||
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
||||||
|
@ -144,10 +145,10 @@ extension ActiveLabel {
|
||||||
element.accessibilityLanguage = accessibilityLanguage
|
element.accessibilityLanguage = accessibilityLanguage
|
||||||
elements.append(element)
|
elements.append(element)
|
||||||
|
|
||||||
for eneity in activeEntities {
|
for entity in activeEntities {
|
||||||
guard let element = eneity.accessibilityElement(in: self) else { continue }
|
guard let element = entity.accessibilityElement(in: self) else { continue }
|
||||||
var glyphRange = NSRange()
|
var glyphRange = NSRange()
|
||||||
layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
|
||||||
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
element.accessibilityFrame = self.convert(rect, to: nil)
|
element.accessibilityFrame = self.convert(rect, to: nil)
|
||||||
element.accessibilityContainer = self
|
element.accessibilityContainer = self
|
||||||
|
|
|
@ -91,6 +91,7 @@ internal enum Asset {
|
||||||
}
|
}
|
||||||
internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey")
|
internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey")
|
||||||
internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
|
internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
|
||||||
|
internal static let brandBlueDarken20 = ColorAsset(name: "Colors/brand.blue.darken.20")
|
||||||
internal static let danger = ColorAsset(name: "Colors/danger")
|
internal static let danger = ColorAsset(name: "Colors/danger")
|
||||||
internal static let disabled = ColorAsset(name: "Colors/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/disabled")
|
||||||
internal static let inactive = ColorAsset(name: "Colors/inactive")
|
internal static let inactive = ColorAsset(name: "Colors/inactive")
|
||||||
|
|
|
@ -22,14 +22,14 @@ extension AvatarConfigurableView {
|
||||||
|
|
||||||
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
||||||
let placeholderImage: UIImage = {
|
let placeholderImage: UIImage = {
|
||||||
let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill)
|
guard let placeholderImage = configuration.placeholderImage else {
|
||||||
if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 {
|
return AppContext.shared.placeholderImageCacheService.image(
|
||||||
return placeholderImage
|
color: .systemFill,
|
||||||
.af.imageAspectScaled(toFill: Self.configurableAvatarImageSize)
|
size: Self.configurableAvatarImageSize,
|
||||||
.af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: false)
|
cornerRadius: Self.configurableAvatarImageCornerRadius
|
||||||
} else {
|
)
|
||||||
return placeholderImage.af.imageRoundedIntoCircle()
|
|
||||||
}
|
}
|
||||||
|
return placeholderImage
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// cancel previous task
|
// cancel previous task
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xB0",
|
||||||
|
"green" : "0x73",
|
||||||
|
"red" : "0x1F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xC9",
|
||||||
|
"green" : "0x80",
|
||||||
|
"red" : "0x1B"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc
|
||||||
|
|
||||||
let largeTitleLabel: UILabel = {
|
let largeTitleLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34))
|
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.systemFont(ofSize: 34, weight: .bold))
|
||||||
label.textColor = .label
|
label.textColor = .label
|
||||||
label.text = L10n.Scene.ConfirmEmail.title
|
label.text = L10n.Scene.ConfirmEmail.title
|
||||||
return label
|
return label
|
||||||
|
@ -45,14 +45,8 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let openEmailButton: UIButton = {
|
let openEmailButton: UIButton = {
|
||||||
let button = UIButton(type: .system)
|
let button = PrimaryActionButton()
|
||||||
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
|
||||||
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color), for: .normal)
|
|
||||||
button.setTitleColor(.white, for: .normal)
|
|
||||||
button.setTitle(L10n.Scene.ConfirmEmail.Button.openEmailApp, for: .normal)
|
button.setTitle(L10n.Scene.ConfirmEmail.Button.openEmailApp, for: .normal)
|
||||||
button.layer.masksToBounds = true
|
|
||||||
button.layer.cornerRadius = 8
|
|
||||||
button.layer.cornerCurve = .continuous
|
|
||||||
button.addTarget(self, action: #selector(openEmailButtonPressed(_:)), for: UIControl.Event.touchUpInside)
|
button.addTarget(self, action: #selector(openEmailButtonPressed(_:)), for: UIControl.Event.touchUpInside)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -237,21 +237,23 @@ extension MastodonPickServerViewController {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
viewModel.emptyStateViewState
|
viewModel.emptyStateViewState
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] state in
|
.sink { [weak self] state in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch state {
|
switch state {
|
||||||
case .none:
|
case .none:
|
||||||
self.emptyStateView.isHidden = true
|
UIView.animate(withDuration: 0.3) {
|
||||||
|
self.emptyStateView.alpha = 0
|
||||||
|
}
|
||||||
case .loading:
|
case .loading:
|
||||||
self.emptyStateView.isHidden = false
|
self.emptyStateView.alpha = 1
|
||||||
self.emptyStateView.networkIndicatorImageView.isHidden = true
|
self.emptyStateView.networkIndicatorImageView.isHidden = true
|
||||||
self.emptyStateView.activityIndicatorView.startAnimating()
|
self.emptyStateView.activityIndicatorView.startAnimating()
|
||||||
self.emptyStateView.infoLabel.isHidden = false
|
self.emptyStateView.infoLabel.isHidden = false
|
||||||
self.emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.findingServers
|
self.emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.findingServers
|
||||||
self.emptyStateView.infoLabel.textAlignment = self.traitCollection.layoutDirection == .rightToLeft ? .right : .left
|
self.emptyStateView.infoLabel.textAlignment = self.traitCollection.layoutDirection == .rightToLeft ? .right : .left
|
||||||
case .badNetwork:
|
case .badNetwork:
|
||||||
self.emptyStateView.isHidden = false
|
self.emptyStateView.alpha = 1
|
||||||
self.emptyStateView.networkIndicatorImageView.isHidden = false
|
self.emptyStateView.networkIndicatorImageView.isHidden = false
|
||||||
self.emptyStateView.activityIndicatorView.stopAnimating()
|
self.emptyStateView.activityIndicatorView.stopAnimating()
|
||||||
self.emptyStateView.infoLabel.isHidden = false
|
self.emptyStateView.infoLabel.isHidden = false
|
||||||
|
|
|
@ -45,9 +45,10 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
||||||
viewModel.context.apiService.servers(language: nil, category: nil)
|
viewModel.context.apiService.servers(language: nil, category: nil)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure:
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
|
viewModel.loadingIndexedServersError.value = error
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ class MastodonPickServerViewModel: NSObject {
|
||||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||||
|
|
||||||
let isLoadingIndexedServers = CurrentValueSubject<Bool, Never>(false)
|
let isLoadingIndexedServers = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let loadingIndexedServersError = CurrentValueSubject<Error?, Never>(nil)
|
||||||
let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none)
|
let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none)
|
||||||
|
|
||||||
init(context: AppContext, mode: PickServerMode) {
|
init(context: AppContext, mode: PickServerMode) {
|
||||||
|
@ -142,10 +143,17 @@ extension MastodonPickServerViewModel {
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
isLoadingIndexedServers
|
Publishers.CombineLatest(
|
||||||
.map { isLoadingIndexedServers -> EmptyStateViewState in
|
isLoadingIndexedServers,
|
||||||
|
loadingIndexedServersError
|
||||||
|
)
|
||||||
|
.map { isLoadingIndexedServers, loadingIndexedServersError -> EmptyStateViewState in
|
||||||
if isLoadingIndexedServers {
|
if isLoadingIndexedServers {
|
||||||
|
if loadingIndexedServersError != nil {
|
||||||
|
return .badNetwork
|
||||||
|
} else {
|
||||||
return .loading
|
return .loading
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ class PickServerCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let thumbnailActivityIdicator = UIActivityIndicatorView(style: .medium)
|
let thumbnailActivityIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
|
||||||
let thumbnailImageView: UIImageView = {
|
let thumbnailImageView: UIImageView = {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
|
@ -99,7 +99,7 @@ class PickServerCell: UITableViewCell {
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let seperator: UIView = {
|
let separator: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -177,7 +177,7 @@ class PickServerCell: UITableViewCell {
|
||||||
|
|
||||||
thumbnailImageView.isHidden = false
|
thumbnailImageView.isHidden = false
|
||||||
thumbnailImageView.af.cancelImageRequest()
|
thumbnailImageView.af.cancelImageRequest()
|
||||||
thumbnailActivityIdicator.stopAnimating()
|
thumbnailActivityIndicator.stopAnimating()
|
||||||
disposeBag.removeAll()
|
disposeBag.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ extension PickServerCell {
|
||||||
containerView.addSubview(domainLabel)
|
containerView.addSubview(domainLabel)
|
||||||
containerView.addSubview(checkbox)
|
containerView.addSubview(checkbox)
|
||||||
containerView.addSubview(descriptionLabel)
|
containerView.addSubview(descriptionLabel)
|
||||||
containerView.addSubview(seperator)
|
containerView.addSubview(separator)
|
||||||
|
|
||||||
containerView.addSubview(expandButton)
|
containerView.addSubview(expandButton)
|
||||||
|
|
||||||
|
@ -231,13 +231,13 @@ extension PickServerCell {
|
||||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 1),
|
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||||
|
|
||||||
// Set bottom separator
|
// Set bottom separator
|
||||||
seperator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
separator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||||
containerView.trailingAnchor.constraint(equalTo: seperator.trailingAnchor),
|
containerView.trailingAnchor.constraint(equalTo: separator.trailingAnchor),
|
||||||
containerView.topAnchor.constraint(equalTo: seperator.topAnchor),
|
containerView.topAnchor.constraint(equalTo: separator.topAnchor),
|
||||||
seperator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh),
|
separator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh),
|
||||||
|
|
||||||
domainLabel.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor),
|
domainLabel.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor),
|
||||||
domainLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor),
|
domainLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
@ -272,14 +272,14 @@ extension PickServerCell {
|
||||||
containerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor),
|
containerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
thumbnailActivityIdicator.translatesAutoresizingMaskIntoConstraints = false
|
thumbnailActivityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
thumbnailImageView.addSubview(thumbnailActivityIdicator)
|
thumbnailImageView.addSubview(thumbnailActivityIndicator)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
thumbnailActivityIdicator.centerXAnchor.constraint(equalTo: thumbnailImageView.centerXAnchor),
|
thumbnailActivityIndicator.centerXAnchor.constraint(equalTo: thumbnailImageView.centerXAnchor),
|
||||||
thumbnailActivityIdicator.centerYAnchor.constraint(equalTo: thumbnailImageView.centerYAnchor),
|
thumbnailActivityIndicator.centerYAnchor.constraint(equalTo: thumbnailImageView.centerYAnchor),
|
||||||
])
|
])
|
||||||
thumbnailActivityIdicator.hidesWhenStopped = true
|
thumbnailActivityIndicator.hidesWhenStopped = true
|
||||||
thumbnailActivityIdicator.stopAnimating()
|
thumbnailActivityIndicator.stopAnimating()
|
||||||
|
|
||||||
NSLayoutConstraint.activate(collapseConstraints)
|
NSLayoutConstraint.activate(collapseConstraints)
|
||||||
|
|
||||||
|
|
|
@ -41,13 +41,44 @@ class PickServerSearchCell: UITableViewCell {
|
||||||
let searchTextField: UITextField = {
|
let searchTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
textField.font = .preferredFont(forTextStyle: .headline)
|
textField.leftView = {
|
||||||
|
let imageView = UIImageView(
|
||||||
|
image: UIImage(
|
||||||
|
systemName: "magnifyingglass",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||||
|
|
||||||
|
let containerView = UIView()
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerView.addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let paddingView = UIView()
|
||||||
|
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerView.addSubview(paddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
|
paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||||
|
paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||||
|
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||||
|
paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
return containerView
|
||||||
|
}()
|
||||||
|
textField.leftViewMode = .always
|
||||||
|
textField.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
textField.tintColor = Asset.Colors.Label.primary.color
|
textField.tintColor = Asset.Colors.Label.primary.color
|
||||||
textField.textColor = Asset.Colors.Label.primary.color
|
textField.textColor = Asset.Colors.Label.primary.color
|
||||||
textField.adjustsFontForContentSizeCategory = true
|
textField.adjustsFontForContentSizeCategory = true
|
||||||
textField.attributedPlaceholder =
|
textField.attributedPlaceholder =
|
||||||
NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||||
attributes: [.font: UIFont.preferredFont(forTextStyle: .headline),
|
attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular),
|
||||||
.foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
|
.foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
|
||||||
textField.clearButtonMode = .whileEditing
|
textField.clearButtonMode = .whileEditing
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
|
|
|
@ -76,22 +76,19 @@ extension PickServerEmptyStateView {
|
||||||
])
|
])
|
||||||
containerStackView.addArrangedSubview(networkIndicatorImageView)
|
containerStackView.addArrangedSubview(networkIndicatorImageView)
|
||||||
|
|
||||||
let infoContainerView = UIView()
|
let infoContainerStackView = UIStackView()
|
||||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
infoContainerStackView.axis = .horizontal
|
||||||
infoContainerView.addSubview(activityIndicatorView)
|
infoContainerStackView.distribution = .fill
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
activityIndicatorView.leadingAnchor.constraint(equalTo: infoContainerView.leadingAnchor),
|
infoContainerStackView.addArrangedSubview(activityIndicatorView)
|
||||||
activityIndicatorView.centerYAnchor.constraint(equalTo: infoContainerView.centerYAnchor),
|
infoContainerStackView.spacing = 4
|
||||||
activityIndicatorView.bottomAnchor.constraint(equalTo: infoContainerView.bottomAnchor),
|
activityIndicatorView.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||||
])
|
|
||||||
infoLabel.translatesAutoresizingMaskIntoConstraints = false
|
infoContainerStackView.addArrangedSubview(infoLabel)
|
||||||
infoContainerView.addSubview(infoLabel)
|
infoLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
NSLayoutConstraint.activate([
|
infoLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
infoLabel.leadingAnchor.constraint(equalTo: activityIndicatorView.trailingAnchor, constant: 4),
|
|
||||||
infoLabel.centerYAnchor.constraint(equalTo: infoContainerView.centerYAnchor),
|
containerStackView.addArrangedSubview(infoContainerStackView)
|
||||||
infoLabel.trailingAnchor.constraint(equalTo: infoContainerView.trailingAnchor),
|
|
||||||
])
|
|
||||||
containerStackView.addArrangedSubview(infoContainerView)
|
|
||||||
|
|
||||||
let bottomPaddingView = UIView()
|
let bottomPaddingView = UIView()
|
||||||
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -104,7 +101,7 @@ extension PickServerEmptyStateView {
|
||||||
])
|
])
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 2.0),
|
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
|
|
||||||
activityIndicatorView.hidesWhenStopped = true
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
@ -126,15 +123,18 @@ struct PickServerEmptyStateView_Previews: PreviewProvider {
|
||||||
emptyStateView.activityIndicatorView.stopAnimating()
|
emptyStateView.activityIndicatorView.stopAnimating()
|
||||||
return emptyStateView
|
return emptyStateView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 150))
|
||||||
|
.previewDisplayName("Bad Network")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let emptyStateView = PickServerEmptyStateView()
|
let emptyStateView = PickServerEmptyStateView()
|
||||||
|
emptyStateView.networkIndicatorImageView.isHidden = true
|
||||||
emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.findingServers
|
emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.findingServers
|
||||||
emptyStateView.infoLabel.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .right : .left
|
emptyStateView.infoLabel.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .right : .left
|
||||||
emptyStateView.activityIndicatorView.startAnimating()
|
emptyStateView.activityIndicatorView.startAnimating()
|
||||||
return emptyStateView
|
return emptyStateView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
.previewDisplayName("Loading…")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let largeTitleLabel: UILabel = {
|
let largeTitleLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34))
|
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold))
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
label.text = L10n.Scene.Register.title
|
label.text = L10n.Scene.Register.title
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
|
@ -93,11 +93,10 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let plusIconImageView: UIImageView = {
|
let plusIconImageView: UIImageView = {
|
||||||
let icon = UIImageView()
|
let icon = UIImageView()
|
||||||
|
|
||||||
let image = Asset.Circles.plusCircleFill.image.withRenderingMode(.alwaysTemplate)
|
let image = Asset.Circles.plusCircleFill.image.withRenderingMode(.alwaysTemplate)
|
||||||
icon.image = image
|
icon.image = image
|
||||||
icon.tintColor = Asset.Colors.Icon.plus.color
|
icon.tintColor = Asset.Colors.Icon.plus.color
|
||||||
icon.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
icon.backgroundColor = .white
|
||||||
return icon
|
return icon
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -110,7 +109,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let usernameTextField: UITextField = {
|
let usernameTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
|
textField.returnKeyType = .next
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
textField.autocorrectionType = .no
|
textField.autocorrectionType = .no
|
||||||
textField.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
textField.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
|
@ -119,8 +118,35 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||||
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
||||||
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
||||||
|
textField.font = MastodonRegisterViewController.textFieldLabelFont
|
||||||
|
textField.leftView = {
|
||||||
|
let containerView = UIView()
|
||||||
|
|
||||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||||
textField.leftView = paddingView
|
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerView.addSubview(paddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
|
paddingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||||
|
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||||
|
paddingView.widthAnchor.constraint(equalToConstant: 5).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = MastodonRegisterViewController.textFieldLabelFont
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.text = " @"
|
||||||
|
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerView.addSubview(label)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
|
label.leadingAnchor.constraint(equalTo: paddingView.trailingAnchor),
|
||||||
|
label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||||
|
label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||||
|
])
|
||||||
|
return containerView
|
||||||
|
}()
|
||||||
textField.leftViewMode = .always
|
textField.leftViewMode = .always
|
||||||
return textField
|
return textField
|
||||||
}()
|
}()
|
||||||
|
@ -134,6 +160,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let displayNameTextField: UITextField = {
|
let displayNameTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
|
textField.returnKeyType = .next
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
textField.autocorrectionType = .no
|
textField.autocorrectionType = .no
|
||||||
textField.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
textField.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
|
@ -145,11 +172,13 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||||
textField.leftView = paddingView
|
textField.leftView = paddingView
|
||||||
textField.leftViewMode = .always
|
textField.leftViewMode = .always
|
||||||
|
textField.font = MastodonRegisterViewController.textFieldLabelFont
|
||||||
return textField
|
return textField
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let emailTextField: UITextField = {
|
let emailTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
|
textField.returnKeyType = .next
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
textField.autocorrectionType = .no
|
textField.autocorrectionType = .no
|
||||||
textField.keyboardType = .emailAddress
|
textField.keyboardType = .emailAddress
|
||||||
|
@ -162,6 +191,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||||
textField.leftView = paddingView
|
textField.leftView = paddingView
|
||||||
textField.leftViewMode = .always
|
textField.leftViewMode = .always
|
||||||
|
textField.font = MastodonRegisterViewController.textFieldLabelFont
|
||||||
return textField
|
return textField
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -174,6 +204,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let passwordTextField: UITextField = {
|
let passwordTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
|
textField.returnKeyType = .next // set to "Return" depends on if the last input field or not
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
textField.autocorrectionType = .no
|
textField.autocorrectionType = .no
|
||||||
textField.keyboardType = .asciiCapable
|
textField.keyboardType = .asciiCapable
|
||||||
|
@ -187,6 +218,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||||
textField.leftView = paddingView
|
textField.leftView = paddingView
|
||||||
textField.leftViewMode = .always
|
textField.leftViewMode = .always
|
||||||
|
textField.font = MastodonRegisterViewController.textFieldLabelFont
|
||||||
return textField
|
return textField
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -206,6 +238,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
lazy var reasonTextField: UITextField = {
|
lazy var reasonTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
|
textField.returnKeyType = .next // set to "Return" depends on if the last input field or not
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
textField.autocorrectionType = .no
|
textField.autocorrectionType = .no
|
||||||
textField.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
textField.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||||
|
@ -217,6 +250,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||||
textField.leftView = paddingView
|
textField.leftView = paddingView
|
||||||
textField.leftViewMode = .always
|
textField.leftViewMode = .always
|
||||||
|
textField.font = MastodonRegisterViewController.textFieldLabelFont
|
||||||
return textField
|
return textField
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -363,11 +397,10 @@ extension MastodonRegisterViewController {
|
||||||
|
|
||||||
// return
|
// return
|
||||||
if viewModel.approvalRequired {
|
if viewModel.approvalRequired {
|
||||||
passwordTextField.returnKeyType = .continue
|
reasonTextField.returnKeyType = .done
|
||||||
} else {
|
} else {
|
||||||
passwordTextField.returnKeyType = .done
|
passwordTextField.returnKeyType = .done
|
||||||
}
|
}
|
||||||
reasonTextField.returnKeyType = .done
|
|
||||||
|
|
||||||
// button
|
// button
|
||||||
stackView.addArrangedSubview(buttonContainer)
|
stackView.addArrangedSubview(buttonContainer)
|
||||||
|
@ -627,6 +660,25 @@ extension MastodonRegisterViewController: UITextFieldDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
|
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
|
||||||
|
switch textField {
|
||||||
|
case usernameTextField:
|
||||||
|
viewModel.username.value = text
|
||||||
|
case displayNameTextField:
|
||||||
|
viewModel.displayName.value = text
|
||||||
|
case emailTextField:
|
||||||
|
viewModel.email.value = text
|
||||||
|
case passwordTextField:
|
||||||
|
viewModel.password.value = text
|
||||||
|
case reasonTextField:
|
||||||
|
viewModel.reason.value = text
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
switch textField {
|
switch textField {
|
||||||
case usernameTextField:
|
case usernameTextField:
|
||||||
|
|
|
@ -106,6 +106,7 @@ final class MastodonRegisterViewModel {
|
||||||
case .success:
|
case .success:
|
||||||
let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
|
let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
|
||||||
self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text)
|
self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text)
|
||||||
|
self.usernameValidateState.value = .invalid
|
||||||
case .failure:
|
case .failure:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
||||||
|
|
||||||
let rulesLabel: UILabel = {
|
let rulesLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .preferredFont(forTextStyle: .body)
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
label.text = "Rules"
|
label.text = "Rules"
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
|
@ -66,8 +66,6 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
||||||
|
|
||||||
let confirmButton: PrimaryActionButton = {
|
let confirmButton: PrimaryActionButton = {
|
||||||
let button = PrimaryActionButton()
|
let button = PrimaryActionButton()
|
||||||
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
|
||||||
button.setTitleColor(.white, for: .normal)
|
|
||||||
button.setTitle(L10n.Scene.ServerRules.Button.confirm, for: .normal)
|
button.setTitle(L10n.Scene.ServerRules.Button.confirm, for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -10,15 +10,14 @@ import Combine
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
final class MastodonServerRulesViewModel {
|
final class MastodonServerRulesViewModel {
|
||||||
// input
|
|
||||||
|
|
||||||
|
// input
|
||||||
let domain: String
|
let domain: String
|
||||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||||
let rules: [Mastodon.Entity.Instance.Rule]
|
let rules: [Mastodon.Entity.Instance.Rule]
|
||||||
let instance: Mastodon.Entity.Instance
|
let instance: Mastodon.Entity.Instance
|
||||||
let applicationToken: Mastodon.Entity.Token
|
let applicationToken: Mastodon.Entity.Token
|
||||||
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
domain: String,
|
domain: String,
|
||||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
||||||
|
@ -36,14 +35,19 @@ final class MastodonServerRulesViewModel {
|
||||||
var rulesAttributedString: NSAttributedString {
|
var rulesAttributedString: NSAttributedString {
|
||||||
let attributedString = NSMutableAttributedString(string: "\n")
|
let attributedString = NSMutableAttributedString(string: "\n")
|
||||||
let configuration = UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .title3))
|
let configuration = UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .title3))
|
||||||
|
let separatorString = Array(repeating: " ", count: 4).joined()
|
||||||
for (i, rule) in rules.enumerated() {
|
for (i, rule) in rules.enumerated() {
|
||||||
|
guard i < 50 else {
|
||||||
|
return NSAttributedString(string: "\(i)" + separatorString + rule.text + "\n\n")
|
||||||
|
}
|
||||||
let imageName = String(i + 1) + ".circle.fill"
|
let imageName = String(i + 1) + ".circle.fill"
|
||||||
let image = UIImage(systemName: imageName, withConfiguration: configuration)!
|
let image = UIImage(systemName: imageName, withConfiguration: configuration)!
|
||||||
let attachment = NSTextAttachment()
|
let attachment = NSTextAttachment()
|
||||||
attachment.image = image.withTintColor(Asset.Colors.Label.primary.color)
|
attachment.image = image.withTintColor(Asset.Colors.Label.primary.color)
|
||||||
let imageAttribute = NSAttributedString(attachment: attachment)
|
let imageAttribute = NSMutableAttributedString(attachment: attachment)
|
||||||
|
imageAttribute.addAttributes([NSAttributedString.Key.baselineOffset : -1.5], range: NSRange(location: 0, length: imageAttribute.length))
|
||||||
|
|
||||||
let ruleString = NSAttributedString(string: " " + rule.text + "\n\n")
|
let ruleString = NSAttributedString(string: separatorString + rule.text + "\n\n")
|
||||||
attributedString.append(imageAttribute)
|
attributedString.append(imageAttribute)
|
||||||
attributedString.append(ruleString)
|
attributedString.append(ruleString)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,10 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||||
private(set) lazy var signUpButton: PrimaryActionButton = {
|
private(set) lazy var signUpButton: PrimaryActionButton = {
|
||||||
let button = PrimaryActionButton()
|
let button = PrimaryActionButton()
|
||||||
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
||||||
let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.Button.normal.color
|
let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.brandBlue.color
|
||||||
|
let backgroundImageHighlightedColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor(white: 0.8, alpha: 1.0) : Asset.Colors.brandBlueDarken20.color
|
||||||
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
|
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
|
||||||
button.setBackgroundImage(.placeholder(color: backgroundImageColor.withAlphaComponent(0.9)), for: .highlighted)
|
button.setBackgroundImage(.placeholder(color: backgroundImageHighlightedColor), for: .highlighted)
|
||||||
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.normal.color : UIColor.white
|
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.normal.color : UIColor.white
|
||||||
button.setTitleColor(titleColor, for: .normal)
|
button.setTitleColor(titleColor, for: .normal)
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -67,6 +68,8 @@ extension WelcomeViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.overrideUserInterfaceStyle = .light
|
||||||
|
|
||||||
setupOnboardingAppearance()
|
setupOnboardingAppearance()
|
||||||
setupIllustrationLayout()
|
setupIllustrationLayout()
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,8 @@ extension PrimaryActionButton {
|
||||||
titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||||
setTitleColor(.white, for: .normal)
|
setTitleColor(.white, for: .normal)
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color), for: .normal)
|
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color), for: .normal)
|
||||||
setupButtonBackground()
|
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlueDarken20.color), for: .highlighted)
|
||||||
|
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled)
|
||||||
applyCornerRadius(radius: 10)
|
applyCornerRadius(radius: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,20 +69,4 @@ extension PrimaryActionButton {
|
||||||
self.setTitle(originalButtonTitle, for: .disabled)
|
self.setTitle(originalButtonTitle, for: .disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
setupButtonBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupButtonBackground() {
|
|
||||||
if UIScreen.main.traitCollection.userInterfaceStyle == .light {
|
|
||||||
setTitleColor(.white, for: .disabled)
|
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
|
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .disabled)
|
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color.withAlphaComponent(0.5)), for: .highlighted)
|
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color.withAlphaComponent(0.5)), for: .disabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,11 @@ extension MosaicImageViewContainer {
|
||||||
container.spacing = 1
|
container.spacing = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias ConfigurableMosaic = (imageView: UIImageView, blurhashOverlayImageView: UIImageView)
|
struct ConfigurableMosaic {
|
||||||
|
let imageView: UIImageView
|
||||||
|
let blurhashOverlayImageView: UIImageView
|
||||||
|
let imageViewSize: CGSize
|
||||||
|
}
|
||||||
|
|
||||||
func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> ConfigurableMosaic {
|
func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> ConfigurableMosaic {
|
||||||
reset()
|
reset()
|
||||||
|
@ -163,15 +167,21 @@ extension MosaicImageViewContainer {
|
||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
return (imageView, blurhashOverlayImageView)
|
return ConfigurableMosaic(
|
||||||
|
imageView: imageView,
|
||||||
|
blurhashOverlayImageView: blurhashOverlayImageView,
|
||||||
|
imageViewSize: maxSize
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupImageViews(count: Int, maxHeight: CGFloat) -> [ConfigurableMosaic] {
|
func setupImageViews(count: Int, maxSize: CGSize) -> [ConfigurableMosaic] {
|
||||||
reset()
|
reset()
|
||||||
guard count > 1 else {
|
guard count > 1 else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maxHeight = maxSize.height
|
||||||
|
|
||||||
containerHeightLayoutConstraint.constant = maxHeight
|
containerHeightLayoutConstraint.constant = maxHeight
|
||||||
containerHeightLayoutConstraint.isActive = true
|
containerHeightLayoutConstraint.isActive = true
|
||||||
|
|
||||||
|
@ -295,7 +305,35 @@ extension MosaicImageViewContainer {
|
||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
return zip(imageViews, blurhashOverlayImageViews).map { ($0, $1) }
|
var mosaics: [ConfigurableMosaic] = []
|
||||||
|
for (i, (imageView, blurhashOverlayImageView)) in zip(imageViews, blurhashOverlayImageViews).enumerated() {
|
||||||
|
let imageViewSize: CGSize = {
|
||||||
|
switch (i, count) {
|
||||||
|
case (_, 4):
|
||||||
|
return CGSize(width: maxSize.width * 0.5, height: maxSize.height * 0.5)
|
||||||
|
case (i, 3):
|
||||||
|
let width = maxSize.width * 0.5
|
||||||
|
if i == 0 {
|
||||||
|
return CGSize(width: width, height: maxSize.height)
|
||||||
|
} else {
|
||||||
|
return CGSize(width: width, height: maxSize.height * 0.5)
|
||||||
|
}
|
||||||
|
case (_, 2):
|
||||||
|
let width = maxSize.width * 0.5
|
||||||
|
return CGSize(width: width, height: maxSize.height)
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
return maxSize
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let mosaic = ConfigurableMosaic(
|
||||||
|
imageView: imageView,
|
||||||
|
blurhashOverlayImageView: blurhashOverlayImageView,
|
||||||
|
imageViewSize: imageViewSize
|
||||||
|
)
|
||||||
|
mosaics.append(mosaic)
|
||||||
|
}
|
||||||
|
return mosaics
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -366,11 +404,11 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let image = images[3]
|
let image = images[3]
|
||||||
let (imageView, _) = view.setupImageView(
|
let mosaic = view.setupImageView(
|
||||||
aspectRatio: image.size,
|
aspectRatio: image.size,
|
||||||
maxSize: CGSize(width: 375, height: 400)
|
maxSize: CGSize(width: 375, height: 400)
|
||||||
)
|
)
|
||||||
imageView.image = image
|
mosaic.imageView.image = image
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
@ -378,14 +416,14 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let image = images[1]
|
let image = images[1]
|
||||||
let (imageView, _) = view.setupImageView(
|
let mosaic = view.setupImageView(
|
||||||
aspectRatio: image.size,
|
aspectRatio: image.size,
|
||||||
maxSize: CGSize(width: 375, height: 400)
|
maxSize: CGSize(width: 375, height: 400)
|
||||||
)
|
)
|
||||||
imageView.layer.masksToBounds = true
|
mosaic.imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = 8
|
mosaic.imageView.layer.cornerRadius = 8
|
||||||
imageView.contentMode = .scaleAspectFill
|
mosaic.imageView.contentMode = .scaleAspectFill
|
||||||
imageView.image = image
|
mosaic.imageView.image = image
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
@ -393,10 +431,9 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(2)
|
let images = self.images.prefix(2)
|
||||||
let mosaics = view.setupImageViews(count: images.count, maxHeight: 162)
|
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
||||||
for (i, mosiac) in mosaics.enumerated() {
|
for (i, mosaic) in mosaics.enumerated() {
|
||||||
let (imageView, _) = mosiac
|
mosaic.imageView.image = images[i]
|
||||||
imageView.image = images[i]
|
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -405,10 +442,9 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(3)
|
let images = self.images.prefix(3)
|
||||||
let mosaics = view.setupImageViews(count: images.count, maxHeight: 162)
|
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
||||||
for (i, mosiac) in mosaics.enumerated() {
|
for (i, mosaic) in mosaics.enumerated() {
|
||||||
let (imageView, _) = mosiac
|
mosaic.imageView.image = images[i]
|
||||||
imageView.image = images[i]
|
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -417,10 +453,9 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(4)
|
let images = self.images.prefix(4)
|
||||||
let mosaics = view.setupImageViews(count: images.count, maxHeight: 162)
|
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
||||||
for (i, mosiac) in mosaics.enumerated() {
|
for (i, mosaic) in mosaics.enumerated() {
|
||||||
let (imageView, _) = mosiac
|
mosaic.imageView.image = images[i]
|
||||||
imageView.image = images[i]
|
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -619,10 +619,9 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
)
|
)
|
||||||
statusView.headerContainerView.isHidden = false
|
statusView.headerContainerView.isHidden = false
|
||||||
let images = MosaicImageView_Previews.images
|
let images = MosaicImageView_Previews.images
|
||||||
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
|
||||||
for (i, mosaic) in mosaics.enumerated() {
|
for (i, mosaic) in mosaics.enumerated() {
|
||||||
let (imageView, _) = mosaic
|
mosaic.imageView.image = images[i]
|
||||||
imageView.image = images[i]
|
|
||||||
}
|
}
|
||||||
statusView.statusMosaicImageViewContainer.isHidden = false
|
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||||
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
|
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
|
||||||
|
@ -644,10 +643,9 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
statusView.updateContentWarningDisplay(isHidden: false, animated: false)
|
statusView.updateContentWarningDisplay(isHidden: false, animated: false)
|
||||||
statusView.drawContentWarningImageView()
|
statusView.drawContentWarningImageView()
|
||||||
let images = MosaicImageView_Previews.images
|
let images = MosaicImageView_Previews.images
|
||||||
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
|
||||||
for (i, mosaic) in mosaics.enumerated() {
|
for (i, mosaic) in mosaics.enumerated() {
|
||||||
let (imageView, _) = mosaic
|
mosaic.imageView.image = images[i]
|
||||||
imageView.image = images[i]
|
|
||||||
}
|
}
|
||||||
statusView.statusMosaicImageViewContainer.isHidden = false
|
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||||
return statusView
|
return statusView
|
||||||
|
|
|
@ -46,39 +46,11 @@ struct MosaicMeta {
|
||||||
let blurhash: String?
|
let blurhash: String?
|
||||||
let altText: String?
|
let altText: String?
|
||||||
|
|
||||||
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
|
|
||||||
workingQueue.async {
|
|
||||||
let image = self.blurhashImage()
|
|
||||||
promise(.success(image))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
func blurhashImage() -> UIImage? {
|
|
||||||
guard let blurhash = blurhash else {
|
guard let blurhash = blurhash else {
|
||||||
return nil
|
return Just(nil).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
return AppContext.shared.blurhashImageCacheService.image(blurhash: blurhash, size: size, url: url)
|
||||||
let imageSize: CGSize = {
|
|
||||||
let aspectRadio = size.width / size.height
|
|
||||||
if size.width > size.height {
|
|
||||||
let width: CGFloat = MosaicMeta.edgeMaxLength
|
|
||||||
let height = width / aspectRadio
|
|
||||||
return CGSize(width: width, height: height)
|
|
||||||
} else {
|
|
||||||
let height: CGFloat = MosaicMeta.edgeMaxLength
|
|
||||||
let width = height * aspectRadio
|
|
||||||
return CGSize(width: width, height: height)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
let image = UIImage(blurHash: blurhash, size: imageSize)
|
|
||||||
|
|
||||||
return image
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// BlurhashImageCacheService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class BlurhashImageCacheService {
|
||||||
|
|
||||||
|
let cache = NSCache<Key, UIImage>()
|
||||||
|
|
||||||
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||||
|
|
||||||
|
func image(blurhash: String, size: CGSize, url: URL) -> AnyPublisher<UIImage?, Never> {
|
||||||
|
return Future { promise in
|
||||||
|
self.workingQueue.async {
|
||||||
|
let key = Key(blurhash: blurhash, size: size, url: url)
|
||||||
|
guard let image = self.cache.object(forKey: key) else {
|
||||||
|
if let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size, url: url) {
|
||||||
|
self.cache.setObject(image, forKey: key)
|
||||||
|
promise(.success(image))
|
||||||
|
} else {
|
||||||
|
promise(.success(nil))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
promise(.success(image))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func blurhashImage(blurhash: String, size: CGSize, url: URL) -> UIImage? {
|
||||||
|
let imageSize: CGSize = {
|
||||||
|
let aspectRadio = size.width / size.height
|
||||||
|
if size.width > size.height {
|
||||||
|
let width: CGFloat = MosaicMeta.edgeMaxLength
|
||||||
|
let height = width / aspectRadio
|
||||||
|
return CGSize(width: width, height: height)
|
||||||
|
} else {
|
||||||
|
let height: CGFloat = MosaicMeta.edgeMaxLength
|
||||||
|
let width = height * aspectRadio
|
||||||
|
return CGSize(width: width, height: height)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
let image = UIImage(blurHash: blurhash, size: imageSize)
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BlurhashImageCacheService {
|
||||||
|
class Key: Hashable {
|
||||||
|
static func == (lhs: BlurhashImageCacheService.Key, rhs: BlurhashImageCacheService.Key) -> Bool {
|
||||||
|
return lhs.blurhash == rhs.blurhash
|
||||||
|
&& lhs.size == rhs.size
|
||||||
|
&& lhs.url == rhs.url
|
||||||
|
}
|
||||||
|
|
||||||
|
let blurhash: String
|
||||||
|
let size: CGSize
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
init(blurhash: String, size: CGSize, url: URL) {
|
||||||
|
self.blurhash = blurhash
|
||||||
|
self.size = size
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(blurhash)
|
||||||
|
hasher.combine(size.width)
|
||||||
|
hasher.combine(size.height)
|
||||||
|
hasher.combine(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
//
|
||||||
|
// PlaceholderImageCacheService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
|
final class PlaceholderImageCacheService {
|
||||||
|
|
||||||
|
let cache = NSCache<Key, UIImage>()
|
||||||
|
|
||||||
|
func image(color: UIColor, size: CGSize, cornerRadius: CGFloat = 0) -> UIImage {
|
||||||
|
let key = Key(color: color, size: size, cornerRadius: cornerRadius)
|
||||||
|
guard let image = cache.object(forKey: key) else {
|
||||||
|
var image = UIImage.placeholder(size: size, color: color)
|
||||||
|
if cornerRadius < size.width * 0.5 {
|
||||||
|
image = image
|
||||||
|
.af.imageAspectScaled(toFill: size)
|
||||||
|
.af.imageRounded(withCornerRadius: cornerRadius, divideRadiusByImageScale: false)
|
||||||
|
} else {
|
||||||
|
image = image.af.imageRoundedIntoCircle()
|
||||||
|
}
|
||||||
|
cache.setObject(image, forKey: key)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlaceholderImageCacheService {
|
||||||
|
class Key: Hashable {
|
||||||
|
let color: UIColor
|
||||||
|
let size: CGSize
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
|
||||||
|
init(color: UIColor, size: CGSize, cornerRadius: CGFloat) {
|
||||||
|
self.color = color
|
||||||
|
self.size = size
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: PlaceholderImageCacheService.Key, rhs: PlaceholderImageCacheService.Key) -> Bool {
|
||||||
|
return lhs.color == rhs.color
|
||||||
|
&& lhs.size == rhs.size
|
||||||
|
&& lhs.cornerRadius == rhs.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(color)
|
||||||
|
hasher.combine(size.width)
|
||||||
|
hasher.combine(size.height)
|
||||||
|
hasher.combine(cornerRadius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,9 @@ class AppContext: ObservableObject {
|
||||||
let blockDomainService: BlockDomainService
|
let blockDomainService: BlockDomainService
|
||||||
let photoLibraryService = PhotoLibraryService()
|
let photoLibraryService = PhotoLibraryService()
|
||||||
|
|
||||||
|
let placeholderImageCacheService = PlaceholderImageCacheService()
|
||||||
|
let blurhashImageCacheService = BlurhashImageCacheService()
|
||||||
|
|
||||||
let documentStore: DocumentStore
|
let documentStore: DocumentStore
|
||||||
private var documentStoreSubscription: AnyCancellable!
|
private var documentStoreSubscription: AnyCancellable!
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import Combine
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
class DocumentStore: ObservableObject {
|
class DocumentStore: ObservableObject {
|
||||||
let blurhashImageCache = NSCache<NSString, NSData>()
|
|
||||||
let appStartUpTimestamp = Date()
|
let appStartUpTimestamp = Date()
|
||||||
var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:]
|
var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue