Merge pull request #9 from tootsuite/feature/sign-up into /develop

Add sign up scene
This commit is contained in:
CMK 2021-02-05 18:19:09 +08:00 committed by GitHub
commit 71a485fc4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1403 additions and 279 deletions

View File

@ -18,7 +18,7 @@ public final class CoreDataStack {
}
public convenience init(databaseName: String = "shared") {
let storeURL = URL.storeURL(for: "group.com.joinmastodon.mastodon-temp", databaseName: databaseName)
let storeURL = URL.storeURL(for: "group.org.joinmastodon.mastodon-temp", databaseName: databaseName)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
self.init(persistentStoreDescriptions: [storeDescription])
}

View File

@ -54,6 +54,8 @@
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
DB0AC70A25CD2E0300D75117 /* HomeViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */; };
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
@ -71,6 +73,10 @@
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -101,6 +107,9 @@
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
DBD4ED1125CC0FEB0041B741 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -205,6 +214,8 @@
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = "<group>"; };
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeViewController+DebugAction.swift"; sourceTree = "<group>"; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -228,6 +239,9 @@
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = "<group>"; };
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -260,6 +274,9 @@
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
@ -274,6 +291,7 @@
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */,
@ -347,6 +365,7 @@
2D42FF8325C82245004A627A /* Button */ = {
isa = PBXGroup;
children = (
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */,
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
);
path = Button;
@ -372,6 +391,7 @@
2D69CFF225CA9E2200C3A1B2 /* Protocol */ = {
isa = PBXGroup;
children = (
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
);
@ -460,6 +480,7 @@
isa = PBXGroup;
children = (
DB0140A625C40C0900F9F3CF /* PinBased */,
DBE0821A25CD382900FD6BBD /* Register */,
DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */,
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */,
);
@ -540,13 +561,14 @@
children = (
DB427DE325BAA00100D1B89D /* Info.plist */,
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
2D69CFF225CA9E2200C3A1B2 /* Protocol */,
2D76319C25C151DE00929FB9 /* Diffiable */,
DB8AF52A25C13561002E6C99 /* State */,
2D61335525C1886800CAE157 /* Service */,
DB8AF55525C1379F002E6C99 /* Scene */,
DB8AF54125C13647002E6C99 /* Coordinator */,
DB8AF56225C138BC002E6C99 /* Extension */,
DB5086CB25CC0DB400C2C187 /* Preference */,
2D69CFF225CA9E2200C3A1B2 /* Protocol */,
DB98338425C945ED00AD9700 /* Generated */,
DB3D0FF825BAA6B200EAA174 /* Resources */,
DB3D0FF725BAA68500EAA174 /* Supporting Files */,
@ -584,6 +606,7 @@
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
);
path = APIService;
sourceTree = "<group>";
@ -598,6 +621,14 @@
path = CoreData;
sourceTree = "<group>";
};
DB5086CB25CC0DB400C2C187 /* Preference */ = {
isa = PBXGroup;
children = (
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */,
);
path = Preference;
sourceTree = "<group>";
};
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
isa = PBXGroup;
children = (
@ -688,10 +719,10 @@
isa = PBXGroup;
children = (
2D7631A425C1532200929FB9 /* Share */,
DB8AF54E25C13703002E6C99 /* MainTab */,
DB01409B25C40BB600F9F3CF /* Authentication */,
2D76316325C14BAC00929FB9 /* PublicTimeline */,
DB8AF54E25C13703002E6C99 /* MainTab */,
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */,
DBD4ED0B25CC0FD40041B741 /* HomeTimeline */,
);
path = Scene;
sourceTree = "<group>";
@ -732,6 +763,25 @@
path = Generated;
sourceTree = "<group>";
};
DBD4ED0B25CC0FD40041B741 /* HomeTimeline */ = {
isa = PBXGroup;
children = (
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */,
DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */,
DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */,
);
path = HomeTimeline;
sourceTree = "<group>";
};
DBE0821A25CD382900FD6BBD /* Register */ = {
isa = PBXGroup;
children = (
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */,
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */,
);
path = Register;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -751,11 +801,11 @@
buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */;
buildPhases = (
7A04933A2AB1D5B758D4F908 /* [CP] Check Pods Manifest.lock */,
DB3D100425BAA71500EAA174 /* ShellScript */,
DB427DCE25BAA00100D1B89D /* Sources */,
DB427DCF25BAA00100D1B89D /* Frameworks */,
DB427DD025BAA00100D1B89D /* Resources */,
5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */,
DB3D100425BAA71500EAA174 /* ShellScript */,
DB89BA0825C10FD0008580ED /* Embed Frameworks */,
);
buildRules = (
@ -770,6 +820,7 @@
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
2D42FF6025C8177C004A627A /* ActiveLabel */,
DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
DB5086B725CC0D6400C2C187 /* Kingfisher */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -896,6 +947,7 @@
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@ -1088,17 +1140,22 @@
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
DB0AC70A25CD2E0300D75117 /* HomeViewController+DebugAction.swift in Sources */,
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
@ -1110,10 +1167,12 @@
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
DBD4ED1125CC0FEB0041B741 /* HomeViewModel.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
@ -1131,6 +1190,7 @@
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
@ -1377,6 +1437,7 @@
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -1401,6 +1462,7 @@
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -1677,6 +1739,14 @@
minimumVersion = 4.1.0;
};
};
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 6.1.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -1704,6 +1774,11 @@
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage;
};
DB5086B725CC0D6400C2C187 /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -7,12 +7,12 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>7</integer>
<integer>8</integer>
</dict>
<key>Mastodon.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -46,6 +46,15 @@
"version": "0.1.1"
}
},
{
"package": "Kingfisher",
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "daebf8ddf974164d1b9a050c8231e263f3106b09",
"version": "6.1.0"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",

View File

@ -39,6 +39,7 @@ extension SceneCoordinator {
enum Scene {
case authentication(viewModel: AuthenticationViewModel)
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
case mastodonRegister(viewModel: MastodonRegisterViewModel)
case alertController(alertController: UIAlertController)
}
@ -120,6 +121,10 @@ private extension SceneCoordinator {
let _viewController = MastodonPinBasedAuthenticationViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonRegister(let viewModel):
let _viewController = MastodonRegisterViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .alertController(let alertController):
if let popoverPresentationController = alertController.popoverPresentationController {
assert(

View File

@ -24,3 +24,9 @@ extension MastodonUser.Property {
)
}
}
extension MastodonUser {
public func avatarImageURL() -> URL? {
return URL(string: avatar)
}
}

View File

@ -26,10 +26,19 @@ internal enum Asset {
internal static let arrowTriangle2Circlepath = ImageAsset(name: "Arrows/arrow.triangle.2.circlepath")
}
internal enum Colors {
internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark")
internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray")
internal static let tootWhite = ColorAsset(name: "Colors/Toot.White")
internal static let likeOrange = ColorAsset(name: "Colors/like.orange")
internal enum Background {
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
}
internal enum Button {
internal static let highlight = ColorAsset(name: "Colors/Button/highlight")
}
internal enum Label {
internal static let primary = ColorAsset(name: "Colors/Label/primary")
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
}
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
}
internal enum ToolBar {
internal static let bookmark = ImageAsset(name: "ToolBar/bookmark")

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@ -42,7 +44,7 @@
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<string>Main</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>

View File

@ -0,0 +1,12 @@
//
// SplashPreference.swift
// Mastodon
//
// Created by Cirno MainasuK on 2020-2-4.
//
import UIKit
extension UserDefaults {
// TODO: splash scene
}

View File

@ -0,0 +1,146 @@
//
// AvatarConfigurableView.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-2-4.
//
import UIKit
import AlamofireImage
import Kingfisher
protocol AvatarConfigurableView {
static var configurableAvatarImageViewSize: CGSize { get }
static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { get }
var configurableAvatarImageView: UIImageView? { get }
var configurableAvatarButton: UIButton? { get }
var configurableVerifiedBadgeImageView: UIImageView? { get }
func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input)
func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration)
}
extension AvatarConfigurableView {
static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { return .mini }
public func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input) {
// TODO: set badge
configurableVerifiedBadgeImageView?.isHidden = true
let cornerRadius = Self.configurableAvatarImageViewSize.width * 0.5
// let scale = (configurableAvatarImageView ?? configurableAvatarButton)?.window?.screen.scale ?? UIScreen.main.scale
let placeholderImage: UIImage = {
let placeholderImage = input.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageViewSize, color: .systemFill)
return placeholderImage.af.imageRoundedIntoCircle()
}()
// cancel previous task
configurableAvatarImageView?.af.cancelImageRequest()
configurableAvatarImageView?.kf.cancelDownloadTask()
configurableAvatarButton?.af.cancelImageRequest(for: .normal)
configurableAvatarButton?.kf.cancelImageDownloadTask()
// reset layer attributes
configurableAvatarImageView?.layer.masksToBounds = false
configurableAvatarImageView?.layer.cornerRadius = 0
configurableAvatarImageView?.layer.cornerCurve = .circular
configurableAvatarButton?.layer.masksToBounds = false
configurableAvatarButton?.layer.cornerRadius = 0
configurableAvatarButton?.layer.cornerCurve = .circular
defer {
let configuration = AvatarConfigurableViewConfiguration(input: input)
avatarConfigurableView(self, didFinishConfiguration: configuration)
}
// set placeholder if no asset
guard let avatarImageURL = input.avatarImageURL else {
configurableAvatarImageView?.image = placeholderImage
configurableAvatarButton?.setImage(placeholderImage, for: .normal)
return
}
if let avatarImageView = configurableAvatarImageView {
// set avatar (GIF using Kingfisher)
switch avatarImageURL.pathExtension {
case "gif":
avatarImageView.kf.setImage(
with: avatarImageURL,
placeholder: placeholderImage,
options: [
.transition(.fade(0.2))
]
)
avatarImageView.layer.masksToBounds = true
avatarImageView.layer.cornerRadius = cornerRadius
avatarImageView.layer.cornerCurve = .circular
default:
let filter = ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize)
avatarImageView.af.setImage(
withURL: avatarImageURL,
placeholderImage: placeholderImage,
filter: filter,
imageTransition: .crossDissolve(0.3),
runImageTransitionIfCached: false,
completion: nil
)
}
}
if let avatarButton = configurableAvatarButton {
switch avatarImageURL.pathExtension {
case "gif":
avatarButton.kf.setImage(
with: avatarImageURL,
for: .normal,
placeholder: placeholderImage,
options: [
.transition(.fade(0.2))
]
)
avatarButton.layer.masksToBounds = true
avatarButton.layer.cornerRadius = cornerRadius
avatarButton.layer.cornerCurve = .circular
default:
let filter = ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize)
avatarButton.af.setImage(
for: .normal,
url: avatarImageURL,
placeholderImage: placeholderImage,
filter: filter,
completion: nil
)
}
}
}
func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) { }
}
struct AvatarConfigurableViewConfiguration {
enum BadgeAppearanceStyle {
case mini
case normal
}
struct Input {
let avatarImageURL: URL?
let placeholderImage: UIImage?
let blocked: Bool
let verified: Bool
init(avatarImageURL: URL?, placeholderImage: UIImage? = nil, blocked: Bool = false, verified: Bool = false) {
self.avatarImageURL = avatarImageURL
self.placeholderImage = placeholderImage
self.blocked = blocked
self.verified = verified
}
}
let input: Input
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x37",
"green" : "0x2D",
"red" : "0x29"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2B",
"green" : "0x24",
"red" : "0x20"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x43",
"green" : "0x36",
"red" : "0x32"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.839",
"green" : "0.573",
"red" : "0.204"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x84",
"green" : "0x69",
"red" : "0x60"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "extended-srgb",
"components" : {
"alpha" : "1.000",
"blue" : "55",
"green" : "45",
"red" : "41"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "55",
"green" : "45",
"red" : "41"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "132",
"green" : "105",
"red" : "96"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "132",
"green" : "105",
"red" : "96"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,41 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"localizable" : true
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "10",
"green" : "159",
"red" : "255"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "10",
"green" : "159",
"red" : "255"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x0A",
"green" : "0x9F",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import Combine
import MastodonSDK
import UITextField_Shake
final class AuthenticationViewController: UIViewController, NeedsDependency {
@ -19,6 +20,14 @@ final class AuthenticationViewController: UIViewController, NeedsDependency {
var viewModel: AuthenticationViewModel!
let domainLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Domain:"
return label
}()
let domainTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "example.com"
@ -28,8 +37,39 @@ final class AuthenticationViewController: UIViewController, NeedsDependency {
return textField
}()
private(set) lazy var signInBarButtonItem = UIBarButtonItem(title: "Sign In", style: .plain, target: self, action: #selector(AuthenticationViewController.signInBarButtonItemPressed(_:)))
let activityIndicatorBarButtonItem = UIBarButtonItem.activityIndicatorBarButtonItem
let signInButton: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal)
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled)
button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
button.setTitle("Sign in", for: .normal)
button.layer.masksToBounds = true
button.layer.cornerRadius = 8
button.layer.cornerCurve = .continuous
return button
}()
let signUpButton: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = .preferredFont(forTextStyle: .subheadline)
button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal)
button.setTitleColor(.systemGray, for: .disabled)
button.setTitle("Sign up", for: .normal)
return button
}()
let signInActivityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
activityIndicatorView.hidesWhenStopped = true
return activityIndicatorView
}()
let signUpActivityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
activityIndicatorView.hidesWhenStopped = true
return activityIndicatorView
}()
}
extension AuthenticationViewController {
@ -38,16 +78,54 @@ extension AuthenticationViewController {
super.viewDidLoad()
title = "Authentication"
view.backgroundColor = .systemBackground
navigationItem.rightBarButtonItem = signInBarButtonItem
view.backgroundColor = Asset.Colors.Background.systemBackground.color
domainLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(domainLabel)
NSLayoutConstraint.activate([
domainLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16),
domainLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
domainLabel.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
])
domainTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(domainTextField)
NSLayoutConstraint.activate([
domainTextField.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 8),
domainTextField.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 8),
domainTextField.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: 8),
domainTextField.heightAnchor.constraint(equalToConstant: 44), // FIXME:
domainTextField.topAnchor.constraint(equalTo: domainLabel.bottomAnchor, constant: 8),
domainTextField.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
domainTextField.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
])
signInButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(signInButton)
NSLayoutConstraint.activate([
signInButton.topAnchor.constraint(equalTo: domainTextField.bottomAnchor, constant: 20),
signInButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
signInButton.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
signInButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
])
signInActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(signInActivityIndicatorView)
NSLayoutConstraint.activate([
signInActivityIndicatorView.centerXAnchor.constraint(equalTo: signInButton.centerXAnchor),
signInActivityIndicatorView.centerYAnchor.constraint(equalTo: signInButton.centerYAnchor),
])
signUpButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(signUpButton)
NSLayoutConstraint.activate([
signUpButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 8),
signUpButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
signUpButton.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
])
signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(signUpActivityIndicatorView)
NSLayoutConstraint.activate([
signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor),
signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor),
])
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: domainTextField)
@ -62,10 +140,30 @@ extension AuthenticationViewController {
.receive(on: DispatchQueue.main)
.sink { [weak self] isAuthenticating in
guard let self = self else { return }
self.navigationItem.rightBarButtonItem = isAuthenticating ? self.activityIndicatorBarButtonItem : self.signInBarButtonItem
isAuthenticating ? self.signInActivityIndicatorView.startAnimating() : self.signInActivityIndicatorView.stopAnimating()
self.signInButton.setTitle(isAuthenticating ? "" : "Sign in", for: .normal)
}
.store(in: &disposeBag)
viewModel.isRegistering
.receive(on: DispatchQueue.main)
.sink { [weak self] isRegistering in
guard let self = self else { return }
isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating()
self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal)
}
.store(in: &disposeBag)
viewModel.isIdle
.receive(on: DispatchQueue.main)
.sink { [weak self] isIdle in
guard let self = self else { return }
self.signInButton.isEnabled = isIdle
self.signUpButton.isEnabled = isIdle
}
.store(in: &disposeBag)
viewModel.authenticated
.receive(on: DispatchQueue.main)
.sink { [weak self] domain, user in
@ -91,11 +189,6 @@ extension AuthenticationViewController {
}
.store(in: &disposeBag)
viewModel.isSignInButtonEnabled
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: signInBarButtonItem)
.store(in: &disposeBag)
viewModel.error
.compactMap { $0 }
.receive(on: DispatchQueue.main)
@ -111,6 +204,10 @@ extension AuthenticationViewController {
)
}
.store(in: &disposeBag)
signInButton.addTarget(self, action: #selector(AuthenticationViewController.signInButtonPressed(_:)), for: .touchUpInside)
signUpButton.addTarget(self, action: #selector(AuthenticationViewController.signUpButtonPressed(_:)), for: .touchUpInside)
}
override func viewWillAppear(_ animated: Bool) {
@ -123,29 +220,21 @@ extension AuthenticationViewController {
extension AuthenticationViewController {
@objc private func signInBarButtonItemPressed(_ sender: UIBarButtonItem) {
@objc private func signInButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let domain = viewModel.domain.value else {
// TODO: alert error
guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else {
domainTextField.shake()
return
}
guard !viewModel.isAuthenticating.value else { return }
guard viewModel.isIdle.value else { return }
viewModel.isAuthenticating.value = true
context.apiService.createApplication(domain: domain)
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
let application = response.value
guard let clientID = application.clientID,
let clientSecret = application.clientSecret else {
guard let info = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else {
throw APIService.APIError.explicit(.badResponse)
}
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
return AuthenticationViewModel.AuthenticateInfo(
domain: domain,
clientID: clientID,
clientSecret: clientSecret,
url: url
)
return info
}
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
@ -162,7 +251,7 @@ extension AuthenticationViewController {
}
} receiveValue: { [weak self] info in
guard let self = self else { return }
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.url)
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL)
self.viewModel.authenticate(
info: info,
pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher
@ -176,6 +265,54 @@ extension AuthenticationViewController {
.store(in: &disposeBag)
}
@objc private func signUpButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else {
domainTextField.shake()
return
}
guard viewModel.isIdle.value else { return }
viewModel.isRegistering.value = true
context.apiService.instance(domain: domain)
.compactMap { [weak self] response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
}
return self.context.apiService.createApplication(domain: domain)
}
.switchToLatest()
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
let application = response.value
guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else {
throw APIService.APIError.explicit(.badResponse)
}
return authenticateInfo
}
.compactMap { [weak self] authenticateInfo -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error>? in
guard let self = self else { return nil }
return self.context.apiService.applicationAccessToken(domain: domain, clientID: authenticateInfo.clientID, clientSecret: authenticateInfo.clientSecret)
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
self.viewModel.isRegistering.value = false
switch completion {
case .failure(let error):
self.viewModel.error.send(error)
case .finished:
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
let mastodonRegisterViewModel = MastodonRegisterViewModel(domain: domain, applicationToken: response.value)
self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show)
}
.store(in: &disposeBag)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate

View File

@ -25,8 +25,10 @@ final class AuthenticationViewModel {
// output
let viewHierarchyShouldReset: Bool
let domain = CurrentValueSubject<String?, Never>(nil)
let isSignInButtonEnabled = CurrentValueSubject<Bool, Never>(false)
let isDomainValid = CurrentValueSubject<Bool, Never>(false)
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
let isRegistering = CurrentValueSubject<Bool, Never>(false)
let isIdle = CurrentValueSubject<Bool, Never>(true)
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
let error = CurrentValueSubject<Error?, Never>(nil)
@ -59,21 +61,70 @@ final class AuthenticationViewModel {
.assign(to: \.value, on: domain)
.store(in: &disposeBag)
Publishers.CombineLatest(
isAuthenticating.eraseToAnyPublisher(),
isRegistering.eraseToAnyPublisher()
)
.map { !$0 && !$1 }
.assign(to: \.value, on: self.isIdle)
.store(in: &disposeBag)
domain
.map { $0 != nil }
.assign(to: \.value, on: isSignInButtonEnabled)
.assign(to: \.value, on: isDomainValid)
.store(in: &disposeBag)
}
}
extension AuthenticationViewModel {
enum AuthenticationError: Error, LocalizedError {
case badCredentials
case registrationClosed
var errorDescription: String? {
switch self {
case .badCredentials: return "Bad Credentials"
case .registrationClosed: return "Registration Closed"
}
}
var failureReason: String? {
switch self {
case .badCredentials: return "Credentials invalid."
case .registrationClosed: return "Server disallow registration."
}
}
var helpAnchor: String? {
switch self {
case .badCredentials: return "Please try again."
case .registrationClosed: return "Please try another domain."
}
}
}
}
extension AuthenticationViewModel {
struct AuthenticateInfo {
let domain: String
let clientID: String
let clientSecret: String
let url: URL
let authorizeURL: URL
init?(domain: String, application: Mastodon.Entity.Application) {
self.domain = domain
guard let clientID = application.clientID,
let clientSecret = application.clientSecret else { return nil }
self.clientID = clientID
self.clientSecret = clientSecret
self.authorizeURL = {
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
return url
}()
}
}
func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject<String, Never>) {
@ -144,7 +195,7 @@ extension AuthenticationViewModel {
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
mastodonUserRequest.fetchLimit = 1
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
return Fail(error: APIService.APIError.explicit(.badCredentials)).eraseToAnyPublisher()
return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher()
}
let property = MastodonAuthentication.Property(

View File

@ -0,0 +1,227 @@
//
// MastodonRegisterViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-2-5.
//
import os.log
import UIKit
import Combine
import MastodonSDK
import UITextField_Shake
final class MastodonRegisterViewController: UIViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: MastodonRegisterViewModel!
let usernameLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Username:"
return label
}()
let usernameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Username"
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
return textField
}()
let emailLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Email:"
return label
}()
let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "example@gmail.com"
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.keyboardType = .emailAddress
return textField
}()
let passwordLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Password:"
return label
}()
let passwordTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Password"
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.keyboardType = .asciiCapable
textField.isSecureTextEntry = true
return textField
}()
let signUpButton: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal)
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled)
button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
button.setTitle("Sign up", for: .normal)
button.layer.masksToBounds = true
button.layer.cornerRadius = 8
button.layer.cornerCurve = .continuous
return button
}()
let signUpActivityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
activityIndicatorView.hidesWhenStopped = true
return activityIndicatorView
}()
}
extension MastodonRegisterViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Sign Up"
view.backgroundColor = Asset.Colors.Background.systemBackground.color
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16),
stackView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
])
stackView.axis = .vertical
stackView.spacing = 8
stackView.addArrangedSubview(usernameLabel)
stackView.addArrangedSubview(usernameTextField)
stackView.addArrangedSubview(emailLabel)
stackView.addArrangedSubview(emailTextField)
stackView.addArrangedSubview(passwordLabel)
stackView.addArrangedSubview(passwordTextField)
signUpButton.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(signUpButton)
NSLayoutConstraint.activate([
signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
])
signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(signUpActivityIndicatorView)
NSLayoutConstraint.activate([
signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor),
signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor),
])
viewModel.isRegistering
.receive(on: DispatchQueue.main)
.sink { [weak self] isRegistering in
guard let self = self else { return }
isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating()
self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal)
self.signUpButton.isEnabled = !isRegistering
}
.store(in: &disposeBag)
viewModel.error
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
guard let self = self else { return }
let alertController = UIAlertController(error, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(okAction)
self.coordinator.present(
scene: .alertController(alertController: alertController),
from: nil,
transition: .alertController(animated: true, completion: nil)
)
}
.store(in: &disposeBag)
signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
}
}
extension MastodonRegisterViewController {
@objc private func signUpButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let username = usernameTextField.text else {
usernameTextField.shake()
return
}
guard let email = emailTextField.text else {
emailTextField.shake()
return
}
guard let password = passwordTextField.text else {
passwordTextField.shake()
return
}
guard !viewModel.isRegistering.value else { return }
viewModel.isRegistering.value = true
let query = Mastodon.API.Account.RegisterQuery(
username: username,
email: email,
password: password,
agreement: true, // TODO:
locale: "en" // TODO:
)
context.apiService.accountRegister(
domain: viewModel.domain,
query: query,
authorization: viewModel.applicationAuthorization
)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
self.viewModel.isRegistering.value = false
switch completion {
case .failure(let error):
self.viewModel.error.send(error)
case .finished:
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
let _ = response.value
// TODO:
let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
guard let self = self else { return }
self.navigationController?.popViewController(animated: true)
}
alertController.addAction(okAction)
self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
}
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,29 @@
//
// MastodonRegisterViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-2-5.
//
import Foundation
import Combine
import MastodonSDK
final class MastodonRegisterViewModel {
// input
let domain: String
let applicationToken: Mastodon.Entity.Token
let isRegistering = CurrentValueSubject<Bool, Never>(false)
// output
let applicationAuthorization: Mastodon.API.OAuth.Authorization
let error = CurrentValueSubject<Error?, Never>(nil)
init(domain: String, applicationToken: Mastodon.Entity.Token) {
self.domain = domain
self.applicationToken = applicationToken
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken)
}
}

View File

@ -0,0 +1,63 @@
//
// HomeViewController+DebugAction.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-2-5.
//
import os.log
import UIKit
#if DEBUG
extension HomeViewController {
var debugMenu: UIMenu {
let menu = UIMenu(
title: "Debug Tools",
image: nil,
identifier: nil,
options: .displayInline,
children: [
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
guard let self = self else { return }
self.signOutAction(action)
}
]
)
return menu
}
}
extension HomeViewController {
@objc private func signOutAction(_ sender: UIAction) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
let currentAccountCount = context.authenticationService.mastodonAuthentications.value.count
let isAuthenticationExistWhenSignOut = currentAccountCount - 1 > 0
// prepare advance
let authenticationViewModel = AuthenticationViewModel(context: context, coordinator: coordinator, isAuthenticationExist: isAuthenticationExistWhenSignOut)
context.authenticationService.signOutMastodonUser(
domain: activeMastodonAuthenticationBox.domain,
userID: activeMastodonAuthenticationBox.userID
)
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .success(let isSignOut):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
guard isSignOut else { return }
if !isAuthenticationExistWhenSignOut {
self.coordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: true, completion: nil))
}
}
}
.store(in: &disposeBag)
}
}
#endif

View File

@ -0,0 +1,71 @@
//
// HomeViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/1/27.
//
import os.log
import UIKit
import Combine
final class HomeViewController: UIViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: HomeViewModel!
let avatarBarButtonItem = AvatarBarButtonItem()
}
extension HomeViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Home"
view.backgroundColor = Asset.Colors.Background.systemBackground.color
navigationItem.leftBarButtonItem = avatarBarButtonItem
avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(HomeViewController.avatarBarButtonItemDidPressed(_:)), for: .touchUpInside)
#if DEBUG
avatarBarButtonItem.avatarButton.menu = debugMenu
avatarBarButtonItem.avatarButton.showsMenuAsPrimaryAction = true
#endif
Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthentication.eraseToAnyPublisher(),
viewModel.viewDidAppear.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] activeMastodonAuthentication, _ in
guard let self = self else { return }
guard let user = activeMastodonAuthentication?.user,
let avatarImageURL = user.avatarImageURL() else {
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: nil)
self.avatarBarButtonItem.configure(withConfigurationInput: input)
return
}
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: avatarImageURL)
self.avatarBarButtonItem.configure(withConfigurationInput: input)
}
.store(in: &disposeBag)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.viewDidAppear.send()
}
}
extension HomeViewController {
@objc private func avatarBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}

View File

@ -0,0 +1,24 @@
//
// HomeViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-2-4.
//
import UIKit
import Combine
final class HomeViewModel {
// input
let context: AppContext
let viewDidAppear = PassthroughSubject<Void, Never>()
// output
init(context: AppContext) {
self.context = context
}
}

View File

@ -1,26 +0,0 @@
//
// HomeViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/1/27.
//
import UIKit
final class HomeViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
}
extension HomeViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Home"
view.backgroundColor = .systemBackground
}
}

View File

@ -40,6 +40,7 @@ class MainTabBarController: UITabBarController {
switch self {
case .home:
let _viewController = HomeViewController()
_viewController.viewModel = HomeViewModel(context: context)
_viewController.context = context
_viewController.coordinator = coordinator
viewController = _viewController

View File

@ -29,6 +29,7 @@ final class PublicTimelineViewController: UIViewController, NeedsDependency, Tim
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
return tableView
}()
@ -41,6 +42,8 @@ extension PublicTimelineViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Asset.Colors.Background.systemBackground.color
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(PublicTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
// bind refresh control
@ -58,10 +61,7 @@ extension PublicTimelineViewController {
.store(in: &disposeBag)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.backgroundColor = Asset.Colors.tootDark.color
view.addSubview(tableView)
view.backgroundColor = Asset.Colors.tootDark.color
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),

View File

@ -0,0 +1,49 @@
//
// AvatarBarButtonItem.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-2-4.
//
import UIKit
final class AvatarBarButtonItem: UIBarButtonItem {
static let avatarButtonSize = CGSize(width: 32, height: 32)
let avatarButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: avatarButtonSize.width).priority(.defaultHigh),
button.heightAnchor.constraint(equalToConstant: avatarButtonSize.height).priority(.defaultHigh),
])
return button
}()
override init() {
super.init()
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension AvatarBarButtonItem {
private func _init() {
customView = avatarButton
}
}
extension AvatarBarButtonItem: AvatarConfigurableView {
static var configurableAvatarImageViewSize: CGSize { return avatarButtonSize }
var configurableAvatarImageView: UIImageView? { return nil }
var configurableAvatarButton: UIButton? { return avatarButton }
var configurableVerifiedBadgeImageView: UIImageView? { return nil }
}

View File

@ -24,13 +24,13 @@ final class TimelinePostView: UIView {
let visibilityImageView: UIImageView = {
let imageView = UIImageView(image: Asset.TootTimeline.global.image.withRenderingMode(.alwaysTemplate))
imageView.tintColor = Asset.Colors.tootGray.color
imageView.tintColor = Asset.Colors.Label.secondary.color
return imageView
}()
let lockImageView: UIImageView = {
let imageview = UIImageView(image: Asset.TootTimeline.textlock.image.withRenderingMode(.alwaysTemplate))
imageview.tintColor = Asset.Colors.tootGray.color
imageview.tintColor = Asset.Colors.Label.secondary.color
imageview.isHidden = true
return imageview
}()
@ -38,7 +38,7 @@ final class TimelinePostView: UIView {
let nameLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "Roboto-Medium", size: 14)
label.textColor = Asset.Colors.tootWhite.color
label.textColor = Asset.Colors.Label.primary.color
label.text = "Alice"
return label
@ -46,7 +46,7 @@ final class TimelinePostView: UIView {
let usernameLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.tootGray.color
label.textColor = Asset.Colors.Label.secondary.color
label.font = UIFont(name: "Roboto-Regular", size: 14)
label.text = "@alice"
return label
@ -56,7 +56,7 @@ final class TimelinePostView: UIView {
let label = UILabel()
label.font = UIFont(name: "Roboto-Regular", size: 14)
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .left : .right
label.textColor = Asset.Colors.tootGray.color
label.textColor = Asset.Colors.Label.secondary.color
label.text = "1d"
return label
}()

View File

@ -11,7 +11,8 @@ import Combine
final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell {
override func _init() {
super._init()
backgroundColor = .clear
activityIndicatorView.isHidden = false
activityIndicatorView.startAnimating()
}

View File

@ -45,7 +45,8 @@ class TimelineLoaderTableViewCell: UITableViewCell {
func _init() {
selectionStyle = .none
backgroundColor = Asset.Colors.tootDark.color
backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(loadMoreButton)
NSLayoutConstraint.activate([

View File

@ -21,7 +21,7 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell {
override func _init() {
super._init()
backgroundColor = Asset.Colors.tootDark.color
backgroundColor = .clear
let separatorLine = UIView.separatorLine
separatorLine.translatesAutoresizingMaskIntoConstraints = false

View File

@ -50,7 +50,7 @@ final class TimelinePostTableViewCell: UITableViewCell {
extension TimelinePostTableViewCell {
private func _init() {
self.backgroundColor = Asset.Colors.tootDark.color
self.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
self.selectionStyle = .none
timelinePostView.translatesAutoresizingMaskIntoConstraints = false
timelinePostViewTopLayoutConstraint = timelinePostView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TimelinePostTableViewCell.verticalMargin)

View File

@ -95,7 +95,7 @@ extension ActionToolbarContainer {
let buttons = [replyButton, retootButton, starButton,bookmartButton, moreButton]
buttons.forEach { button in
button.tintColor = Asset.Colors.tootGray.color
button.tintColor = Asset.Colors.Label.secondary.color
button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
button.setTitle("", for: .normal)
button.setTitleColor(.secondaryLabel, for: .normal)
@ -165,7 +165,7 @@ extension ActionToolbarContainer {
}
private func isstarButtonHighlightStateDidChange(to isHighlight: Bool) {
let tintColor = isHighlight ? Asset.Colors.likeOrange.color : Asset.Colors.tootGray.color
let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Label.secondary.color
starButton.tintColor = tintColor
starButton.setTitleColor(tintColor, for: .normal)
starButton.setTitleColor(tintColor, for: .highlighted)

View File

@ -17,7 +17,6 @@ extension APIService {
enum ErrorReason {
// application internal error
case authenticationMissing
case badCredentials
case badRequest
case badResponse
case requestThrottle
@ -42,7 +41,6 @@ extension APIService.APIError: LocalizedError {
var errorDescription: String? {
switch errorReason {
case .authenticationMissing: return "Fail to Authenticatie"
case .badCredentials: return "Bad Credentials"
case .badRequest: return "Bad Request"
case .badResponse: return "Bad Response"
case .requestThrottle: return "Request Throttled"
@ -61,7 +59,6 @@ extension APIService.APIError: LocalizedError {
var failureReason: String? {
switch errorReason {
case .authenticationMissing: return "Account credential not found."
case .badCredentials: return "Credentials invalid."
case .badRequest: return "Request invalid."
case .badResponse: return "Response invalid."
case .requestThrottle: return "Request too frequency."
@ -76,7 +73,6 @@ extension APIService.APIError: LocalizedError {
var helpAnchor: String? {
switch errorReason {
case .authenticationMissing: return "Please request after authenticated."
case .badCredentials: return "Please try again.."
case .badRequest: return "Please try again."
case .badResponse: return "Please try again."
case .requestThrottle: return "Please try again later."

View File

@ -43,4 +43,17 @@ extension APIService {
.eraseToAnyPublisher()
}
func accountRegister(
domain: String,
query: Mastodon.API.Account.RegisterQuery,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
return Mastodon.API.Account.register(
session: session,
domain: domain,
query: query,
authorization: authorization
)
}
}

View File

@ -31,5 +31,23 @@ extension APIService {
query: query
)
}
func applicationAccessToken(
domain: String,
clientID: String,
clientSecret: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
let query = Mastodon.API.OAuth.AccessTokenQuery(
clientID: clientID,
clientSecret: clientSecret,
code: nil,
grantType: "client_credentials"
)
return Mastodon.API.OAuth.accessToken(
session: session,
domain: domain,
query: query
)
}
}

View File

@ -22,7 +22,7 @@ extension APIService {
limit: Int = 100,
local: Bool? = nil,
authorizationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let authorization = authorizationBox.userAuthorization
let requestMastodonUserID = authorizationBox.userID
let query = Mastodon.API.Timeline.HomeTimelineQuery(
@ -39,7 +39,7 @@ extension APIService {
query: query,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
return APIService.Persist.persistTimeline(
managedObjectContext: self.backgroundManagedObjectContext,
domain: domain,
@ -50,7 +50,7 @@ extension APIService {
log: OSLog.api
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
switch result {
case .success:
return response

View File

@ -0,0 +1,24 @@
//
// APIService+Instance.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-2-5.
//
import Foundation
import Combine
import CoreData
import CoreDataStack
import CommonOSLog
import DateToolsSwift
import MastodonSDK
extension APIService {
func instance(
domain: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Instance>, Error> {
return Mastodon.API.Instance.instance(session: session, domain: domain)
}
}

View File

@ -22,7 +22,7 @@ extension APIService {
sinceID: Mastodon.Entity.Status.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
limit: Int = 100
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let query = Mastodon.API.Timeline.PublicTimelineQuery(
local: nil,
remote: nil,
@ -38,7 +38,7 @@ extension APIService {
domain: domain,
query: query
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
return APIService.Persist.persistTimeline(
managedObjectContext: self.backgroundManagedObjectContext,
domain: domain,
@ -49,7 +49,7 @@ extension APIService {
log: OSLog.api
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
switch result {
case .success:
return response

View File

@ -16,7 +16,7 @@ extension APIService.CoreData {
static func createOrMergeToot(
into managedObjectContext: NSManagedObjectContext,
for requestMastodonUser: MastodonUser?,
entity: Mastodon.Entity.Toot,
entity: Mastodon.Entity.Status,
domain: String,
networkDate: Date,
log: OSLog
@ -83,7 +83,7 @@ extension APIService.CoreData {
}
}
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Toot, networkDate: Date) {
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Status, networkDate: Date) {
guard networkDate > toot.updatedAt else { return }
// merge

View File

@ -24,7 +24,7 @@ extension APIService.Persist {
managedObjectContext: NSManagedObjectContext,
domain: String,
query: Mastodon.API.Timeline.TimelineQuery,
response: Mastodon.Response.Content<[Mastodon.Entity.Toot]>,
response: Mastodon.Response.Content<[Mastodon.Entity.Status]>,
persistType: PersistTimelineType,
requestMastodonUserID: MastodonUser.ID?, // could be nil when response from public endpoint
log: OSLog

View File

@ -12,6 +12,18 @@
]
}
},
{
"id" : "9DA3EAE9-A502-49E9-BC74-5ED71250F4A8",
"name" : "mastodon.social",
"options" : {
"environmentVariableEntries" : [
{
"key" : "domain",
"value" : "mastodon.social"
}
]
}
},
{
"id" : "C5184AF3-B83B-4A7E-949C-6B1AA3ABE7D1",
"name" : "pawoo.net",
@ -23,6 +35,18 @@
}
]
}
},
{
"id" : "71C5BF3D-A8CE-468B-B22E-E6DA4AD5AF4E",
"name" : "freespeechextremist.com",
"options" : {
"environmentVariableEntries" : [
{
"key" : "domain",
"value" : "freespeechextremist.com"
}
]
}
}
],
"defaultOptions" : {
@ -32,6 +56,7 @@
{
"skippedTests" : [
"MastodonSDKTests\/testCreateAnAnpplication()",
"MastodonSDKTests\/testHomeTimeline()",
"MastodonSDKTests\/testVerifyAppCredentials()"
],
"target" : {

View File

@ -13,6 +13,9 @@ extension Mastodon.API.Account {
static func verifyCredentialsEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/verify_credentials")
}
static func registerEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts")
}
public static func verifyCredentials(
session: URLSession,
@ -31,4 +34,50 @@ extension Mastodon.API.Account {
}
.eraseToAnyPublisher()
}
public static func register(
session: URLSession,
domain: String,
query: RegisterQuery,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
let request = Mastodon.API.post(
url: registerEndpointURL(domain: domain),
query: query,
authorization: authorization
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}
extension Mastodon.API.Account {
public struct RegisterQuery: Codable, PostQuery {
public let reason: String?
public let username: String
public let email: String
public let password: String
public let agreement: Bool
public let locale: String
public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) {
self.reason = reason
self.username = username
self.email = email
self.password = password
self.agreement = agreement
self.locale = locale
}
var body: Data? {
return try? Mastodon.API.encoder.encode(self)
}
}
}

View File

@ -0,0 +1,46 @@
//
// Mastodon+API+Instance.swift
//
//
// Created by MainasuK Cirno on 2021-2-5.
//
import Foundation
import Combine
extension Mastodon.API.Instance {
static func instanceEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("instance")
}
/// Information about the server
///
/// - Since: 1.0.0
/// - Version: 3.3.0
/// # Last Update
/// 2021/2/5
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/instance/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - Returns: `AnyPublisher` contains `Instance` nested in the response
public static func instance(
session: URLSession,
domain: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Instance>, Error> {
let request = Mastodon.API.get(
url: instanceEndpointURL(domain: domain),
query: nil,
authorization: nil
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: Mastodon.Entity.Instance.self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}

View File

@ -21,7 +21,7 @@ extension Mastodon.API.Timeline {
session: URLSession,
domain: String,
query: PublicTimelineQuery
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let request = Mastodon.API.get(
url: publicTimelineEndpointURL(domain: domain),
query: query,
@ -29,7 +29,7 @@ extension Mastodon.API.Timeline {
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Status].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
@ -40,7 +40,7 @@ extension Mastodon.API.Timeline {
domain: String,
query: HomeTimelineQuery,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let request = Mastodon.API.get(
url: homeTimelineEndpointURL(domain: domain),
query: query,
@ -48,7 +48,7 @@ extension Mastodon.API.Timeline {
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Status].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
@ -57,8 +57,8 @@ extension Mastodon.API.Timeline {
}
public protocol TimelineQueryType {
var maxID: Mastodon.Entity.Toot.ID? { get }
var sinceID: Mastodon.Entity.Toot.ID? { get }
var maxID: Mastodon.Entity.Status.ID? { get }
var sinceID: Mastodon.Entity.Status.ID? { get }
}
extension Mastodon.API.Timeline {
@ -70,18 +70,18 @@ extension Mastodon.API.Timeline {
public let local: Bool?
public let remote: Bool?
public let onlyMedia: Bool?
public let maxID: Mastodon.Entity.Toot.ID?
public let sinceID: Mastodon.Entity.Toot.ID?
public let minID: Mastodon.Entity.Toot.ID?
public let maxID: Mastodon.Entity.Status.ID?
public let sinceID: Mastodon.Entity.Status.ID?
public let minID: Mastodon.Entity.Status.ID?
public let limit: Int?
public init(
local: Bool? = nil,
remote: Bool? = nil,
onlyMedia: Bool? = nil,
maxID: Mastodon.Entity.Toot.ID? = nil,
sinceID: Mastodon.Entity.Toot.ID? = nil,
minID: Mastodon.Entity.Toot.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
sinceID: Mastodon.Entity.Status.ID? = nil,
minID: Mastodon.Entity.Status.ID? = nil,
limit: Int? = nil
) {
self.local = local
@ -108,16 +108,16 @@ extension Mastodon.API.Timeline {
}
public struct HomeTimelineQuery: Codable, TimelineQuery, GetQuery {
public let maxID: Mastodon.Entity.Toot.ID?
public let sinceID: Mastodon.Entity.Toot.ID?
public let minID: Mastodon.Entity.Toot.ID?
public let maxID: Mastodon.Entity.Status.ID?
public let sinceID: Mastodon.Entity.Status.ID?
public let minID: Mastodon.Entity.Status.ID?
public let limit: Int?
public let local: Bool?
public init(
maxID: Mastodon.Entity.Toot.ID? = nil,
sinceID: Mastodon.Entity.Toot.ID? = nil,
minID: Mastodon.Entity.Toot.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
sinceID: Mastodon.Entity.Status.ID? = nil,
minID: Mastodon.Entity.Status.ID? = nil,
limit: Int? = nil,
local: Bool? = nil
) {

View File

@ -84,6 +84,7 @@ extension Mastodon.API {
extension Mastodon.API {
public enum Account { }
public enum App { }
public enum Instance { }
public enum OAuth { }
public enum Timeline { }
}

View File

@ -13,7 +13,7 @@ extension Mastodon.Entity {
/// - Since: 1.1.0
/// - Version: 3.3.0
/// # Last Update
/// 2021/1/28
/// 2021/2/5
/// # Reference
/// [Document](https://docs.joinmastodon.org/entities/instance/)
public struct Instance: Codable {
@ -28,7 +28,7 @@ extension Mastodon.Entity {
public let registrations: Bool?
public let approvalRequired: Bool?
public let invitesEnabled: Bool?
public let urls: [InstanceURL]?
public let urls: InstanceURL?
public let statistics: Statistics?
public let thumbnail: String?

View File

@ -8,9 +8,7 @@
import Foundation
extension Mastodon.Entity {
public typealias Toot = Status
/// Status
///
/// - Since: 0.1.0

View File

@ -0,0 +1,41 @@
//
// MastodonSDK+API+Instance.swift
//
//
// Created by MainasuK Cirno on 2021-2-5.
//
import os.log
import XCTest
import Combine
@testable import MastodonSDK
extension MastodonSDKTests {
func testInstance() throws {
try _testInstance(domain: domain)
}
func _testInstance(domain: String) throws {
let theExpectation = expectation(description: "Fetch Instance Infomation")
Mastodon.API.Instance.instance(session: session, domain: domain)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
XCTFail(error.localizedDescription)
case .finished:
break
}
} receiveValue: { response in
XCTAssertNotEqual(response.value.uri, "")
print(response.value)
theExpectation.fulfill()
}
.store(in: &disposeBag)
wait(for: [theExpectation], timeout: 10.0)
}
}

View File

@ -6,10 +6,14 @@ target 'Mastodon' do
# Pods for Mastodon
# misc
# UI
pod 'UITextField+Shake', '~> 1.2'
# misc
pod 'SwiftGen', '~> 6.4.0'
pod 'DateToolsSwift', '~> 5.0.0'
pod 'Kanna', '~> 5.2.2'
target 'MastodonTests' do
inherit! :search_paths
# Pods for testing

View File

@ -2,23 +2,27 @@ PODS:
- DateToolsSwift (5.0.0)
- Kanna (5.2.4)
- SwiftGen (6.4.0)
- "UITextField+Shake (1.2.1)"
DEPENDENCIES:
- DateToolsSwift (~> 5.0.0)
- Kanna (~> 5.2.2)
- SwiftGen (~> 6.4.0)
- "UITextField+Shake (~> 1.2)"
SPEC REPOS:
trunk:
- DateToolsSwift
- Kanna
- SwiftGen
- "UITextField+Shake"
SPEC CHECKSUMS:
DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
PODFILE CHECKSUM: 8b24099ae9ac02698d464cc508af9550352c85cb
PODFILE CHECKSUM: 30e8e3a555251a512e7b5e91183747152f126e7a
COCOAPODS: 1.10.1

View File

@ -43,13 +43,14 @@ arch -x86_64 pod install
## Acknowledgements
- [ActiveLabel](https://github.com/optonaut/ActiveLabel.swift)
- [ActiveLabel](https://github.com/TwidereProject/ActiveLabel.swift)
- [AlamofireImage](https://github.com/Alamofire/AlamofireImage)
- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator)
- [Alamofire](https://github.com/Alamofire/Alamofire)
- [CommonOSLog](https://github.com/mainasuk/CommonOSLog)
- [DateToolSwift](https://github.com/MatthewYork/DateTools)
- [Kanna](https://github.com/tid-kijyun/Kanna)
- [Kingfisher](https://github.com/onevcat/Kingfisher)
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)