diff --git a/Modules/Sources/WordPressData/Blog+Extensions.swift b/Modules/Sources/WordPressData/Blog+Extensions.swift index ffe6ed0b235a..060b24c1f759 100644 --- a/Modules/Sources/WordPressData/Blog+Extensions.swift +++ b/Modules/Sources/WordPressData/Blog+Extensions.swift @@ -29,7 +29,8 @@ extension Blog { } // Add remaining formats sorted by their display names - let nonStandardFormats = postFormats + let nonStandardFormats = + postFormats .filter { $0.key != Blog.postFormatStandard } .sorted { $0.value.localizedCaseInsensitiveCompare($1.value) == .orderedAscending } .map { $0.key } diff --git a/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift b/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift index 4c4c0c557edd..05a33dc0e11e 100644 --- a/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift +++ b/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift @@ -1,6 +1,5 @@ import Foundation import CoreData -import WordPressKit /// `PublicizeInfo` encapsulates the information related to Jetpack Social auto-sharing. /// @@ -16,18 +15,11 @@ public class PublicizeInfo: NSManagedObject { } @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "PublicizeInfo") + NSFetchRequest(entityName: "PublicizeInfo") } @nonobjc public class func newObject(in context: NSManagedObjectContext) -> PublicizeInfo? { - return NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? PublicizeInfo - } - - public func configure(with remote: RemotePublicizeInfo) { - self.shareLimit = Int64(remote.shareLimit) - self.toBePublicizedCount = Int64(remote.toBePublicizedCount) - self.sharedPostsCount = Int64(remote.sharedPostsCount) - self.sharesRemaining = Int64(remote.sharesRemaining) + NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? PublicizeInfo } /// A value-type representation for Publicize auto-sharing usage. diff --git a/Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift b/Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift deleted file mode 100644 index 7e732f3ce53b..000000000000 --- a/Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import WordPressKitObjC - -/// Encapsulates remote service logic related to Jetpack Social. -public class JetpackSocialServiceRemote: ServiceRemoteWordPressComREST { - - /// Retrieves the Publicize information for the given site. - /// - /// Note: Sites with disabled share limits will return success with nil value. - /// - /// - Parameters: - /// - siteID: The target site's dotcom ID. - /// - completion: Closure to be called once the request completes. - public func fetchPublicizeInfo(for siteID: Int, - completion: @escaping (Result) -> Void) { - let path = path(forEndpoint: "sites/\(siteID)/jetpack-social", withVersion: ._2_0) - Task { @MainActor in - await self.wordPressComRestApi - .perform( - .get, - URLString: path, - jsonDecoder: .apiDecoder, - type: RemotePublicizeInfo.self - ) - .map { $0.body } - .flatMapError { original -> Result in - if case let .endpointError(endpointError) = original, endpointError.response?.statusCode == 200, endpointError.code == .responseSerializationFailed { - return .success(nil) - } - return .failure(original.asNSError()) - } - .execute(completion) - } - } -} diff --git a/Modules/Sources/WordPressKit/KeyringConnection.swift b/Modules/Sources/WordPressKit/KeyringConnection.swift deleted file mode 100644 index 22656b33fdb4..000000000000 --- a/Modules/Sources/WordPressKit/KeyringConnection.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// KeyringConnection represents a keyring connected to a particular external service. -/// We only rarely need keyring data and we don't really need to persist it. For these -/// reasons KeyringConnection is treated like a model, even though it is not an NSManagedObject, -/// but also treated like it is a Remote Object. -/// -open class KeyringConnection: NSObject { - @objc open var additionalExternalUsers = [KeyringConnectionExternalUser]() - @objc open var dateIssued = Date() - @objc open var dateExpires: Date? - @objc open var externalID = "" // Some services uses strings for their IDs - @objc open var externalName = "" - @objc open var externalDisplay = "" - @objc open var externalProfilePicture = "" - @objc open var label = "" - @objc open var keyringID: NSNumber = 0 - @objc open var refreshURL = "" - @objc open var service = "" - @objc open var status = "" - @objc open var type = "" - @objc open var userID: NSNumber = 0 -} diff --git a/Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift b/Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift deleted file mode 100644 index 72a10467acbf..000000000000 --- a/Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -/// KeyringConnectionExternalUser represents an additional user account on the -/// external service that could be used other than the default account. -/// -open class KeyringConnectionExternalUser: NSObject { - @objc open var externalID = "" - @objc open var externalName = "" - @objc open var externalCategory = "" - @objc open var externalProfilePicture = "" -} diff --git a/Modules/Sources/WordPressKit/RemotePublicizeConnection.swift b/Modules/Sources/WordPressKit/RemotePublicizeConnection.swift deleted file mode 100644 index 76b49f5c75e7..000000000000 --- a/Modules/Sources/WordPressKit/RemotePublicizeConnection.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -@objc open class RemotePublicizeConnection: NSObject { - @objc open var connectionID: NSNumber = 0 - @objc open var dateIssued = Date() - @objc open var dateExpires: Date? - @objc open var externalID = "" - @objc open var externalName = "" - @objc open var externalDisplay = "" - @objc open var externalProfilePicture = "" - @objc open var externalProfileURL = "" - @objc open var externalFollowerCount: NSNumber = 0 - @objc open var keyringConnectionID: NSNumber = 0 - @objc open var keyringConnectionUserID: NSNumber = 0 - @objc open var label = "" - @objc open var refreshURL = "" - @objc open var service = "" - @objc open var shared = false - @objc open var status = "" - @objc open var siteID: NSNumber = 0 - @objc open var userID: NSNumber = 0 -} diff --git a/Modules/Sources/WordPressKit/RemotePublicizeInfo.swift b/Modules/Sources/WordPressKit/RemotePublicizeInfo.swift deleted file mode 100644 index 032fe371d042..000000000000 --- a/Modules/Sources/WordPressKit/RemotePublicizeInfo.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public struct RemotePublicizeInfo: Decodable { - public let shareLimit: Int - public let toBePublicizedCount: Int - public let sharedPostsCount: Int - public let sharesRemaining: Int - - private enum CodingKeys: CodingKey { - case shareLimit - case toBePublicizedCount - case sharedPostsCount - case sharesRemaining - } -} diff --git a/Modules/Sources/WordPressKit/RemotePublicizeService.swift b/Modules/Sources/WordPressKit/RemotePublicizeService.swift deleted file mode 100644 index 2c0515642ec4..000000000000 --- a/Modules/Sources/WordPressKit/RemotePublicizeService.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -@objc open class RemotePublicizeService: NSObject { - @objc open var connectURL = "" - @objc open var detail = "" - @objc open var externalUsersOnly = false - @objc open var icon = "" - @objc open var jetpackSupport = false - @objc open var jetpackModuleRequired = "" - @objc open var label = "" - @objc open var multipleExternalUserIDSupport = false - @objc open var order: NSNumber = 0 - @objc open var serviceID = "" - @objc open var type = "" - @objc open var status = "" -} diff --git a/Modules/Sources/WordPressKit/SharingServiceRemote.swift b/Modules/Sources/WordPressKit/SharingServiceRemote.swift index 943d78465583..52b6ac3fa3d5 100644 --- a/Modules/Sources/WordPressKit/SharingServiceRemote.swift +++ b/Modules/Sources/WordPressKit/SharingServiceRemote.swift @@ -2,7 +2,7 @@ import Foundation import NSObject_SafeExpectations /// SharingServiceRemote is responsible for wrangling the REST API calls related to -/// publiczice services, publicize connections, and keyring connections. +/// sharing buttons. /// open class SharingServiceRemote: ServiceRemoteWordPressComREST { @@ -30,372 +30,6 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { return NSError(domain: domain, code: code, userInfo: userInfo) } - // MARK: - Publicize Related Methods - - /// Fetches the list of Publicize services. - /// - /// - Parameters: - /// - success: An optional success block accepting an array of `RemotePublicizeService` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func getPublicizeServices(_ success: (([RemotePublicizeService]) -> Void)?, failure: ((NSError?) -> Void)?) { - let endpoint = "meta/external-services" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let params = ["type": "publicize"] - - wordPressComRESTAPI.get(path, parameters: params as [String: AnyObject]?) { responseObject, httpResponse in - guard let responseDict = responseObject as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - success?(self.remotePublicizeServicesFromDictionary(responseDict)) - } failure: { error, _ in - failure?(error as NSError) - } - } - - /// Fetches the list of Publicize services for a specified siteID. - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting an array of `RemotePublicizeService` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - @objc open func getPublicizeServices(for siteID: NSNumber, - success: (([RemotePublicizeService]) -> Void)?, - failure: ((NSError?) -> Void)?) { - let path = path(forEndpoint: "sites/\(siteID)/external-services", withVersion: ._2_0) - let params = ["type": "publicize" as AnyObject] - - wordPressComRESTAPI.get( - path, - parameters: params, - success: { response, httpResponse in - guard let responseDict = response as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - success?(self.remotePublicizeServicesFromDictionary(responseDict)) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Fetches the current user's list of keyring connections. - /// - /// - Parameters: - /// - success: An optional success block accepting an array of `KeyringConnection` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func getKeyringConnections(_ success: (([KeyringConnection]) -> Void)?, failure: ((NSError?) -> Void)?) { - let endpoint = "me/keyring-connections" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - wordPressComRESTAPI.get(path, - parameters: nil, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - let connections = responseDict.array(forKey: ConnectionDictionaryKeys.connections) ?? [] - let keyringConnections: [KeyringConnection] = connections.map { dict -> KeyringConnection in - let conn = KeyringConnection() - let dict = dict as AnyObject - let externalUsers = dict.array(forKey: ConnectionDictionaryKeys.additionalExternalUsers) ?? [] - conn.additionalExternalUsers = self.externalUsersForKeyringConnection(externalUsers as NSArray) - conn.dateExpires = WPKitDateUtils.date(fromISOString: dict.string(forKey: ConnectionDictionaryKeys.expires)) - conn.dateIssued = WPKitDateUtils.date(fromISOString: dict.string(forKey: ConnectionDictionaryKeys.issued)) - conn.externalDisplay = dict.string(forKey: ConnectionDictionaryKeys.externalDisplay) ?? conn.externalDisplay - conn.externalID = dict.string(forKey: ConnectionDictionaryKeys.externalID) ?? conn.externalID - conn.externalName = dict.string(forKey: ConnectionDictionaryKeys.externalName) ?? conn.externalName - if conn.externalDisplay.isEmpty { - conn.externalDisplay = conn.externalName - } - conn.externalProfilePicture = dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? conn.externalProfilePicture - conn.keyringID = dict.number(forKey: ConnectionDictionaryKeys.ID) ?? conn.keyringID - conn.label = dict.string(forKey: ConnectionDictionaryKeys.label) ?? conn.label - conn.refreshURL = dict.string(forKey: ConnectionDictionaryKeys.refreshURL) ?? conn.refreshURL - conn.status = dict.string(forKey: ConnectionDictionaryKeys.status) ?? conn.status - conn.service = dict.string(forKey: ConnectionDictionaryKeys.service) ?? conn.service - conn.type = dict.string(forKey: ConnectionDictionaryKeys.type) ?? conn.type - conn.userID = dict.number(forKey: ConnectionDictionaryKeys.userID) ?? conn.userID - - return conn - } - - onSuccess(keyringConnections) - }, - failure: { error, _ in - failure?(error as NSError) - }) - } - - /// Creates KeyringConnectionExternalUser instances from the past array of - /// external user dictionaries. - /// - /// - Parameters: - /// - externalUsers: An array of NSDictionaries where each NSDictionary represents a KeyringConnectionExternalUser - /// - /// - Returns: An array of KeyringConnectionExternalUser instances. - /// - private func externalUsersForKeyringConnection(_ externalUsers: NSArray) -> [KeyringConnectionExternalUser] { - let arr: [KeyringConnectionExternalUser] = externalUsers.map { dict -> KeyringConnectionExternalUser in - let externalUser = KeyringConnectionExternalUser() - externalUser.externalID = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalID) ?? externalUser.externalID - externalUser.externalName = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalName) ?? externalUser.externalName - externalUser.externalProfilePicture = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? externalUser.externalProfilePicture - externalUser.externalCategory = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalCategory) ?? externalUser.externalCategory - - return externalUser - } - return arr - } - - /// Fetches the current user's list of Publicize connections for the specified site's ID. - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting an array of `RemotePublicizeConnection` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func getPublicizeConnections(_ siteID: NSNumber, success: (([RemotePublicizeConnection]) -> Void)?, failure: ((NSError?) -> Void)?) { - let endpoint = "sites/\(siteID)/publicize-connections" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - wordPressComRESTAPI.get(path, - parameters: nil, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - let connections = responseDict.array(forKey: ConnectionDictionaryKeys.connections) ?? [] - let publicizeConnections: [RemotePublicizeConnection] = connections.compactMap { dict -> RemotePublicizeConnection? in - let conn = self.remotePublicizeConnectionFromDictionary(dict as! NSDictionary) - return conn - } - - onSuccess(publicizeConnections) - }, - failure: { error, _ in - failure?(error as NSError) - }) - } - - /// Create a new Publicize connection bweteen the specified blog and - /// the third-pary service represented by the keyring. - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - keyringConnectionID: The ID of the third-party site's keyring connection. - /// - success: An optional success block accepting a `RemotePublicizeConnection` object. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func createPublicizeConnection(_ siteID: NSNumber, - keyringConnectionID: NSNumber, - externalUserID: String?, - success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError) -> Void)?) { - - let endpoint = "sites/\(siteID)/publicize-connections/new" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - var parameters: [String: AnyObject] = [PublicizeConnectionParams.keyringConnectionID: keyringConnectionID] - if let userID = externalUserID { - parameters[PublicizeConnectionParams.externalUserID] = userID as AnyObject? - } - - wordPressComRESTAPI.post(path, - parameters: parameters, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - }) - } - - /// Update the shared status of the specified publicize connection - /// - /// - Parameters: - /// - connectionID: The ID of the publicize connection. - /// - externalID: The connection's externalID. Pass `nil` if the keyring - /// connection's default external ID should be used. Otherwise pass the external - /// ID of one if the keyring connection's `additionalExternalUsers`. - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting no arguments. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func updatePublicizeConnectionWithID(_ connectionID: NSNumber, - externalID: String?, - forSite siteID: NSNumber, - success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)?) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let externalUserID = (externalID == nil) ? "false" : externalID! - - let parameters = [ - PublicizeConnectionParams.externalUserID: externalUserID - ] - - wordPressComRESTAPI.post(path, - parameters: parameters as [String: AnyObject]?, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - }) - } - - /// Update the shared status of the specified publicize connection - /// - /// - Parameters: - /// - connectionID: The ID of the publicize connection. - /// - shared: True if the connection is shared with all users of the blog. False otherwise. - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting no arguments. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func updatePublicizeConnectionWithID(_ connectionID: NSNumber, - shared: Bool, - forSite siteID: NSNumber, - success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)?) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let parameters = [ - PublicizeConnectionParams.shared: shared - ] - - wordPressComRESTAPI.post(path, - parameters: parameters as [String: AnyObject]?, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - }) - } - - /// Disconnects (deletes) the specified publicize connection - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - connectionID: The ID of the publicize connection. - /// - success: An optional success block accepting no arguments. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func deletePublicizeConnection(_ siteID: NSNumber, connectionID: NSNumber, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)/delete" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - wordPressComRESTAPI.post(path, - parameters: nil, - success: { _, _ in - success?() - }, - failure: { error, _ in - failure?(error as NSError) - }) - } - - /// Composees a `RemotePublicizeConnection` populated with values from the passed `NSDictionary` - /// - /// - Parameter dict: An `NSDictionary` representing a `RemotePublicizeConnection`. - /// - /// - Returns: A `RemotePublicizeConnection` object. - /// - private func remotePublicizeConnectionFromDictionary(_ dict: NSDictionary) -> RemotePublicizeConnection? { - guard let connectionID = dict.number(forKey: ConnectionDictionaryKeys.ID) else { - return nil - } - - let conn = RemotePublicizeConnection() - conn.connectionID = connectionID - conn.externalDisplay = dict.string(forKey: ConnectionDictionaryKeys.externalDisplay) ?? conn.externalDisplay - conn.externalID = dict.string(forKey: ConnectionDictionaryKeys.externalID) ?? conn.externalID - conn.externalName = dict.string(forKey: ConnectionDictionaryKeys.externalName) ?? conn.externalName - if conn.externalDisplay.isEmpty { - conn.externalDisplay = conn.externalName - } - conn.externalProfilePicture = dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? conn.externalProfilePicture - conn.externalProfileURL = dict.string(forKey: ConnectionDictionaryKeys.externalProfileURL) ?? conn.externalProfileURL - conn.keyringConnectionID = dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionID) ?? conn.keyringConnectionID - conn.keyringConnectionUserID = dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionUserID) ?? conn.keyringConnectionUserID - conn.label = dict.string(forKey: ConnectionDictionaryKeys.label) ?? conn.label - conn.refreshURL = dict.string(forKey: ConnectionDictionaryKeys.refreshURL) ?? conn.refreshURL - conn.status = dict.string(forKey: ConnectionDictionaryKeys.status) ?? conn.status - conn.service = dict.string(forKey: ConnectionDictionaryKeys.service) ?? conn.service - - if let expirationDateAsString = dict.string(forKey: ConnectionDictionaryKeys.expires) { - conn.dateExpires = WPKitDateUtils.date(fromISOString: expirationDateAsString) - } - - if let issueDateAsString = dict.string(forKey: ConnectionDictionaryKeys.issued) { - conn.dateIssued = WPKitDateUtils.date(fromISOString: issueDateAsString) - } - - if let sharedDictNumber = dict.number(forKey: ConnectionDictionaryKeys.shared) { - conn.shared = sharedDictNumber.boolValue - } - - if let siteIDDictNumber = dict.number(forKey: ConnectionDictionaryKeys.siteID) { - conn.siteID = siteIDDictNumber - } - - if let userIDDictNumber = dict.number(forKey: ConnectionDictionaryKeys.userID) { - conn.userID = userIDDictNumber - } - - return conn - } - // MARK: - Sharing Button Related Methods /// Fetches the list of sharing buttons for a blog. @@ -405,11 +39,16 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting an array of `RemoteSharingButton` objects. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func getSharingButtonsForSite(_ siteID: NSNumber, success: (([RemoteSharingButton]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func getSharingButtonsForSite( + _ siteID: NSNumber, + success: (([RemoteSharingButton]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "sites/\(siteID)/sharing-buttons" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - wordPressComRESTAPI.get(path, + wordPressComRESTAPI.get( + path, parameters: nil, success: { responseObject, httpResponse in guard let onSuccess = success else { @@ -428,7 +67,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } /// Updates the list of sharing buttons for a blog. @@ -439,13 +79,19 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting an array of `RemoteSharingButton` objects. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func updateSharingButtonsForSite(_ siteID: NSNumber, sharingButtons: [RemoteSharingButton], success: (([RemoteSharingButton]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func updateSharingButtonsForSite( + _ siteID: NSNumber, + sharingButtons: [RemoteSharingButton], + success: (([RemoteSharingButton]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "sites/\(siteID)/sharing-buttons" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) let buttons = dictionariesFromRemoteSharingButtons(sharingButtons) let parameters = [SharingButtonsKeys.sharingButtons: buttons] - wordPressComRESTAPI.post(path, + wordPressComRESTAPI.post( + path, parameters: parameters as [String: AnyObject]?, success: { responseObject, httpResponse in guard let onSuccess = success else { @@ -464,10 +110,11 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } - /// Composees a `RemotePublicizeConnection` populated with values from the passed `NSDictionary` + /// Composes `RemoteSharingButton` objects from the passed `NSArray` of `NSDictionary`s. /// /// - Parameter buttons: An `NSArray` of `NSDictionary`s representing `RemoteSharingButton` objects. /// @@ -497,7 +144,7 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { } private func dictionariesFromRemoteSharingButtons(_ buttons: [RemoteSharingButton]) -> [NSDictionary] { - return buttons.map({ btn -> NSDictionary in + buttons.map({ btn -> NSDictionary in let dict = NSMutableDictionary() dict[SharingButtonsKeys.buttonID] = btn.buttonID @@ -512,90 +159,6 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { return dict }) } - - private func remotePublicizeServicesFromDictionary(_ dictionary: NSDictionary) -> [RemotePublicizeService] { - let responseString = dictionary.description as NSString - let services: NSDictionary = (dictionary.forKey(ServiceDictionaryKeys.services) as? NSDictionary) ?? NSDictionary() - - return services.allKeys.map { key in - let dict = (services.forKey(key) as? NSDictionary) ?? NSDictionary() - let pub = RemotePublicizeService() - - pub.connectURL = dict.string(forKey: ServiceDictionaryKeys.connectURL) ?? "" - pub.detail = dict.string(forKey: ServiceDictionaryKeys.description) ?? "" - pub.externalUsersOnly = dict.number(forKey: ServiceDictionaryKeys.externalUsersOnly)?.boolValue ?? false - pub.icon = dict.string(forKey: ServiceDictionaryKeys.icon) ?? "" - pub.serviceID = dict.string(forKey: ServiceDictionaryKeys.ID) ?? "" - pub.jetpackModuleRequired = dict.string(forKey: ServiceDictionaryKeys.jetpackModuleRequired) ?? "" - pub.jetpackSupport = dict.number(forKey: ServiceDictionaryKeys.jetpackSupport)?.boolValue ?? false - pub.label = dict.string(forKey: ServiceDictionaryKeys.label) ?? "" - pub.multipleExternalUserIDSupport = dict.number(forKey: ServiceDictionaryKeys.multipleExternalUserIDSupport)?.boolValue ?? false - pub.type = dict.string(forKey: ServiceDictionaryKeys.type) ?? "" - pub.status = dict.string(forKey: ServiceDictionaryKeys.status) ?? "" - - // We're not guarenteed to get the right order by inspecting the - // response dictionary's keys. Instead, we can check the index - // of each service in the response string. - pub.order = NSNumber(value: responseString.range(of: pub.serviceID).location) - - return pub - } - } -} - -// Keys for PublicizeService dictionaries -private struct ServiceDictionaryKeys { - static let connectURL = "connect_URL" - static let description = "description" - static let externalUsersOnly = "external_users_only" - static let ID = "ID" - static let icon = "icon" - static let jetpackModuleRequired = "jetpack_module_required" - static let jetpackSupport = "jetpack_support" - static let label = "label" - static let multipleExternalUserIDSupport = "multiple_external_user_ID_support" - static let services = "services" - static let type = "type" - static let status = "status" -} - -// Keys for both KeyringConnection and PublicizeConnection dictionaries -private struct ConnectionDictionaryKeys { - // shared keys - static let connections = "connections" - static let expires = "expires" - static let externalID = "external_ID" - static let externalName = "external_name" - static let externalDisplay = "external_display" - static let externalProfilePicture = "external_profile_picture" - static let issued = "issued" - static let ID = "ID" - static let label = "label" - static let refreshURL = "refresh_URL" - static let service = "service" - static let sites = "sites" - static let status = "status" - static let userID = "user_ID" - - // only KeyringConnections - static let additionalExternalUsers = "additional_external_users" - static let type = "type" - static let externalCategory = "external_category" - - // only PublicizeConnections - static let externalFollowerCount = "external_follower_count" - static let externalProfileURL = "external_profile_URL" - static let keyringConnectionID = "keyring_connection_ID" - static let keyringConnectionUserID = "keyring_connection_user_ID" - static let shared = "shared" - static let siteID = "site_ID" -} - -// Names of parameters passed when creating or updating a publicize connection -private struct PublicizeConnectionParams { - static let keyringConnectionID = "keyring_connection_ID" - static let externalUserID = "external_user_ID" - static let shared = "shared" } // Names of parameters used in SharingButton requests diff --git a/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift b/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift index ea4d377311ad..75829e8265bd 100644 --- a/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift +++ b/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift @@ -28,7 +28,7 @@ class BlogBuilder { } func with(atomic: Bool) -> Self { - return set(blogOption: "is_wpcom_atomic", value: atomic ? 1 : 0) + set(blogOption: "is_wpcom_atomic", value: atomic ? 1 : 0) } func with(isHostedAtWPCom: Bool) -> Self { @@ -37,7 +37,7 @@ class BlogBuilder { } func with(supportsDomains: Bool) -> Self { - return with(isHostedAtWPCom: supportsDomains) + with(isHostedAtWPCom: supportsDomains) .with(isAdmin: supportsDomains) } @@ -60,7 +60,7 @@ class BlogBuilder { } func with(wordPressVersion: String) -> Self { - return set(blogOption: "software_version", value: wordPressVersion) + set(blogOption: "software_version", value: wordPressVersion) } func with(username: String) -> Self { @@ -104,7 +104,8 @@ class BlogBuilder { func withAccount(username: String = "test_user") -> Self { // Add Account - let account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount + let account = + NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount account.keychain = MockKeychainService() account.keychainServiceName = "test-service" account.keychainMigration = MockAuthKeyMigration() @@ -152,7 +153,9 @@ class BlogBuilder { func with(domainCount: Int, of type: DomainType, domainName: String = "") -> Self { var domains: [ManagedDomain] = [] for _ in 0.. Self { + func withMappedDomain( + originalUrl: String = "http://domain1.com", + mappedDomainUrl: String = "http://domain2.com" + ) -> Self { set(blogOption: "unmapped_url", value: originalUrl) set(blogOption: "home_url", value: mappedDomainUrl) @@ -188,7 +194,7 @@ class BlogBuilder { } func with(isWPForTeamsSite: Bool) -> Self { - return set(blogOption: "is_wpforteams_site", value: isWPForTeamsSite) + set(blogOption: "is_wpforteams_site", value: isWPForTeamsSite) } func with(connections: Set) -> Self { @@ -205,7 +211,7 @@ class BlogBuilder { @discardableResult func build() -> Blog { - return blog + blog } @discardableResult @@ -232,7 +238,8 @@ extension Blog { return } - let account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount + let account = + NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount account.keychain = MockKeychainService() account.keychainServiceName = "test-service" account.keychainMigration = MockAuthKeyMigration() diff --git a/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift b/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift index ebc8f7ebd3ac..9308893b4ea8 100644 --- a/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift +++ b/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift @@ -132,7 +132,9 @@ class PostBuilder { return self } - guard let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media else { + guard + let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media + else { return self } media.localURL = image @@ -152,7 +154,7 @@ class PostBuilder { func with(media: [Media]) -> PostBuilder { for item in media { - item.blog = post.blog + item.blog = post.blog } post.media = Set(media) @@ -183,6 +185,6 @@ class PostBuilder { func build() -> Post { // TODO: Enable this assertion once we can ensure that the post's MOC isn't being deallocated after the `PostBuilder` is // assert(post.managedObjectContext != nil) - return post + post } } diff --git a/Sources/Keystone/WordPress.h b/Sources/Keystone/WordPress.h index cbfa826b92b9..627a0a97fc35 100644 --- a/Sources/Keystone/WordPress.h +++ b/Sources/Keystone/WordPress.h @@ -37,7 +37,6 @@ FOUNDATION_EXPORT const unsigned char WordPressVersionString[]; #import #import #import -#import #import #import #import diff --git a/Tests/KeystoneTests/Helpers/PostBuilder.swift b/Tests/KeystoneTests/Helpers/PostBuilder.swift index d08cf1059307..e48ad17efa05 100644 --- a/Tests/KeystoneTests/Helpers/PostBuilder.swift +++ b/Tests/KeystoneTests/Helpers/PostBuilder.swift @@ -132,7 +132,9 @@ class PostBuilder { return self } - guard let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media else { + guard + let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media + else { return self } media.localURL = image @@ -152,7 +154,7 @@ class PostBuilder { func with(media: [Media]) -> PostBuilder { for item in media { - item.blog = post.blog + item.blog = post.blog } post.media = Set(media) @@ -183,6 +185,6 @@ class PostBuilder { func build() -> Post { // TODO: Enable this assertion once we can ensure that the post's MOC isn't being deallocated after the `PostBuilder` is // assert(post.managedObjectContext != nil) - return post + post } } diff --git a/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift b/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift deleted file mode 100644 index 06e22dc5d125..000000000000 --- a/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift +++ /dev/null @@ -1,215 +0,0 @@ -import WordPressShared -import XCTest -@testable import WordPress -@testable import WordPressData - -class DashboardJetpackSocialCardCellTests: CoreDataTestCase { - - private let featureFlags = FeatureFlagOverrideStore() - - override func setUp() { - super.setUp() - - featureFlags.override(RemoteFeatureFlag.jetpackSocialImprovements, withValue: true) - } - - override func tearDown() { - super.tearDown() - - featureFlags.override(RemoteFeatureFlag.jetpackSocialImprovements, withValue: RemoteFeatureFlag.jetpackSocialImprovements.defaultValue) - } - - // MARK: - `shouldShowCard` tests - - func testCardDisplays() { - // Given, when - let blog = createTestBlog() - - // Then - XCTAssertTrue(shouldShowCard(for: blog)) - } - - func testCardDoesNotDisplayWhenFeatureDisabled() throws { - // Given, when - let blog = createTestBlog() - - // When - featureFlags.override(RemoteFeatureFlag.jetpackSocialImprovements, withValue: false) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardNotDisplayWhenPublicizeNotSupported() { - // Given, when - let blog = createTestBlog(isPublicizeSupported: false) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardNotDisplayWhenPublicizeServicesDoesNotExist() throws { - // Given, when - let blog = createTestBlog(hasServices: false) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardDoesNotDisplayWithPublicizeConnections() throws { - // Given, when - let blog = createTestBlog(hasConnections: true) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardDoesNotDisplayWhenViewHidden() throws { - // Given - let blog = createTestBlog() - let dotComID = try XCTUnwrap(blog.dotComID) - // FIXME: Using this production type in the test will mess the global state - let repository = UserPersistentStoreFactory.instance() - let key = DashboardJetpackSocialCardCell.Constants.hideNoConnectionViewKey - - // When - repository.set([dotComID.stringValue: true], forKey: key) - addTeardownBlock { - repository.removeObject(forKey: key) - } - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardDisplaysWhenJetpackSiteRunsOutOfShares() throws { - // Given, when - let blog = createTestBlog(hasConnections: true, publicizeInfoState: .exceedingLimit) - - // Then - XCTAssertTrue(shouldShowCard(for: blog)) - } - - // MARK: - Card state tests - - func testInitialCardState() { - // Given, when - let subject = DashboardJetpackSocialCardCell() - - // Then - XCTAssertEqual(subject.displayState, .none) - } - - func testCardStateWithNoConnections() { - // Given - let subject = DashboardJetpackSocialCardCell() - let blog = createTestBlog() - - // When - subject.configure(blog: blog, viewController: nil, apiResponse: nil) - - // Then - XCTAssertEqual(subject.displayState, .noConnections) - } - - func testCardStateWhenUpdatedWithUnsupportedBlog() { - // Given - let subject = DashboardJetpackSocialCardCell() - let initialBlog = createTestBlog() - let blog = createTestBlog(isPublicizeSupported: false, hasServices: false, hasConnections: true) - subject.configure(blog: initialBlog, viewController: nil, apiResponse: nil) - - // When - subject.configure(blog: blog, viewController: nil, apiResponse: nil) - - // Then - XCTAssertEqual(subject.displayState, .none) - } - - // MARK: Atomic Site Tests - - // In some cases, atomic sites could sometimes get limited sharing from the API. - // We'll need to ignore any sharing limit information if it's a Simple or Atomic site. - // Refs: p9F6qB-dLk-p2#comment-56603 - func testCardOutOfSharesDoesNotDisplayForAtomicSites() throws { - // Given, when - let blog = createTestBlog(isAtomic: true, hasConnections: true, publicizeInfoState: .exceedingLimit) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardNoConnectionDisplaysForAtomicSites() throws { - // Given, when - let blog = createTestBlog(isAtomic: true) - - // Then - XCTAssertTrue(shouldShowCard(for: blog)) - } -} - -// MARK: - Helpers - -private extension DashboardJetpackSocialCardCellTests { - - func shouldShowCard(for blog: Blog) -> Bool { - return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) - } - - enum PublicizeInfoState { - case none - case belowLimit - case exceedingLimit - } - - func createTestBlog(isPublicizeSupported: Bool = true, - isAtomic: Bool = false, - hasServices: Bool = true, - hasConnections: Bool = false, - publicizeInfoState: PublicizeInfoState = .none) -> Blog { - var builder = BlogBuilder(mainContext) - .withAnAccount() - .with(dotComID: 12345) - .with(capabilities: [.publishPosts]) - .with(atomic: isAtomic) - - if isPublicizeSupported { - builder = builder.with(modules: ["publicize"]) - } - - if hasServices { - _ = createPublicizeService() - } - - if hasConnections { - let connection = PublicizeConnection(context: mainContext) - connection.status = "ok" - builder = builder.with(connections: [connection]) - } - - let blog = builder.build() - - switch publicizeInfoState { - case .belowLimit: - let publicizeInfo = PublicizeInfo(context: mainContext) - publicizeInfo.shareLimit = 30 - publicizeInfo.sharesRemaining = 25 - blog.publicizeInfo = publicizeInfo - break - case .exceedingLimit: - let publicizeInfo = PublicizeInfo(context: mainContext) - publicizeInfo.shareLimit = 30 - publicizeInfo.sharesRemaining = 0 - blog.publicizeInfo = publicizeInfo - break - default: - break - } - - return blog - } - - func createPublicizeService() -> PublicizeService { - return PublicizeService(context: mainContext) - } -} diff --git a/Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift b/Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift deleted file mode 100644 index 467c639ab857..000000000000 --- a/Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import XCTest - -@testable import WordPress -@testable import WordPressData - -final class Blog_PublicizeTests: CoreDataTestCase { - - func testAutoSharingInfoForDotComBlog() { - let blog = makeBlog(hostedAtWPCom: true, activeSocialFeature: false) - - // all dotcom sites should have no sharing limitations. - XCTAssertFalse(blog.isSocialSharingLimited) - } - - func testAutoSharingInfoForDotComBlogWithStoredSharingLimit() { - // unlikely case, but let's test anyway. - let blog = makeBlog(hostedAtWPCom: true, activeSocialFeature: false, hasPreExistingData: true) - - XCTAssertFalse(blog.isSocialSharingLimited) - XCTAssertNil(blog.sharingLimit) - } - - func testAutoSharingInfoForSelfHostedBlog() { - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: false) - - XCTAssertTrue(blog.isSocialSharingLimited) - } - - func testAutoSharingInfoForSelfHostedBlogWithStoredSharingLimit() { - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: false, hasPreExistingData: true) - - XCTAssertNotNil(blog.sharingLimit) - } - - func testAutoSharingInfoForSelfHostedBlogWithSocialFeature() { - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: true) - - XCTAssertFalse(blog.isSocialSharingLimited) - } - - func testAutoSharingInfoForSelfHostedBlogWithSocialFeatureAndStoredSharingLimit() { - // Example: a free site purchased an individual Social Basic plan. In this case, there should still be a - // `PublicizeInfo` data stored in Core Data, but the blog might still have the social feature active. - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: true, hasPreExistingData: true) - - // the sharing limit should be nil regardless of the existence of stored data. - XCTAssertNil(blog.sharingLimit) - } - - // MARK: - Helpers - - private func makeBlog(hostedAtWPCom: Bool, activeSocialFeature: Bool, hasPreExistingData: Bool = false) -> Blog { - let blog = BlogBuilder(mainContext) - .with(isHostedAtWPCom: hostedAtWPCom) - .with(planActiveFeatures: activeSocialFeature ? ["social-shares-1000"] : []) - .build() - - if hasPreExistingData { - let publicizeInfo = PublicizeInfo(context: mainContext) - publicizeInfo.shareLimit = 30 - publicizeInfo.sharesRemaining = 25 - blog.publicizeInfo = publicizeInfo - } - - return blog - } -} diff --git a/Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift b/Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift deleted file mode 100644 index 5252ddeb5a46..000000000000 --- a/Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift +++ /dev/null @@ -1,285 +0,0 @@ -import XCTest - -@testable import WordPress -@testable import WordPressData - -class Post_JetpackSocialTests: CoreDataTestCase { - - private let keyringAndConnectionIDPairs = [ - (100, 200), - (101, 201), - (102, 202) - ] - - private lazy var connections: Set = { - let connectionsArray = keyringAndConnectionIDPairs.map { keyringID, connectionID in - let connection = PublicizeConnection(context: mainContext) - connection.keyringConnectionID = NSNumber(value: keyringID) - connection.connectionID = NSNumber(value: connectionID) - return connection - } - return Set(connectionsArray) - }() - - private lazy var blog: Blog = { - BlogBuilder(mainContext).with(connections: connections).build() - }() - - // MARK: - Checking for PublicizeConnection state - - func testCheckPublicizeConnectionHavingOnlyKeyringIDEntry() { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled] - ]) - - // When - let result = post.publicizeConnectionDisabledForKeyringID(keyringID) - - // Then - XCTAssertTrue(result) - } - - func testCheckPublicizeConnectionHavingOnlyConnectionIDEntry() { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - connectionID: [.valueKey: .disabled] - ]) - - // When - let result = post.publicizeConnectionDisabledForKeyringID(keyringID) - - // Then - XCTAssertTrue(result) - } - - func testCheckPublicizeConnectionHavingDifferentKeyringAndConnectionEntries() { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .enabled], - connectionID: [.valueKey: .disabled] - ]) - let post2 = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled], - connectionID: [.valueKey: .enabled] - ]) - - // When - let result1 = post.publicizeConnectionDisabledForKeyringID(keyringID) - let result2 = post2.publicizeConnectionDisabledForKeyringID(keyringID) - - // Then - // if either one of the value is true, then the method should return true. See: pctCYC-XT-p2#comment-1000 - XCTAssertTrue(result1) - XCTAssertTrue(result2) - } - - // MARK: - Disabling connections - - func testDisableConnectionWithoutAnyEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost() - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(entry[.valueKey], .disabled) - } - - func testDisableConnectionWithPriorKeyringEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .enabled] - ]) - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(entry[.valueKey], .disabled) - } - - func testDisableConnectionWithPriorKeyringAndConnectionEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .enabled], - connectionID: [.valueKey: .enabled] - ]) - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // both entries' values should be updated. - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .disabled) - - let connectionEntry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(connectionEntry[.valueKey], .disabled) - } - - func testDisableConnectionHavingOnlyConnectionIDEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - connectionID: [.valueKey: .enabled] - ]) - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // if the keyring entry doesn't exist, the dictionary key should be updated to the keyringID. - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .disabled) - - // the connection entry should be deleted. - XCTAssertNil(post.disabledPublicizeConnections?[connectionID]) - } - - // MARK: - Enabling connections - - // Note: unlikely case since there must be an entry in the `disabledPublicizeConnections` for the switch to be on. - func testEnableConnectionWithoutAnyEntries() { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost() - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // Calling the enable method should do nothing. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - } - - func testEnableConnectionWithLocalKeyringEntry() { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // if the entry hasn't been synced yet, the entry will be deleted since all connections are enabled by default. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - } - - func testEnableConnectionWithSyncedKeyringEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled, .idKey: "24"] // having an id means the entry exists on backend. - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .enabled) - } - - // both keyring entry and connection entry are synced - func testEnableConnectionWithPriorSyncedKeyringAndConnectionEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled, .idKey: "24"], // having an id means the entry exists on backend. - connectionID: [.valueKey: .disabled, .idKey: "26"] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // both entries should be updated. - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .enabled) - - let connectionEntry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(connectionEntry[.valueKey], .enabled) - } - - // the keyring entry is local, but the connection entry is synced. - func testEnableConnectionWithPriorLocalKeyringAndSyncedConnectionEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled], - connectionID: [.valueKey: .disabled, .idKey: "26"] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // the local entry should be removed. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - - // but the synced entry should be updated. - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(entry[.valueKey], .enabled) - } - - func testEnableConnectionHavingOnlyConnectionEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let keyringID2 = NSNumber(value: 101) - let connectionID2 = NSNumber(value: 201) - let post = makePost(disabledConnections: [ - connectionID: [.valueKey: .disabled, .idKey: "26"], - connectionID2: [.valueKey: .disabled] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - post.enablePublicizeConnectionWithKeyringID(keyringID2) - - // Then - // there shouldn't be any entries keyed by the keyringIDs. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - XCTAssertNil(post.disabledPublicizeConnections?[keyringID2]) - - // the entry with connectionID should be updated. - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(entry[.valueKey], .enabled) - - // and if the entry isn't synced, it should be deleted. - XCTAssertNil(post.disabledPublicizeConnections?[connectionID2]) - } - - // MARK: - Helpers - - private func makePost(disabledConnections: [NSNumber: [String: String]] = [:]) -> Post { - PostBuilder(mainContext, blog: blog) - .with(disabledConnections: disabledConnections) - .build() - } -} - -private extension String { - static let idKey = Post.Constants.publicizeIdKey - static let valueKey = Post.Constants.publicizeValueKey - static let disabled = Post.Constants.publicizeDisabledValue - static let enabled = Post.Constants.publicizeEnabledValue -} diff --git a/Tests/KeystoneTests/Tests/Models/PostTests.swift b/Tests/KeystoneTests/Tests/Models/PostTests.swift index 983763bbcc7a..3c381c27c87f 100644 --- a/Tests/KeystoneTests/Tests/Models/PostTests.swift +++ b/Tests/KeystoneTests/Tests/Models/PostTests.swift @@ -7,15 +7,16 @@ import XCTest class PostTests: CoreDataTestCase { fileprivate func newTestBlog() -> Blog { - return NSEntityDescription.insertNewObject(forEntityName: Blog.entityName(), into: mainContext) as! Blog + NSEntityDescription.insertNewObject(forEntityName: Blog.entityName(), into: mainContext) as! Blog } fileprivate func newTestPost() -> Post { - return NSEntityDescription.insertNewObject(forEntityName: Post.entityName(), into: mainContext) as! Post + NSEntityDescription.insertNewObject(forEntityName: Post.entityName(), into: mainContext) as! Post } fileprivate func newTestPostCategory() -> PostCategory { - return NSEntityDescription.insertNewObject(forEntityName: PostCategory.entityName(), into: mainContext) as! PostCategory + NSEntityDescription.insertNewObject(forEntityName: PostCategory.entityName(), into: mainContext) + as! PostCategory } fileprivate func newTestPostCategory(_ name: String) -> PostCategory { @@ -112,7 +113,11 @@ class PostTests: CoreDataTestCase { func testThatRemoveCategoriesWorks() { let post = newTestPost() - let testCategories = Set(arrayLiteral: newTestPostCategory("1"), newTestPostCategory("2"), newTestPostCategory("3")) + let testCategories = Set( + arrayLiteral: newTestPostCategory("1"), + newTestPostCategory("2"), + newTestPostCategory("3") + ) post.categories = testCategories XCTAssertNotEqual(post.categories?.count, 0) @@ -153,8 +158,10 @@ class PostTests: CoreDataTestCase { let post = newTestPost() let blog = newTestBlog() - blog.postFormats = [defaultPostFormat.key: defaultPostFormat.value, - secondaryPostFormat.key: secondaryPostFormat.value] + blog.postFormats = [ + defaultPostFormat.key: defaultPostFormat.value, + secondaryPostFormat.key: secondaryPostFormat.value + ] post.blog = blog post.postFormat = secondaryPostFormat.key @@ -169,8 +176,10 @@ class PostTests: CoreDataTestCase { let post = newTestPost() let blog = newTestBlog() - blog.postFormats = [defaultPostFormat.key: defaultPostFormat.value, - secondaryPostFormat.key: secondaryPostFormat.value] + blog.postFormats = [ + defaultPostFormat.key: defaultPostFormat.value, + secondaryPostFormat.key: secondaryPostFormat.value + ] post.blog = blog post.setPostFormatText(secondaryPostFormat.value) @@ -185,7 +194,7 @@ class PostTests: CoreDataTestCase { "z": "Z", "standard": "Standard", "a": "A", - "d": "D", + "d": "D" ] let expectedPostFormats = [ diff --git a/Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift b/Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift deleted file mode 100644 index 6cc727597023..000000000000 --- a/Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift +++ /dev/null @@ -1,231 +0,0 @@ -import Foundation -import XCTest -import OHHTTPStubs -import OHHTTPStubsSwift - -@testable import WordPress -@testable import WordPressData - -class JetpackSocialServiceTests: CoreDataTestCase { - - private let timeout: TimeInterval = 1.0 - private let blogID = 1001 - - private var jetpackSocialPath: String { - "/wpcom/v2/sites/\(blogID)/jetpack-social" - } - - private lazy var service: JetpackSocialService = { - .init(coreDataStack: contextManager) - }() - - override func setUp() { - super.setUp() - - BlogBuilder(mainContext).with(blogID: blogID).build() - contextManager.saveContextAndWait(mainContext) - } - - override func tearDown() { - HTTPStubs.removeAllStubs() - super.tearDown() - } - - // MARK: syncSharingLimit - - // non-existing PublicizeInfo + some RemotePublicizeInfo -> insert - func testSyncSharingLimitWithNewPublicizeInfo() throws { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: ["share_limit": 30, - "to_be_publicized_count": 15, - "shared_posts_count": 15, - "shares_remaining": 14] as [String: Any], - statusCode: 200, - headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - XCTAssertNotNil(sharingLimit) - XCTAssertEqual(sharingLimit?.remaining, 14) - XCTAssertEqual(sharingLimit?.limit, 30) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // non-existing PublicizeInfo + nil RemotePublicizeInfo -> nothing changes - func testSyncSharingLimitWithNilPublicizeInfo() { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - XCTAssertNil(sharingLimit) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // pre-existing PublicizeInfo + some RemotePublicizeInfo -> update - func testSyncSharingLimitWithNewPublicizeInfoGivenPreExistingData() throws { - try addPreExistingPublicizeInfo() - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: ["share_limit": 30, - "to_be_publicized_count": 15, - "shared_posts_count": 15, - "shares_remaining": 14] as [String: Any], - statusCode: 200, - headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - // the sharing limit fields should be updated according to the newest data. - XCTAssertNotNil(sharingLimit) - XCTAssertEqual(sharingLimit?.remaining, 14) - XCTAssertEqual(sharingLimit?.limit, 30) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // pre-existing PublicizeInfo + nil RemotePublicizeInfo -> delete - func testSyncSharingLimitWithNilPublicizeInfoGivenPreExistingData() throws { - try addPreExistingPublicizeInfo() - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - // the pre-existing sharing limit should've been deleted. - XCTAssertNil(sharingLimit) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // non-existing blog ID + some RemotePublicizeInfo - func testSyncSharingLimitWithNewPublicizeInfoGivenInvalidBlogID() { - let invalidBlogID = 1002 - stub(condition: isPath("/wpcom/v2/sites/\(invalidBlogID)/jetpack-social")) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should fail") - service.syncSharingLimit(for: invalidBlogID) { result in - guard case .failure(let error) = result, - case .blogNotFound(let id) = error as? JetpackSocialService.ServiceError else { - XCTFail("Expected JetpackSocialService.ServiceError to occur") - return expectation.fulfill() - } - - XCTAssertEqual(id, invalidBlogID) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testSyncSharingLimitRemoteFetchFailure() { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should fail") - service.syncSharingLimit(for: blogID) { result in - guard case .failure = result else { - XCTFail("syncSharingLimit unexpectedly succeeded") - return expectation.fulfill() - } - - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // MARK: syncSharingLimit Objective-C - - func testObjcSyncSharingLimitSuccess() async { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let syncSucceeded = await withCheckedContinuation { continuation in - service.syncSharingLimit(dotComID: NSNumber(value: blogID)) { - continuation.resume(returning: true) - } failure: { _ in - continuation.resume(returning: false) - } - } - - XCTAssertTrue(syncSucceeded) - } - - func testObjcSyncSharingLimitNilIDFailure() async { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } - - let syncSucceeded = await withCheckedContinuation { continuation in - service.syncSharingLimit(dotComID: NSNumber(value: blogID)) { - continuation.resume(returning: true) - } failure: { _ in - continuation.resume(returning: false) - } - } - - XCTAssertFalse(syncSucceeded) - } - - func testObjcSyncSharingLimitRequestFailure() async { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } - - let syncSucceeded = await withCheckedContinuation { continuation in - service.syncSharingLimit(dotComID: NSNumber(value: blogID)) { - continuation.resume(returning: true) - } failure: { _ in - continuation.resume(returning: false) - } - } - - XCTAssertFalse(syncSucceeded) - } -} - -// MARK: - Helpers - -private extension JetpackSocialServiceTests { - - func addPreExistingPublicizeInfo() throws { - let blog = try Blog.lookup(withID: blogID, in: mainContext) - let info = PublicizeInfo(context: mainContext) - info.sharesRemaining = 550 - info.shareLimit = 1000 - blog?.publicizeInfo = info - contextManager.saveContextAndWait(mainContext) - } -} diff --git a/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift b/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift index 29319a7fcd44..9b9521533247 100644 --- a/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift +++ b/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift @@ -161,7 +161,10 @@ class PostCoordinatorTests: CoreDataTestCase { media.blog = post.blog media.mediaType = .image media.filename = "test-image.jpg" - media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL(forResource: "test-image", fileExtension: "jpg") + media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL( + forResource: "test-image", + fileExtension: "jpg" + ) // important otherwise MediaService will use temporary objectID and fail try mainContext.save() @@ -170,7 +173,8 @@ class PostCoordinatorTests: CoreDataTestCase { revision1.postTitle = "title-b" revision1.media = [media] let uploadID = media.gutenbergUploadID - revision1.content = "\n
\n" + revision1.content = + "\n
\n" // GIVEN stub(condition: isPath("/rest/v1.2/sites/80511/posts/new")) { request in @@ -191,7 +195,10 @@ class PostCoordinatorTests: CoreDataTestCase { try await coordinator.waitForSync(post, to: revision1) // THEN image block was updated - XCTAssertEqual(post.content, "\n
\n") + XCTAssertEqual( + post.content, + "\n
\n" + ) // THEN revisions were uploaded XCTAssertFalse(post.hasRevision()) @@ -231,11 +238,17 @@ class PostCoordinatorTests: CoreDataTestCase { coordinator.syncRetryDelay = 10 // GIVEN the app quickly restoring connectivity after the first failure - coordinator.syncEvents.sink { - if case .finished(_, let result) = $0, case .failure = result { - NotificationCenter.default.post(name: .reachabilityUpdated, object: nil, userInfo: [Foundation.Notification.reachabilityKey: true]) + coordinator.syncEvents + .sink { + if case .finished(_, let result) = $0, case .failure = result { + NotificationCenter.default.post( + name: .reachabilityUpdated, + object: nil, + userInfo: [Foundation.Notification.reachabilityKey: true] + ) + } } - }.store(in: &cancellables) + .store(in: &cancellables) coordinator.setNeedsSync(for: revision1) try await coordinator.waitForSync(post, to: revision1, ignoreErrors: true) @@ -260,7 +273,10 @@ class PostCoordinatorTests: CoreDataTestCase { media.blog = post.blog media.mediaType = .image media.filename = "test-image.jpg" - media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL(forResource: "test-image", fileExtension: "jpg") + media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL( + forResource: "test-image", + fileExtension: "jpg" + ) // important otherwise MediaService will use temporary objectID and fail try mainContext.save() @@ -269,7 +285,8 @@ class PostCoordinatorTests: CoreDataTestCase { revision1.postTitle = "title-b" revision1.media = [media] let uploadID = media.gutenbergUploadID - revision1.content = "\n
\n" + revision1.content = + "\n
\n" // GIVEN let expectation = self.expectation(description: "started-media-request") @@ -316,10 +333,13 @@ class PostCoordinatorTests: CoreDataTestCase { // GIVEN a server where the post was deleted stub(condition: isPath("/rest/v1.2/sites/80511/posts/974")) { _ in - return try! HTTPStubsResponse(value: [ - "error": "unknown_post", - "message": "Unknown post" - ], statusCode: 404) + try! HTTPStubsResponse( + value: [ + "error": "unknown_post", + "message": "Unknown post" + ], + statusCode: 404 + ) } // WHEN @@ -329,7 +349,8 @@ class PostCoordinatorTests: CoreDataTestCase { XCTFail("Expected sync to fail") } catch { guard let error = error as? PostRepository.PostSaveError, - case .deleted = error else { + case .deleted = error + else { return XCTFail("Unexpected error") } } @@ -393,10 +414,13 @@ class PostCoordinatorTests: CoreDataTestCase { return response } stub(condition: isPath("/rest/v1.2/sites/80511/posts/974")) { request in - XCTAssertEqual(request.getBodyParameters(), [ - "slug": "hello", - "status": "publish" - ]) + XCTAssertEqual( + request.getBodyParameters(), + [ + "slug": "hello", + "status": "publish" + ] + ) var post = WordPressComPost.mock post.status = AbstractPost.Status.publish.rawValue post.title = "title-a" @@ -445,18 +469,21 @@ class PostCoordinatorTests: CoreDataTestCase { let parameters = request.getBodyParameters() ?? [:] XCTAssertEqual(parameters["status"], "publish") let metadata = (parameters["metadata"] as? [[String: AnyHashable]]) ?? [] - XCTAssertEqual(Set(metadata), Set([ - [ - "key": "_wpas_mess", - "operation": "update", - "value": "message-a" - ], - [ - "key": "_jetpack_blogging_prompt_key", - "operation": "update", - "value": "prompt-a" - ] - ])) + XCTAssertEqual( + Set(metadata), + Set([ + [ + "key": "_wpas_mess", + "operation": "update", + "value": "message-a" + ], + [ + "key": "_jetpack_blogging_prompt_key", + "operation": "update", + "value": "prompt-a" + ] + ]) + ) var post = WordPressComPost.mock post.metadata = [ WordPressComPost.Metadata(id: "752", key: "_wpas_mess", value: "message-a") @@ -466,10 +493,6 @@ class PostCoordinatorTests: CoreDataTestCase { // WHEN try await coordinator.publish(post) - - // THEN - XCTAssertEqual(post.publicizeMessage, "message-a") - XCTAssertEqual(post.publicizeMessageID, "752") } // MARK: - Misc @@ -593,36 +616,37 @@ class PostCoordinatorTests: CoreDataTestCase { } private let mediaResponse = """ -{ - "media": [ { - "ID": 1236, - "URL": "https://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", - "guid": "http://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", - "date": "2024-03-25T18:50:12-04:00", - "post_ID": 0, - "author_ID": 34129043, - "file": "img_0005-1-1.jpg", - "mime_type": "image/jpeg", - "extension": "jpg", - "title": "img_0005-1", - "caption": "", - "description": "", - "alt": "", - "icon": "https://s1.wp.com/wp-includes/images/media/default.png", - "size": "701.54 KB", - "height": 1335, - "width": 2000 + "media": [ + { + "ID": 1236, + "URL": "https://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", + "guid": "http://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", + "date": "2024-03-25T18:50:12-04:00", + "post_ID": 0, + "author_ID": 34129043, + "file": "img_0005-1-1.jpg", + "mime_type": "image/jpeg", + "extension": "jpg", + "title": "img_0005-1", + "caption": "", + "description": "", + "alt": "", + "icon": "https://s1.wp.com/wp-includes/images/media/default.png", + "size": "701.54 KB", + "height": 1335, + "width": 2000 + } + ] } - ] -} -""" + """ private extension URLRequest { func getBodyParameters() -> [String: AnyHashable]? { guard let data = httpBodyStream?.read(), - let object = try? JSONSerialization.jsonObject(with: data), - let parameters = object as? [String: AnyHashable] else { + let object = try? JSONSerialization.jsonObject(with: data), + let parameters = object as? [String: AnyHashable] + else { return nil } return parameters @@ -630,7 +654,12 @@ private extension URLRequest { } private extension PostCoordinator { - func waitForSync(_ post: AbstractPost, to revision: AbstractPost, ignoreErrors: Bool = false, timeout: TimeInterval = 5) async throws { + func waitForSync( + _ post: AbstractPost, + to revision: AbstractPost, + ignoreErrors: Bool = false, + timeout: TimeInterval = 5 + ) async throws { var olderRevisionIDs = Set(post.allRevisions.filter(\.isSyncNeeded).map(\.objectID)) olderRevisionIDs.remove(revision.objectID) return try await waitForSync(post, ignoreErrors: ignoreErrors, timeout: timeout) { operation in @@ -649,8 +678,14 @@ private extension PostCoordinator { /// /// - parameter timeout: The default value is 5 seconds. /// - parameter handler: Return `true` if the revision matches the one you expected. - func waitForSync(_ post: AbstractPost, ignoreErrors: Bool = false, timeout: TimeInterval = 5, handler: @escaping (PostCoordinator.SyncOperation) -> Bool) async throws { - let result = await syncEvents + func waitForSync( + _ post: AbstractPost, + ignoreErrors: Bool = false, + timeout: TimeInterval = 5, + handler: @escaping (PostCoordinator.SyncOperation) -> Bool + ) async throws { + let result = + await syncEvents .compactMap { event -> Result? in guard case .finished(let operation, let result) = event else { return nil diff --git a/Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift b/Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift deleted file mode 100644 index 8cbfa05b676e..000000000000 --- a/Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift +++ /dev/null @@ -1,204 +0,0 @@ -import XCTest - -@testable import WordPress -@testable import WordPressData - -class PostHelperJetpackSocialTests: CoreDataTestCase { - let connectionId = 123 - let keyringId = 456 - - // MARK: - RemotePost -> Post tests - - func testMetadataWithConnectionIDKey() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: String] = [ - "id": "10", - "key": "_wpas_skip_publicize_123", - "value": "1" - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // the keyring ID should be used as the key, while keeping the entry intact. - XCTAssertEqual(result[NSNumber(value: keyringId)], metadataEntry) - } - - func testMetadataWithKeyringIDKey() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: String] = [ - "id": "10", - "key": "_wpas_skip_456", - "value": "1" - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // the keyring ID should be used as the key, while keeping the entry intact. - XCTAssertEqual(result[NSNumber(value: keyringId)], metadataEntry) - } - - func testMetadataWithConnectionIDKeyNotMatchingAnyPublicizeConnections() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: String] = [ - "id": "10", - "key": "_wpas_skip_publicize_500", // connectionId different from the one in local. - "value": "1" - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // the connection ID should be used as key. - XCTAssertEqual(result[NSNumber(value: 500)], metadataEntry) - } - - func testMetadataEntriesWithNonStringValues() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: Any] = [ - "id": "10", - "key": "sharing_disabled", - "value": [123] - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // invalid entries should be ignored. - XCTAssertTrue(result.isEmpty) - } - - // MARK: - Post -> RemotePost tests - - func testDisabledConnectionsWithId() throws { - // Given - let connectionId2 = 234 - let keyringId2 = 567 - let presetDisabledConnections: [NSNumber: [String: String]] = [ - NSNumber(value: keyringId): ["id": "10", "value": "1"], - NSNumber(value: keyringId2): ["id": "11", "key": "_wpas_skip_\(keyringId2)", "value": "0"] - ] - let connections = makeConnections(with: [(connectionId, keyringId), (connectionId2, keyringId2)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - XCTAssertEqual(entries.count, 2) - - // keyless entries with id should default to the _wpas_skip_ format, despite having a matching connection. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_\(keyringId)" })) - - // entries with keys should be passed as is. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_\(keyringId2)" })) - } - - // should convert to publicize format. - func testKeylessDisabledConnectionsWithoutId() throws { - // Given - let connectionId2 = 234 - let keyringId2 = 567 - let presetDisabledConnections: [NSNumber: [String: String]] = [ - NSNumber(value: keyringId): ["value": "1"], - NSNumber(value: keyringId2): ["key": "_wpas_skip_\(keyringId2)", "value": "0"] - ] - let connections = makeConnections(with: [(connectionId, keyringId), (connectionId2, keyringId2)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - XCTAssertEqual(entries.count, 2) - - // local entries should be updated to the _wpas_skip_publicize format. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_publicize_\(connectionId)" })) - - // it's unlikely for a no-id entry to have a key, since it's assigned by the PostService when syncing. - // but in this unlikely case, the key should be sent as is. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_\(keyringId2)" })) - } - - func testDisabledConnectionsWithoutIdAndNoMatchingPublicizeConnection() { - let nonExistentKeyringId = 3942 - let presetDisabledConnections = [NSNumber(value: nonExistentKeyringId): ["value": "1"]] - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - XCTAssertEqual(entries.count, 1) - - // local entries with no matching PublicizeConnection should default to _wpas_skip format. - XCTAssertEqual(entries.first?["key"], "_wpas_skip_\(nonExistentKeyringId)") - } - - func testInvalidDisabledConnectionEntry() { - let presetDisabledConnections = [NSNumber(value: 0): ["value": "1"]] - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - // Dictionary entries keyed by 0 should be ignored. This is a bug from previous implementation. - XCTAssertEqual(entries.count, 0) - } - - // MARK: - Test Helpers - - private func makeConnections(with pairs: [(connectionID: Int, keyringID: Int)]) -> Set { - return Set(pairs.map { - let connection = PublicizeConnection(context: mainContext) - connection.connectionID = NSNumber(value: $0.connectionID) - connection.keyringConnectionID = NSNumber(value: $0.keyringID) - - return connection - }) - } - - private func makeBlog(connections: Set? = nil) -> Blog { - var builder = BlogBuilder(mainContext) - - if let connections { - builder = builder.with(connections: connections) - } - - return builder.build() - } - - private func makePost(for blog: Blog, disabledConnections: [NSNumber: [String: String]]? = nil) -> Post { - var builder = PostBuilder(mainContext, blog: blog) - - if let disabledConnections { - builder = builder.with(disabledConnections: disabledConnections) - } - - return builder.build() - } -} diff --git a/Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift b/Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift deleted file mode 100644 index 83e75b8b7c74..000000000000 --- a/Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import XCTest -import WordPressKit -import OHHTTPStubs -import OHHTTPStubsSwift - -@testable import WordPress -@testable import WordPressData - -class SharingServiceTests: CoreDataTestCase { - - private let userID = 101 - private let blogID = 10 - - private lazy var account: WPAccount = { - AccountBuilder(contextManager.mainContext) - .with(id: Int64(userID)) - .with(username: "username") - .with(authToken: "authToken") - .build() - }() - - private lazy var blog: Blog = { - let blog = BlogBuilder(mainContext).with(blogID: blogID).build() - blog.account = account - - // ensure that the changes are persisted to the stack. - contextManager.saveContextAndWait(mainContext) - return blog - }() - - // MARK: Sync Publicize Connections - - func testSyncingPublicizeConnectionsForNonDotComBlogCallsACompletionBlock() throws { - let blog = Blog.createBlankBlog(in: mainContext) - blog.account = nil - - let expect = expectation(description: "Sharing service completion block called.") - - let sharingService = SharingSyncService(coreDataStack: contextManager) - sharingService.syncPublicizeConnectionsForBlog(blog) { - expect.fulfill() - } failure: { _ in - expect.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - } - - func testSyncingPublicizeConnectionsExcludesUnsharedConnections() async throws { - // Given - let service = SharingSyncService(coreDataStack: contextManager) - - stub(condition: isPath("/rest/v1.1/sites/\(blogID)/publicize-connections") && isMethodGET()) { _ in - HTTPStubsResponse(jsonObject: [ - "connections": [ - ["ID": 1000, "shared": "0", "user_ID": 101] as [String: Any], // owned connection - ["ID": 1001, "shared": "1", "user_ID": 201] as [String: Any], // shared connection - ["ID": 1002, "shared": "0", "user_ID": 301] as [String: Any], // private connection from others - ] - ], statusCode: 200, headers: nil) - } - - // When - try await withCheckedThrowingContinuation { continuation in - service.syncPublicizeConnectionsForBlog(blog) { - continuation.resume() - } failure: { error in - continuation.resume(throwing: error!) - } - } - - // Then - let connections = try XCTUnwrap(blog.connections) - - // the one with ID `1002` should be skipped since it's an unshared private connection from another user. - XCTAssertEqual(connections.count, 2) - - // connections owned by the user should be available. - XCTAssertTrue(connections.contains(where: { $0.connectionID.isEqual(to: NSNumber(value: 1000)) })) - - // shared connections owned by others should also be available. - XCTAssertTrue(connections.contains(where: { $0.connectionID.isEqual(to: NSNumber(value: 1001)) })) - } -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json deleted file mode 100644 index cee274bdc55f..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "code": "rest_cannot_view", - "message": "Required headers are missing from the request.", - "data": { - "status": 403 - } -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json deleted file mode 100644 index 3f84a1ccad67..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "is_share_limit_enabled": false, - "is_enhanced_publishing_enabled": false, - "is_social_image_generator_enabled": false -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json deleted file mode 100644 index e17256d046c7..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "is_share_limit_enabled": true, - "to_be_publicized_count": 1, - "share_limit": 30, - "publicized_count": 15, - "shared_posts_count": 15, - "shares_remaining": 14, - "is_enhanced_publishing_enabled": false, - "is_social_image_generator_enabled": false -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json deleted file mode 100644 index c1798cdf0e43..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "services": { - "facebook": { - "ID": "facebook", - "label": "Facebook", - "type": "publicize", - "description": "Publish your posts to your Facebook timeline or page.", - "genericon": { - "class": "facebook-alt", - "unicode": "\\f203" - }, - "icon": "http:\/\/i.wordpress.com\/wp-content\/admin-plugins\/publicize\/assets\/publicize-fb-2x.png", - "connect_URL": "https:\/\/public-api.wordpress.com\/connect\/?action=request&kr_nonce=9b9bc62d36&nonce=a14cc808a1&for=connect&service=facebook&blog=193189927&kr_blog_nonce=e12bbe845e&magic=keyring", - "multiple_external_user_ID_support": true, - "external_users_only": true, - "jetpack_support": true, - "jetpack_module_required": "publicize" - }, - "twitter": { - "ID": "twitter", - "label": "Twitter", - "type": "publicize", - "description": "Publish your posts to your Twitter account.", - "genericon": { - "class": "twitter", - "unicode": "\\f202" - }, - "icon": "http:\/\/i.wordpress.com\/wp-content\/admin-plugins\/publicize\/assets\/publicize-twitter-2x.png", - "connect_URL": "https:\/\/public-api.wordpress.com\/connect\/?action=request&kr_nonce=9b9bc62d36&nonce=7aa41a23e8&for=connect&service=twitter&blog=193189927&kr_blog_nonce=e12bbe845e&magic=keyring", - "multiple_external_user_ID_support": false, - "external_users_only": false, - "jetpack_support": true, - "jetpack_module_required": "publicize", - "status": "unsupported" - } - } -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift index 919f67aa7550..3bc1ef010461 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift @@ -14,95 +14,8 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { SharingServiceRemote(wordPressComRestApi: api) }() - private let publicizeServicesMockFilename = "sites-external-services.json" - // MARK: - Tests - func testGetPublicizeServicesV1_1() { - let url = service.path(forEndpoint: "meta/external-services", withVersion: ._1_1) - - service.getPublicizeServices(nil, failure: nil) - - XCTAssertTrue(api.getMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - - let typeParameter = (api.parametersPassedIn as? NSDictionary)?.value(forKey: "type") as? String - XCTAssertEqual(typeParameter, "publicize", "Incorrect type parameter") - } - - func testGetPublicizeServicesV2() { - let mockID = NSNumber(value: 10) - let expectation = expectation(description: "Publicize services v2.0 should succeed") - let pathToStub = "sites/\(mockID)/external-services" - let mockService = SharingServiceRemote(wordPressComRestApi: getRestApi()) - - stubRemoteResponse(pathToStub, filename: publicizeServicesMockFilename, contentType: .ApplicationJSON) - - mockService.getPublicizeServices(for: mockID) { publicizeServices in - guard let facebookService = publicizeServices.first(where: { $0.serviceID == "facebook" }) else { - XCTFail("Expected a RemotePublicizeService to exist") - return - } - XCTAssertTrue(facebookService.status.isEmpty) - - guard let twitterService = publicizeServices.first(where: { $0.serviceID == "twitter"}) else { - XCTFail("Expected a RemotePublicizeService to exist") - return - } - XCTAssertEqual(twitterService.status, "unsupported") - - expectation.fulfill() - } failure: { _ in - XCTFail("Failure block unexpectedly called") - } - - wait(for: [expectation], timeout: timeout) - } - - func testGetKeyringServices() { - let url = service.path(forEndpoint: "me/keyring-connections", withVersion: ._1_1) - - service.getKeyringConnections(nil, failure: nil) - - XCTAssertTrue(api.getMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - - func testGetPublicizeConnections() { - let mockID = NSNumber(value: 10) - let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections", withVersion: ._1_1) - - service.getPublicizeConnections(mockID, success: nil, failure: nil) - - XCTAssertTrue(api.getMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - - func testCreatePublicizeConnection() { - let mockID = NSNumber(value: 10) - let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections/new", withVersion: ._1_1) - - service.createPublicizeConnection(mockID, - keyringConnectionID: mockID, - externalUserID: nil, - success: nil, - failure: nil) - - XCTAssertTrue(api.postMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - - func testDeletePublicizeConnection() { - let mockID = NSNumber(value: 10) - let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections/\(mockID)/delete", - withVersion: ._1_1) - - service.deletePublicizeConnection(mockID, connectionID: mockID, success: nil, failure: nil) - - XCTAssertTrue(api.postMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - func testGetSharingButtonsForSite() { let mockID = NSNumber(value: 10) let url = service.path(forEndpoint: "sites/\(mockID)/sharing-buttons", withVersion: ._1_1) @@ -117,10 +30,12 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { let mockID = NSNumber(value: 10) let url = service.path(forEndpoint: "sites/\(mockID)/sharing-buttons", withVersion: ._1_1) - service.updateSharingButtonsForSite(mockID, - sharingButtons: [RemoteSharingButton](), - success: nil, - failure: nil) + service.updateSharingButtonsForSite( + mockID, + sharingButtons: [RemoteSharingButton](), + success: nil, + failure: nil + ) XCTAssertTrue(api.postMethodCalled, "Method was not called") XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift deleted file mode 100644 index f015c1bfdda4..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation -import XCTest -@testable import WordPressKit - -class JetpackSocialServiceRemoteTests: RemoteTestCase, RESTTestable { - let siteID = 1001 - let jetpackSocialWithPublicizeFilename = "jetpack-social-with-publicize.json" - let jetpackSocialWithoutPublicizeFilename = "jetpack-social-no-publicize.json" - let jetpackSocialErrorFilename = "jetpack-social-403.json" - - private var endpoint: String { - "sites/\(siteID)/jetpack-social" - } - - private lazy var remote: JetpackSocialServiceRemote = { - .init(wordPressComRestApi: getRestApi()) - }() - - // MARK: - Tests - - func testPublicizeInfoReturnValueForSitesWithPublicize() { - stubRemoteResponse(endpoint, filename: jetpackSocialWithPublicizeFilename, contentType: .ApplicationJSON) - let expect = expectation(description: "Jetpack Social request should succeed") - - remote.fetchPublicizeInfo(for: siteID) { result in - switch result { - case .success(let info): - guard let info else { - XCTFail("PublicizeInfo should exist") - return - } - - // test parsing correctness - XCTAssertEqual(info.shareLimit, 30) - XCTAssertEqual(info.toBePublicizedCount, 1) - XCTAssertEqual(info.sharedPostsCount, 15) - XCTAssertEqual(info.sharesRemaining, 14) - expect.fulfill() - - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - } - wait(for: [expect], timeout: timeout) - } - - func testPublicizeInfoReturnValueForSitesWithoutPublicize() { - stubRemoteResponse(endpoint, filename: jetpackSocialWithoutPublicizeFilename, contentType: .ApplicationJSON) - let expect = expectation(description: "Jetpack Social request should succeed") - - remote.fetchPublicizeInfo(for: siteID) { result in - switch result { - case .success(let info): - // for sites without publicize, the request should succeed with nil result. - XCTAssertNil(info) - expect.fulfill() - - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - } - wait(for: [expect], timeout: timeout) - } - - func testPublicizeInfoError() { - stubRemoteResponse(endpoint, filename: jetpackSocialErrorFilename, contentType: .ApplicationJSON, status: 403) - let expect = expectation(description: "Jetpack Social request should fail") - - remote.fetchPublicizeInfo(for: siteID) { result in - switch result { - case .success: - XCTFail("Unexpected success result") - case .failure: - expect.fulfill() - } - } - wait(for: [expect], timeout: timeout) - } -} diff --git a/WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift b/WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift deleted file mode 100644 index e5b46d8cf4ea..000000000000 --- a/WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift +++ /dev/null @@ -1,31 +0,0 @@ -import WordPressData - -/// Blog extension for methods related to Jetpack Social. -extension Blog { - // MARK: - Publicize - - /// Whether the blog has Social auto-sharing limited. - /// Note that sites hosted at WP.com has no Social sharing limitations. - var isSocialSharingLimited: Bool { - let hasUnlimitedSharing = (planActiveFeatures ?? []).contains(Constants.unlimitedSharingFeatureKey) - return !(isHostedAtWPcom || isAtomic || hasUnlimitedSharing) - } - - /// The auto-sharing limit information for the blog. - var sharingLimit: PublicizeInfo.SharingLimit? { - // For blogs with unlimited shares, return nil early. - // This is because the endpoint will still return sharing limits as if the blog doesn't have unlimited sharing. - guard isSocialSharingLimited else { - return nil - } - return publicizeInfo?.sharingLimit - } - - // MARK: - Private constants - - private enum Constants { - /// The feature key listed in the blog's plan's features. At the moment, `social-shares-1000` means unlimited - /// sharing, but in the future we might introduce a proper differentiation between 1000 and unlimited. - static let unlimitedSharingFeatureKey = "social-shares-1000" - } -} diff --git a/WordPress/Classes/Models/PublicizeConnection+Creation.swift b/WordPress/Classes/Models/PublicizeConnection+Creation.swift deleted file mode 100644 index 7c65733ba0cc..000000000000 --- a/WordPress/Classes/Models/PublicizeConnection+Creation.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import WordPressData -import WordPressKit - -extension PublicizeConnection { - - /// Composes a new `PublicizeConnection`, or updates an existing one, with - /// data represented by the passed `RemotePublicizeConnection`. - /// - /// - Parameter remoteConnection: The remote connection representing the publicize connection. - /// - /// - Returns: A `PublicizeConnection`. - /// - static func createOrReplace(from remoteConnection: RemotePublicizeConnection, in context: NSManagedObjectContext) -> PublicizeConnection { - let pubConnection = findPublicizeConnection(byID: remoteConnection.connectionID, in: context) - ?? NSEntityDescription.insertNewObject(forEntityName: PublicizeConnection.entityName(), - into: context) as! PublicizeConnection - - pubConnection.connectionID = remoteConnection.connectionID - pubConnection.dateExpires = remoteConnection.dateExpires - pubConnection.dateIssued = remoteConnection.dateIssued - pubConnection.externalDisplay = remoteConnection.externalDisplay - pubConnection.externalFollowerCount = remoteConnection.externalFollowerCount - pubConnection.externalID = remoteConnection.externalID - pubConnection.externalName = remoteConnection.externalName - pubConnection.externalProfilePicture = remoteConnection.externalProfilePicture - pubConnection.externalProfileURL = remoteConnection.externalProfileURL - pubConnection.keyringConnectionID = remoteConnection.keyringConnectionID - pubConnection.keyringConnectionUserID = remoteConnection.keyringConnectionUserID - pubConnection.label = remoteConnection.label - pubConnection.refreshURL = remoteConnection.refreshURL - pubConnection.service = remoteConnection.service - pubConnection.shared = remoteConnection.shared - pubConnection.status = remoteConnection.status - pubConnection.siteID = remoteConnection.siteID - pubConnection.userID = remoteConnection.userID - - return pubConnection - } - - /// Finds a cached `PublicizeConnection` by its `connectionID` - /// - /// - Parameter connectionID: The ID of the `PublicizeConnection`. - /// - /// - Returns: The requested `PublicizeConnection` or nil. - /// - private static func findPublicizeConnection(byID connectionID: NSNumber, in context: NSManagedObjectContext) -> PublicizeConnection? { - let request = NSFetchRequest(entityName: PublicizeConnection.classNameWithoutNamespaces()) - request.predicate = NSPredicate(format: "connectionID = %@", connectionID) - return try? context.fetch(request).first - } -} diff --git a/WordPress/Classes/Models/PublicizeService+Lookup.swift b/WordPress/Classes/Models/PublicizeService+Lookup.swift deleted file mode 100644 index fe29bada4c49..000000000000 --- a/WordPress/Classes/Models/PublicizeService+Lookup.swift +++ /dev/null @@ -1,48 +0,0 @@ -import CoreData -import WordPressData -import WordPressKit - -extension PublicizeService { - /// Finds a cached `PublicizeService` matching the specified service name. - /// - /// - Parameter name: The name of the service. This is the `serviceID` attribute for a `PublicizeService` object. - /// - /// - Returns: The requested `PublicizeService` or nil. - /// - static func lookupPublicizeServiceNamed(_ name: String, in context: NSManagedObjectContext) throws -> PublicizeService? { - let request = NSFetchRequest(entityName: PublicizeService.classNameWithoutNamespaces()) - request.predicate = NSPredicate(format: "serviceID = %@", name) - return try context.fetch(request).first - } - - @objc(lookupPublicizeServiceNamed:inContext:) - public static func objc_lookupPublicizeServiceNamed(_ name: String, in context: NSManagedObjectContext) -> PublicizeService? { - try? lookupPublicizeServiceNamed(name, in: context) - } - - /// Returns an array of all cached `PublicizeService` objects. - /// - /// - Returns: An array of `PublicizeService`. The array is empty if no objects are cached. - /// - @objc(allPublicizeServicesInContext:error:) - public static func allPublicizeServices(in context: NSManagedObjectContext) throws -> [PublicizeService] { - let request = NSFetchRequest(entityName: PublicizeService.classNameWithoutNamespaces()) - let sortDescriptor = NSSortDescriptor(key: "order", ascending: true) - request.sortDescriptors = [sortDescriptor] - return try context.fetch(request) - } - - /// Returns an array of all cached `PublicizeService` objects that are supported by Jetpack Social. - /// - /// Note that services without a `status` field from the remote will be marked as supported by default. - /// - /// - Parameter context: The managed object context. - /// - Returns: An array of `PublicizeService`. The array is empty if no objects are cached. - static func allSupportedServices(in context: NSManagedObjectContext) throws -> [PublicizeService] { - let request = NSFetchRequest(entityName: PublicizeService.classNameWithoutNamespaces()) - let sortDescriptor = NSSortDescriptor(key: "order", ascending: true) - request.predicate = NSPredicate(format: "status == %@", Self.defaultStatus) - request.sortDescriptors = [sortDescriptor] - return try context.fetch(request) - } -} diff --git a/WordPress/Classes/Services/BlogService.m b/WordPress/Classes/Services/BlogService.m index 9df30485a4e6..5b471d2a223b 100644 --- a/WordPress/Classes/Services/BlogService.m +++ b/WordPress/Classes/Services/BlogService.m @@ -131,37 +131,6 @@ - (void)syncBlogAndAllMetadata:(Blog *)blog completionHandler:(void (^)(void))co dispatch_group_leave(syncGroup); }]; - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:self.coreDataStack]; - dispatch_group_enter(syncGroup); - [sharingService syncPublicizeConnectionsForBlog:blog - success:^{ - dispatch_group_leave(syncGroup); - } - failure:^(NSError *error) { - DDLogError(@"Failed syncing publicize connections for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - SharingService *publicizeService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - dispatch_group_enter(syncGroup); - [publicizeService syncPublicizeServicesForBlog:blog success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nullable error) { - DDLogError(@"Failed syncing publicize services for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - if ([RemoteFeature enabled:RemoteFeatureFlagJetpackSocialImprovements] && blog.dotComID != nil) { - JetpackSocialService *jetpackSocialService = [[JetpackSocialService alloc] initWithContextManager:ContextManager.sharedInstance]; - dispatch_group_enter(syncGroup); - [jetpackSocialService syncSharingLimitWithDotComID:blog.dotComID success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nullable error) { - DDLogError(@"Failed syncing publicize sharing limit for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - } - dispatch_group_enter(syncGroup); [remote getAllAuthorsWithSuccess:^(NSArray *users) { [self updateMultiAuthor:users forBlog:blogObjectID completionHandler:^{ diff --git a/WordPress/Classes/Services/JetpackSocialService.swift b/WordPress/Classes/Services/JetpackSocialService.swift deleted file mode 100644 index 2594bfbfac97..000000000000 --- a/WordPress/Classes/Services/JetpackSocialService.swift +++ /dev/null @@ -1,111 +0,0 @@ -import CoreData -import WordPressData -import WordPressKit - -@objc public class JetpackSocialService: NSObject { - - // MARK: Properties - - private let coreDataStack: CoreDataStackSwift - - private lazy var remote: JetpackSocialServiceRemote = { - let api = coreDataStack.performQuery { context in - return WordPressComRestApi.defaultV2Api(in: context) - } - return .init(wordPressComRestApi: api) - }() - - // MARK: Methods - - /// Init method for Objective-C. - /// - @objc public init(contextManager: ContextManager) { - self.coreDataStack = contextManager - } - - init(coreDataStack: CoreDataStackSwift = ContextManager.shared) { - self.coreDataStack = coreDataStack - } - - /// Fetches and updates the Publicize information for the site associated with the `blogID`. - /// The method returns a value type that contains the remaining usage of Social auto-sharing and the maximum limit for the associated site. - /// - /// - Note: If the returned result is a success with nil sharing limit, it's likely that the blog is hosted on WP.com, and has no Social sharing limitations. - /// - /// Furthermore, even if the sharing limit exists, it may not be applicable for the blog since the user might have purchased a product that ignores this limitation. - /// - /// - Parameters: - /// - blogID: The ID of the blog. - /// - completion: Closure that's called after the sync process completes. - func syncSharingLimit(for blogID: Int, completion: @escaping (Result) -> Void) { - // allow `self` to be retained inside this closure so the completion block will always be executed. - remote.fetchPublicizeInfo(for: blogID) { result in - switch result { - case .success(let remotePublicizeInfo): - self.coreDataStack.performAndSave({ context -> PublicizeInfo.SharingLimit? in - guard let blog = try Blog.lookup(withID: blogID, in: context) else { - // unexpected to fall into this case, since the API should return an error response. - throw ServiceError.blogNotFound(id: blogID) - } - - if let remotePublicizeInfo, - let newOrExistingInfo = blog.publicizeInfo ?? PublicizeInfo.newObject(in: context) { - // add or update the publicizeInfo for the blog. - newOrExistingInfo.configure(with: remotePublicizeInfo) - blog.publicizeInfo = newOrExistingInfo - } else if let existingPublicizeInfo = blog.publicizeInfo { - // if the remote object is nil, delete the blog's publicizeInfo if it exists. - context.delete(existingPublicizeInfo) - blog.publicizeInfo = nil - } - - return blog.publicizeInfo?.sharingLimit - }, completion: { completion($0) }, on: .main) - - case .failure(let error): - completion(.failure(error)) - } - } - } - - /// Sync method for Objective-C. - /// Fetches the latest state of the blog's Publicize auto-sharing limits and stores them locally. - /// - /// - Parameters: - /// - dotComID: The WP.com ID of the blog. - /// - success: Closure called when the sync process succeeds. - /// - failure: Closure called when the sync process fails. - @objc public func syncSharingLimit(dotComID: NSNumber?, - success: (() -> Void)?, - failure: ((NSError?) -> Void)?) { - guard let blogID = dotComID?.intValue else { - failure?(ServiceError.nilBlogID as NSError) - return - } - - syncSharingLimit(for: blogID, completion: { result in - switch result { - case .success: - success?() - case .failure(let error): - failure?(error as NSError) - } - }) - } - - // MARK: Errors - - enum ServiceError: LocalizedError { - case blogNotFound(id: Int) - case nilBlogID - - var errorDescription: String? { - switch self { - case .blogNotFound(let id): - return "Blog with id: \(id) was unexpectedly not found." - case .nilBlogID: - return "Blog ID is unexpectedly nil." - } - } - } -} diff --git a/WordPress/Classes/Services/PostHelper+JetpackSocial.swift b/WordPress/Classes/Services/PostHelper+JetpackSocial.swift deleted file mode 100644 index 659be73c2122..000000000000 --- a/WordPress/Classes/Services/PostHelper+JetpackSocial.swift +++ /dev/null @@ -1,121 +0,0 @@ -import WordPressData - -extension PostHelper { - typealias StringDictionary = [String: String] - typealias Keys = Post.Constants - typealias SkipPrefix = Post.PublicizeMetadataSkipPrefix - - /// Returns a dictionary format for the `Post`'s `disabledPublicizeConnection` property based on the given metadata. - /// - /// This will try to handle both Publicize skip key formats, `_wpas_skip_{keyringID}` and `_wpas_skip_publicize_{connectionID`. - /// - /// There's a possibility that the `keyringID` obtained from remote doesn't match with any of the `PublicizeConnection` - /// that's stored locally, perhaps due to the app being out of sync. In this case, we'll fall back to using the old format. - /// - /// - Parameters: - /// - post: The associated `Post` object. Optional because Obj-C shouldn't be trusted. - /// - metadata: The metadata dictionary for the post. Optional because Obj-C shouldn't be trusted. - /// - Returns: A dictionary for the `Post`'s `disabledPublicizeConnections` property. - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept to avoid a Core Data migration and for remaining legacy references. - @objc(disabledPublicizeConnectionsForPost:andMetadata:) - static func disabledPublicizeConnections( - for post: AbstractPost?, - metadata: [[String: Any]]? - ) -> [NSNumber: StringDictionary] { - guard let post, let metadata else { - return [:] - } - - return - metadata - .compactMap { $0 as? [String: String] } - .filter { $0[Keys.publicizeKeyKey]?.hasPrefix(SkipPrefix.keyring.rawValue) ?? false } - .reduce(into: [NSNumber: StringDictionary]()) { partialResult, entry in - // every metadata entry should have a key. - guard let key = entry[Keys.publicizeKeyKey] else { - return - } - - func getDictionaryID() -> Int? { - guard let prefixType = SkipPrefix.prefix(of: key) else { - return nil - } - - switch prefixType { - case .keyring: - return Int(key.removingPrefix(SkipPrefix.keyring.rawValue)) - - case .connection: - // If the key uses the new format, try to find an existing `PublicizeConnection` matching - // the connectionID, and return its keyringID. - let entryConnectionID = Int(key.removingPrefix(SkipPrefix.connection.rawValue)) - - guard let connections = post.blog.connections, - let connectionID = entryConnectionID, - let connection = connections.first(where: { $0.connectionID.intValue == connectionID }) - else { - /// Otherwise, fall back to the connectionID extracted from the metadata key. - /// Note that entries with `connectionID` won't be detected by the Post's - /// `publicizeConnectionDisabledForKeyringID` method. - /// - /// However, the Publicize methods in `Post.swift` will attempt to update the key into - /// its `keyringID`, since the `PublicizeConnection` object is guaranteed to exist. - return entryConnectionID - } - - return connection.keyringConnectionID.intValue - } - } - - if let id = getDictionaryID() { - partialResult[NSNumber(value: id)] = entry - } - } - } - - /// Converts the `Post`'s `disabledPublicizeConnections` dictionary to metadata entries. - /// - /// - Parameter post: The associated `Post` object. - /// - Returns: An array of metadata dictionaries representing the `Post`'s disabled connections. - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept to avoid a Core Data migration and for remaining legacy references. - @objc(publicizeMetadataEntriesForPost:) - static func publicizeMetadataEntries(for post: Post?) -> [StringDictionary] { - guard let post, - let disabledConnectionsDictionary = post.disabledPublicizeConnections - else { - return [] - } - - return disabledConnectionsDictionary.compactMap { (keyringID: NSNumber, entry: StringDictionary) in - // The previous implementation didn't properly parse `_wpas_skip_publicize_` keys, causing it - // to use 0 as the dictionary key. Although this will be ignored by the server, let's make sure - // it's not sent to the remote any longer. - guard keyringID.intValue > 0 else { - return nil - } - - // Each entry should have a `value`, an optional `id`, and an optional `key`. - // If the entry already has a key, then there's nothing to do; Pass the dictionary as is. - if let _ = entry[Keys.publicizeKeyKey] { - return entry - } - - // If the key doesn't exist, this means that the dictionary is still using the old format. - // Try to add a key with the new format ONLY if the metadata hasn't been synced to the remote. - let metadataKeyValue: String = { - guard entry[Keys.publicizeIdKey] == nil, - let connections = post.blog.connections, - let connection = connections.first(where: { $0.keyringConnectionID == keyringID }) - else { - // Fall back to the old keyring format. - return "\(SkipPrefix.keyring.rawValue)\(keyringID)" - } - return "\(SkipPrefix.connection.rawValue)\(connection.connectionID)" - }() - - return entry.merging([Keys.publicizeKeyKey: metadataKeyValue]) { _, newValue in newValue } - } - } -} diff --git a/WordPress/Classes/Services/PostHelper.m b/WordPress/Classes/Services/PostHelper.m index 6e8d9fd0813a..976bb96e3ea2 100644 --- a/WordPress/Classes/Services/PostHelper.m +++ b/WordPress/Classes/Services/PostHelper.m @@ -89,8 +89,6 @@ + (void)updatePost:(AbstractPost *)post withRemotePost:(RemotePost *)remotePost [self updatePost:postPost withRemoteCategories:remotePost.categories inContext:managedObjectContext]; NSString *publicID = nil; - NSString *publicizeMessage = nil; - NSString *publicizeMessageID = nil; if (remotePost.metadata) { NSDictionary *latitudeDictionary = [self dictionaryWithKey:@"geo_latitude" inMetadata:remotePost.metadata]; NSDictionary *longitudeDictionary = [self dictionaryWithKey:@"geo_longitude" inMetadata:remotePost.metadata]; @@ -103,14 +101,8 @@ + (void)updatePost:(AbstractPost *)post withRemotePost:(RemotePost *)remotePost coord.longitude = [longitude doubleValue]; publicID = [geoPublicDictionary stringForKey:@"id"]; } - NSDictionary *publicizeMessageDictionary = [self dictionaryWithKey:@"_wpas_mess" inMetadata:remotePost.metadata]; - publicizeMessage = [publicizeMessageDictionary stringForKey:@"value"]; - publicizeMessageID = [publicizeMessageDictionary stringForKey:@"id"]; } postPost.publicID = publicID; - postPost.publicizeMessage = publicizeMessage; - postPost.publicizeMessageID = publicizeMessageID; - postPost.disabledPublicizeConnections = [self disabledPublicizeConnectionsForPost:post andMetadata:remotePost.metadata]; } } diff --git a/WordPress/Classes/Services/SharingService.swift b/WordPress/Classes/Services/SharingService.swift index 51b771c4d6fc..b53a0ce243b6 100644 --- a/WordPress/Classes/Services/SharingService.swift +++ b/WordPress/Classes/Services/SharingService.swift @@ -1,10 +1,8 @@ import Foundation import WordPressData -import WordPressShared import WordPressKit -/// SharingService is responsible for wrangling publicize services, publicize -/// connections, and keyring connections. +/// SharingService is responsible for wrangling sharing buttons. /// @objc public class SharingService: NSObject { let SharingAPIErrorNotFound = "not_found" @@ -23,348 +21,6 @@ import WordPressKit self.coreDataStack = coreDataStack } - // MARK: - Publicize Related Methods - - /// Syncs the list of Publicize services. The list is expected to very rarely change. - /// - /// - Parameters: - /// - blog: The `Blog` for which to sync publicize services - /// - success: An optional success block accepting no parameters - /// - failure: An optional failure block accepting an `NSError` parameter - /// - @objc public func syncPublicizeServicesForBlog(_ blog: Blog, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { - guard let remote = remoteForBlog(blog), - let blogID = blog.dotComID else { - failure?(nil) - return - } - - remote.getPublicizeServices(for: blogID, success: { remoteServices in - // Process the results - self.mergePublicizeServices(remoteServices, success: success) - }, failure: failure) - } - - /// Fetches the current user's list of keyring connections. Nothing is saved to core data. - /// The success block should accept an array of `KeyringConnection` objects. - /// - /// - Parameters: - /// - blog: The `Blog` for which to sync keyring connections - /// - success: An optional success block accepting an array of `KeyringConnection` objects - /// - failure: An optional failure block accepting an `NSError` parameter - /// - @objc public func fetchKeyringConnectionsForBlog(_ blog: Blog, success: (([KeyringConnection]) -> Void)?, failure: ((NSError?) -> Void)?) { - guard let remote = remoteForBlog(blog) else { - return - } - remote.getKeyringConnections({ keyringConnections in - // Just return the result - success?(keyringConnections) - }, - failure: failure) - } - - /// Creates a new publicize connection for the specified `Blog`, using the specified - /// keyring. Optionally the connection can target a particular external user account. - /// - /// - Parameters - /// - blog: The `Blog` for which to sync publicize connections - /// - keyring: The `KeyringConnection` to use - /// - externalUserID: An optional string representing a user ID on the external service. - /// - success: An optional success block accepting a `PublicizeConnection` parameter. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func createPublicizeConnectionForBlog(_ blog: Blog, - keyring: KeyringConnection, - externalUserID: String?, - success: ((PublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)?) { - let blogObjectID = blog.objectID - guard let remote = remoteForBlog(blog) else { - return - } - let dotComID = blog.dotComID! - remote.createPublicizeConnection( - dotComID, - keyringConnectionID: keyring.keyringID, - externalUserID: externalUserID, - success: { remoteConnection in - let properties = [ - "service": keyring.service - ] - WPAppAnalytics.track(.sharingPublicizeConnected, properties: properties, blogID: dotComID) - - self.coreDataStack.performAndSave({ context -> NSManagedObjectID in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) - }, completion: { result in - let transformed = result.flatMap { objectID in - Result { - let object = try self.coreDataStack.mainContext.existingObject(with: objectID) - return object as! PublicizeConnection - } - } - switch transformed { - case let .success(object): - success?(object) - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, on: .main) - }, - failure: { (error: NSError?) in - failure?(error) - }) - } - - /// Update the specified `PublicizeConnection`. The update to core data is performed - /// optimistically. In case of failure the original value will be restored. - /// - /// - Parameters: - /// - shared: True if the connection should be shared with all users of the blog. - /// - pubConn: The `PublicizeConnection` to update - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func updateSharedForBlog( - _ blog: Blog, - shared: Bool, - forPublicizeConnection pubConn: PublicizeConnection, - success: (() -> Void)?, - failure: ((NSError?) -> Void)? - ) { - typealias PubConnUpdateResult = (oldValue: Bool, siteID: NSNumber, connectionID: NSNumber, service: String, remote: SharingServiceRemote?) - - let blogObjectID = blog.objectID - coreDataStack.performAndSave({ context -> PubConnUpdateResult in - let blogInContext = try context.existingObject(with: blogObjectID) as! Blog - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - let oldValue = pubConnInContext.shared - pubConnInContext.shared = shared - return ( - oldValue: oldValue, - siteID: pubConnInContext.siteID, - connectionID: pubConnInContext.connectionID, - service: pubConnInContext.service, - remote: self.remoteForBlog(blogInContext) - ) - }, completion: { result in - switch result { - case let .success(value): - if value.oldValue == shared { - success?() - return - } - - value.remote?.updatePublicizeConnectionWithID( - value.connectionID, - shared: shared, - forSite: value.siteID, - success: { remoteConnection in - let properties = [ - "service": value.service, - "is_site_wide": NSNumber(value: shared).stringValue - ] - WPAppAnalytics.track(.sharingPublicizeConnectionAvailableToAllChanged, properties: properties, blogID: value.siteID) - - self.coreDataStack.performAndSave({ context in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) - }, completion: { result in - switch result { - case .success: - success?() - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, on: .main) - }, - failure: { (error: NSError?) in - self.coreDataStack.performAndSave({ context in - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - pubConnInContext.shared = value.oldValue - }, completion: { _ in - failure?(error) - }, on: .main) - } - ) - case let .failure(error): - failure?(error as NSError) - } - }, on: .main) - } - - /// Update the specified `PublicizeConnection`. The update to core data is performed - /// optimistically. In case of failure the original value will be restored. - /// - /// - Parameters: - /// - externalID: True if the connection should be shared with all users of the blog. - /// - pubConn: The `PublicizeConnection` to update - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func updateExternalID(_ externalID: String, - forBlog blog: Blog, - forPublicizeConnection pubConn: PublicizeConnection, - success: (() -> Void)?, - failure: ((NSError?) -> Void)?) { - if pubConn.externalID == externalID { - success?() - return - } - - let blogObjectID = blog.objectID - let siteID = pubConn.siteID - guard let remote = remoteForBlog(blog) else { - return - } - remote.updatePublicizeConnectionWithID(pubConn.connectionID, - externalID: externalID, - forSite: siteID, - success: { remoteConnection in - self.coreDataStack.performAndSave({ context in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) - }, completion: { result in - switch result { - case .success: - success?() - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, on: .main) - }, - failure: failure) - } - - /// Deletes the specified `PublicizeConnection`. The delete from core data is performed - /// optimistically. The caller's `failure` block should be responsible for resyncing - /// the deleted connection. - /// - /// - Parameters: - /// - pubConn: The `PublicizeConnection` to delete - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func deletePublicizeConnectionForBlog(_ blog: Blog, pubConn: PublicizeConnection, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { - // optimistically delete the connection locally. - coreDataStack.performAndSave({ context in - let blogInContext = try context.existingObject(with: blog.objectID) as! Blog - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - - let siteID = pubConnInContext.siteID - context.delete(pubConnInContext) - return (siteID, pubConnInContext.connectionID, pubConnInContext.service, self.remoteForBlog(blogInContext)) - }, completion: { result in - switch result { - case let .success((siteID, connectionID, service, remote)): - remote?.deletePublicizeConnection( - siteID, - connectionID: connectionID, - success: { - let properties = [ - "service": service - ] - WPAppAnalytics.track(.sharingPublicizeDisconnected, properties: properties, blogID: siteID) - success?() - }, - failure: { (error: NSError?) in - if let errorCode = error?.userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String { - if errorCode == self.SharingAPIErrorNotFound { - // This is a special situation. If the call to disconnect the service returns not_found then the service - // has probably already been disconnected and the call was made with stale data. - // Assume this is the case and treat this error as a successful disconnect. - success?() - return - } - } - failure?(error) - } - ) - case let .failure(error): - failure?(error as NSError) - } - }, on: .main) - } - - // MARK: - Private PublicizeService Methods - - /// Called when syncing Publicize services. Merges synced and cached data, removing - /// anything that does not exist on the server. Saves the context. - /// - /// - Parameters - /// - remoteServices: An array of `RemotePublicizeService` objects to merge. - /// - success: An optional callback block to be performed when core data has saved the changes. - /// - private func mergePublicizeServices(_ remoteServices: [RemotePublicizeService], success: (() -> Void)? ) { - coreDataStack.performAndSave({ context in - let currentPublicizeServices = (try? PublicizeService.allPublicizeServices(in: context)) ?? [] - - // Create or update based on the contents synced. - let servicesToKeep = remoteServices.map { remoteService -> PublicizeService in - self.createOrReplaceFromRemotePublicizeService(remoteService, in: context) - } - - // Delete any cached PublicizeServices that were not synced. - for pubService in currentPublicizeServices { - if !servicesToKeep.contains(pubService) { - context.delete(pubService) - } - } - }, completion: success, on: .main) - } - - /// Composes a new `PublicizeService`, or updates an existing one, with data represented by the passed `RemotePublicizeService`. - /// - /// - Parameter remoteService: The remote publicize service representing a `PublicizeService` - /// - /// - Returns: A `PublicizeService`. - /// - private func createOrReplaceFromRemotePublicizeService(_ remoteService: RemotePublicizeService, in context: NSManagedObjectContext) -> PublicizeService { - var pubService = try? PublicizeService.lookupPublicizeServiceNamed(remoteService.serviceID, in: context) - if pubService == nil { - pubService = NSEntityDescription.insertNewObject(forEntityName: PublicizeService.entityName(), - into: context) as? PublicizeService - } - pubService?.connectURL = remoteService.connectURL - pubService?.detail = remoteService.detail - pubService?.externalUsersOnly = remoteService.externalUsersOnly - pubService?.icon = remoteService.icon - pubService?.jetpackModuleRequired = remoteService.jetpackModuleRequired - pubService?.jetpackSupport = remoteService.jetpackSupport - pubService?.label = remoteService.label - pubService?.multipleExternalUserIDSupport = remoteService.multipleExternalUserIDSupport - pubService?.order = remoteService.order - pubService?.serviceID = remoteService.serviceID - pubService?.type = remoteService.type - pubService?.status = (remoteService.status.isEmpty ? PublicizeService.defaultStatus : remoteService.status) - - return pubService! - } - - // MARK: - Private PublicizeConnection Methods - - /// Composes a new `PublicizeConnection`, with data represented by the passed `RemotePublicizeConnection`. - /// Throws an error if unable to find a `Blog` for the `blogObjectID` - /// - /// - Parameter blogObjectID: And `NSManagedObjectID` for for a `Blog` entity. - /// - /// - Returns: A `PublicizeConnection`. - /// - private func createOrReplacePublicizeConnectionForBlogWithObjectID( - _ blogObjectID: NSManagedObjectID, - remoteConnection: RemotePublicizeConnection, - in context: NSManagedObjectContext - ) throws -> NSManagedObjectID { - let blog = try context.existingObject(with: blogObjectID) as! Blog - let pubConn = PublicizeConnection.createOrReplace(from: remoteConnection, in: context) - pubConn.blog = blog - - try context.obtainPermanentIDs(for: [pubConn]) - - return pubConn.objectID - } - // MARK: Sharing Button Related Methods /// Syncs `SharingButton`s for the specified wpcom blog. @@ -380,11 +36,13 @@ import WordPressKit return } - remote.getSharingButtonsForSite(blog.dotComID!, + remote.getSharingButtonsForSite( + blog.dotComID!, success: { (remoteButtons: [RemoteSharingButton]) in self.mergeSharingButtonsForBlog(blogObjectID, remoteSharingButtons: remoteButtons, onComplete: success) }, - failure: failure) + failure: failure + ) } /// Pushes changes to the specified blog's `SharingButton`s back up to the blog. @@ -395,18 +53,25 @@ import WordPressKit /// - success: An optional success block accepting no parameters. /// - failure: An optional failure block accepting an `NSError` parameter. /// - @objc public func updateSharingButtonsForBlog(_ blog: Blog, sharingButtons: [SharingButton], success: (() -> Void)?, failure: ((NSError?) -> Void)?) { + @objc public func updateSharingButtonsForBlog( + _ blog: Blog, + sharingButtons: [SharingButton], + success: (() -> Void)?, + failure: ((NSError?) -> Void)? + ) { let blogObjectID = blog.objectID guard let remote = remoteForBlog(blog) else { return } - remote.updateSharingButtonsForSite(blog.dotComID!, + remote.updateSharingButtonsForSite( + blog.dotComID!, sharingButtons: remoteShareButtonsFromShareButtons(sharingButtons), success: { (remoteButtons: [RemoteSharingButton]) in self.mergeSharingButtonsForBlog(blogObjectID, remoteSharingButtons: remoteButtons, onComplete: success) }, - failure: failure) + failure: failure + ) } /// Called when syncing sharing buttons. Merges synced and cached data, removing @@ -417,42 +82,57 @@ import WordPressKit /// - remoteSharingButtons: An array of `RemoteSharingButton` objects to merge. /// - onComplete: An optional callback block to be performed when core data has saved the changes. /// - private func mergeSharingButtonsForBlog(_ blogObjectID: NSManagedObjectID, remoteSharingButtons: [RemoteSharingButton], onComplete: (() -> Void)?) { - coreDataStack.performAndSave({ context in - let blog = try context.existingObject(with: blogObjectID) as! Blog + private func mergeSharingButtonsForBlog( + _ blogObjectID: NSManagedObjectID, + remoteSharingButtons: [RemoteSharingButton], + onComplete: (() -> Void)? + ) { + coreDataStack.performAndSave( + { context in + let blog = try context.existingObject(with: blogObjectID) as! Blog - let currentSharingbuttons = try SharingButton.allSharingButtons(for: blog, in: context) + let currentSharingbuttons = try SharingButton.allSharingButtons(for: blog, in: context) - // Create or update based on the contents synced. - let buttonsToKeep = remoteSharingButtons.map { remoteButton -> SharingButton in - return self.createOrReplaceFromRemoteSharingButton(remoteButton, blog: blog, in: context) - } + // Create or update based on the contents synced. + let buttonsToKeep = remoteSharingButtons.map { remoteButton -> SharingButton in + self.createOrReplaceFromRemoteSharingButton(remoteButton, blog: blog, in: context) + } - // Delete any cached PublicizeServices that were not synced. - for button in currentSharingbuttons { - if !buttonsToKeep.contains(button) { - context.delete(button) + // Delete any cached SharingButtons that were not synced. + for button in currentSharingbuttons { + if !buttonsToKeep.contains(button) { + context.delete(button) + } } - } - }, completion: { _ in - onComplete?() - }, on: .main) + }, + completion: { _ in + onComplete?() + }, + on: .main + ) } /// Composes a new `SharingButton`, or updates an existing one, with /// data represented by the passed `RemoteSharingButton`. /// /// - Parameters: - /// - remoteButton: The remote connection representing the publicize connection. + /// - remoteButton: The remote sharing button to create or update from. /// - blog: The `Blog` that owns or will own the button. /// /// - Returns: A `SharingButton`. /// - private func createOrReplaceFromRemoteSharingButton(_ remoteButton: RemoteSharingButton, blog: Blog, in context: NSManagedObjectContext) -> SharingButton { + private func createOrReplaceFromRemoteSharingButton( + _ remoteButton: RemoteSharingButton, + blog: Blog, + in context: NSManagedObjectContext + ) -> SharingButton { var shareButton = try? SharingButton.lookupSharingButton(byID: remoteButton.buttonID, for: blog, in: context) if shareButton == nil { - shareButton = NSEntityDescription.insertNewObject(forEntityName: SharingButton.entityName(), - into: context) as? SharingButton + shareButton = + NSEntityDescription.insertNewObject( + forEntityName: SharingButton.entityName(), + into: context + ) as? SharingButton } shareButton?.buttonID = remoteButton.buttonID @@ -475,7 +155,7 @@ import WordPressKit /// - Returns: An array of `RemoteSharingButton` objects. /// private func remoteShareButtonsFromShareButtons(_ shareButtons: [SharingButton]) -> [RemoteSharingButton] { - return shareButtons.map { shareButton -> RemoteSharingButton in + shareButtons.map { shareButton -> RemoteSharingButton in let btn = RemoteSharingButton() btn.buttonID = shareButton.buttonID btn.name = shareButton.name diff --git a/WordPress/Classes/Services/SharingSyncService.swift b/WordPress/Classes/Services/SharingSyncService.swift deleted file mode 100644 index 603eb602de74..000000000000 --- a/WordPress/Classes/Services/SharingSyncService.swift +++ /dev/null @@ -1,124 +0,0 @@ -import Foundation -import WordPressData -import WordPressShared -import WordPressKit - -/// SharingService is responsible for wrangling publicize services, publicize -/// connections, and keyring connections. -/// -@objc public class SharingSyncService: NSObject { - - let coreDataStack: CoreDataStack - - @objc public init(coreDataStack: CoreDataStack) { - self.coreDataStack = coreDataStack - } - - /// Returns the remote to use with the service. - /// - /// - Parameter blog: The blog to use for the rest api. - /// - private func remoteForBlog(_ blog: Blog) -> SharingServiceRemote? { - guard let api = blog.wordPressComRestApi else { - return nil - } - - return SharingServiceRemote(wordPressComRestApi: api) - } - - /// Syncs Publicize connections for the specified wpcom blog. - /// - /// - Parameters: - /// - blog: The `Blog` for which to sync publicize connections - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an `NSError` parameter. - /// - @objc open func syncPublicizeConnectionsForBlog(_ blog: Blog, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { - guard let remote = remoteForBlog(blog), - let blogID = blog.dotComID else { - failure?(Error.siteWithNoRemote as NSError) - return - } - - remote.getPublicizeConnections(blogID, success: { [blogObjectID = blog.objectID] remoteConnections in - let currentUserID: NSNumber = self.coreDataStack.performQuery { context in - guard let blog = try? context.existingObject(with: blogObjectID) as? Blog, - let accountID = blog.account?.userID else { - return NSNumber(value: 0) - } - return accountID - } - - // Ensure that we're only processing shared or owned connections - let authorizedConnections = remoteConnections.filter { $0.shared || $0.userID.isEqual(to: currentUserID) } - - // Process the results - self.mergePublicizeConnectionsForBlog(blogObjectID, remoteConnections: authorizedConnections, onComplete: success) - }, failure: failure) - } - - /// Called when syncing Publicize connections. Merges synced and cached data, removing - /// anything that does not exist on the server. Saves the context. - /// - /// - Parameters: - /// - blogObjectID: the NSManagedObjectID of a `Blog` - /// - remoteConnections: An array of `RemotePublicizeConnection` objects to merge. - /// - onComplete: An optional callback block to be performed when core data has saved the changes. - /// - private func mergePublicizeConnectionsForBlog(_ blogObjectID: NSManagedObjectID, remoteConnections: [RemotePublicizeConnection], onComplete: (() -> Void)?) { - coreDataStack.performAndSave({ context in - var blog: Blog - do { - blog = try context.existingObject(with: blogObjectID) as! Blog - } catch let error as NSError { - DDLogError("Error fetching Blog: \(error)") - // Because of the error we'll bail early, but we still need to call - // the success callback if one was passed. - return - } - - let currentPublicizeConnections = self.allPublicizeConnections(for: blog, in: context) - - // Create or update based on the contents synced. - let connectionsToKeep = remoteConnections.map { remoteConnection -> PublicizeConnection in - let pubConnection = PublicizeConnection.createOrReplace(from: remoteConnection, in: context) - pubConnection.blog = blog - return pubConnection - } - - // Delete any cached PublicizeServices that were not synced. - for pubConnection in currentPublicizeConnections { - if !connectionsToKeep.contains(pubConnection) { - context.delete(pubConnection) - } - } - }, completion: { onComplete?() }, on: .main) - } - - /// Returns an array of all cached `PublicizeConnection` objects. - /// - /// - Parameters - /// - blog: A `Blog` object - /// - /// - Returns: An array of `PublicizeConnection`. The array is empty if no objects are cached. - /// - private func allPublicizeConnections(for blog: Blog, in context: NSManagedObjectContext) -> [PublicizeConnection] { - let request = NSFetchRequest(entityName: PublicizeConnection.classNameWithoutNamespaces()) - request.predicate = NSPredicate(format: "blog = %@", blog) - - var connections: [PublicizeConnection] - do { - connections = try context.fetch(request) as! [PublicizeConnection] - } catch let error as NSError { - DDLogError("Error fetching Publicize Connections: \(error.localizedDescription)") - connections = [] - } - - return connections - } - - // Error for failure states - enum Error: Swift.Error { - case siteWithNoRemote - } -} diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index c4ef1d2696c9..3a6ec9d5ac93 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -33,7 +33,6 @@ #import "SettingsMultiTextViewController.h" #import "SettingTableViewCell.h" #import "SettingsTextViewController.h" -#import "SharingViewController.h" #import "SiteSettingsViewController.h" #import "StatsViewController.h" #import "SuggestionsTableView.h" diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index cd9caac4ee95..3057e96b9dc2 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -28,7 +28,6 @@ public enum FeatureFlag: Int, CaseIterable { case statsAds case customPostTypes case cptPostsAndPages - case socialSharingV2 case mediaLibraryV2 /// Returns a boolean indicating if the feature is enabled. @@ -91,8 +90,6 @@ public enum FeatureFlag: Int, CaseIterable { return BuildConfiguration.current == .debug case .cptPostsAndPages: return BuildConfiguration.current == .debug - case .socialSharingV2: - return BuildConfiguration.current == .debug case .mediaLibraryV2: return BuildConfiguration.current == .debug } @@ -139,7 +136,6 @@ extension FeatureFlag { case .statsAds: "Stats Ads Tab" case .customPostTypes: "Custom Post Types" case .cptPostsAndPages: "Custom Post Types: Posts and Pages" - case .socialSharingV2: "Social Sharing v2" case .mediaLibraryV2: "Media Library v2" } } diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index 68d0e5c1431c..7618900e2fca 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -19,7 +19,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { case bloggingPromptsSocial case siteEditorMVP case contactSupportChatbot - case jetpackSocialImprovements case domainManagement case dynamicDashboardCards case plansInSiteCreation @@ -68,8 +67,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return true case .contactSupportChatbot: return false - case .jetpackSocialImprovements: - return AppConfiguration.isJetpack case .domainManagement: return false case .dynamicDashboardCards: @@ -130,8 +127,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return "site_editor_mvp" case .contactSupportChatbot: return "contact_support_chatbot" - case .jetpackSocialImprovements: - return "jetpack_social_improvements_v1" case .domainManagement: return "domain_management" case .dynamicDashboardCards: @@ -191,8 +186,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return "Site Editor MVP" case .contactSupportChatbot: return "Contact Support via DocsBot" - case .jetpackSocialImprovements: - return "Jetpack Social Improvements v1" case .domainManagement: return "Domain Management" case .dynamicDashboardCards: diff --git a/WordPress/Classes/Utility/ObjCBridge.swift b/WordPress/Classes/Utility/ObjCBridge.swift index 8f95ee07cb8a..096fe2b4ecf7 100644 --- a/WordPress/Classes/Utility/ObjCBridge.swift +++ b/WordPress/Classes/Utility/ObjCBridge.swift @@ -15,14 +15,6 @@ import WordPressData SupportTableViewController().showFromTabBar() } - @objc public class func makeSharingAuthorizationViewController(publicizer: PublicizeService, url: URL, blog: Blog, delegate: SharingAuthorizationDelegate) -> UIViewController { - SharingAuthorizationWebViewController(with: publicizer, url: url, for: blog, delegate: delegate) - } - - @objc public class func makeSharingButtonsViewController(blog: Blog) -> UIViewController { - SharingButtonsViewController(blog: blog) - } - @objc public class func trackBlazeEntryPointDisplayed(source: BlazeSource) { BlazeEventsTracker.trackEntryPointDisplayed(for: source) } diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 7094a993b266..9b05360d2865 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -457,7 +457,6 @@ class AztecPostViewController: UIViewController, PostEditor { addObservers(toPost: post) registerMediaObserver() - disableSocialConnectionsIfNecessary() } required init?(coder aDecoder: NSCoder) { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift deleted file mode 100644 index e3ff2ed2478e..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift +++ /dev/null @@ -1,405 +0,0 @@ -import SwiftUI -import AutomatticTracks -import WordPressData -import WordPressShared - -class DashboardJetpackSocialCardCell: DashboardCollectionViewCell { - - // MARK: - Properties - - private let repository: UserPersistentRepository - - private var blog: Blog? - private weak var dashboardViewController: BlogDashboardViewController? - - private(set) var displayState: DisplayState = .none { - didSet { - guard oldValue != displayState else { - return - } - updateUI() - } - } - - private var isNoConnectionViewHidden: Bool { - get { - guard let dotComID = blog?.dotComID?.stringValue, - let isNoConnectionHidden = noConnectionHiddenSites[dotComID] else { - return false - } - return isNoConnectionHidden - } - set { - guard let dotComID = blog?.dotComID?.stringValue else { - return - } - var currentHiddenSites = noConnectionHiddenSites - currentHiddenSites[dotComID] = true - repository.set(currentHiddenSites, forKey: Constants.hideNoConnectionViewKey) - } - } - - private var isNoSharesViewHidden: Bool { - get { - guard let dotComID = blog?.dotComID?.stringValue, - let isNoSharesHidden = noSharesHiddenSites[dotComID] else { - return false - } - return isNoSharesHidden - } - set { - guard let dotComID = blog?.dotComID?.stringValue else { - return - } - var currentHiddenSites = noSharesHiddenSites - currentHiddenSites[dotComID] = true - repository.set(currentHiddenSites, forKey: Constants.hideNoSharesViewKey) - } - } - - private var noConnectionHiddenSites: [String: Bool] { - let dictionary = repository.dictionary(forKey: Constants.hideNoConnectionViewKey) as? [String: Bool] - return dictionary ?? [:] - } - - private var noSharesHiddenSites: [String: Bool] { - let dictionary = repository.dictionary(forKey: Constants.hideNoSharesViewKey) as? [String: Bool] - return dictionary ?? [:] - } - - // MARK: - UI Properties - - private var cardTitle: String { - switch displayState { - case .noConnections: - return Constants.connectTitle - case .noShares: - return Constants.noSharesTitle - default: - return "" - } - } - - private var cardFrameView: BlogDashboardCardFrameView { - let frameView = BlogDashboardCardFrameView() - frameView.translatesAutoresizingMaskIntoConstraints = false - frameView.setTitle(cardTitle) - frameView.onEllipsisButtonTap = { } - frameView.ellipsisButton.showsMenuAsPrimaryAction = true - frameView.ellipsisButton.menu = contextMenu - - return frameView - } - - private var contextMenu: UIMenu { - let hideThisAction = UIAction(title: Constants.hideThis, - image: Constants.hideThisImage, - attributes: [UIMenuElement.Attributes.destructive], - handler: contextMenuHandler) - return UIMenu(title: String(), options: .displayInline, children: [hideThisAction]) - } - - var contextMenuHandler: UIActionHandler { - return { [weak self] _ in - guard let self else { - return - } - self.hideCard(for: self.displayState) - } - } - - // MARK: - Initializers - - override init(frame: CGRect) { - self.repository = UserPersistentStoreFactory.instance() - super.init(frame: frame) - observeManagedObjectsChange() - } - - init(repository: UserPersistentRepository = UserPersistentStoreFactory.instance()) { - self.repository = repository - super.init(frame: .zero) - observeManagedObjectsChange() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - DashboardCollectionViewCell - - func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { - self.blog = blog - self.dashboardViewController = viewController - updateDisplayState(for: blog) - } - - // MARK: - Functions - - static func shouldShowCard(for blog: Blog) -> Bool { - guard RemoteFeatureFlag.jetpackSocialImprovements.enabled() else { - return false - } - return showNoConnectionView(for: blog) || showNoSharesView(for: blog) - } - - // MARK: - Constants - - struct Constants { - static let hideNoConnectionViewKey = "dashboard-social-no-connection-view-hidden" - static let hideNoSharesViewKey = "dashboard-social-no-shares-view-hidden" - static let connectTitle = NSLocalizedString("dashboard.card.social.noconnections.title", - value: "Share across your social networks", - comment: "Title for the Jetpack Social dashboard card when the user has no social connections.") - static let noSharesTitle = NSLocalizedString("dashboard.card.social.noshares.title", - value: "You’re out of shares!", - comment: "Title for the Jetpack Social dashboard card when the user has no social shares left.") - static let hideThis = NSLocalizedString("dashboard.card.social.menu.hide", - value: "Hide this", - comment: "Title for a menu action in the context menu on the Jetpack Social dashboard card.") - static let hideThisImage = UIImage(systemName: "minus.circle") - static let cardInsets = EdgeInsets(top: 8.0, leading: 16.0, bottom: 8.0, trailing: 16.0) - static let trackingSource = "home_dashboard" - } - - enum DisplayState { - case none - case noConnections - case noShares - } -} - -// MARK: - Private Functions - -private extension DashboardJetpackSocialCardCell { - - static func showNoConnectionView(for blog: Blog) -> Bool { - guard let context = blog.managedObjectContext, - let dotComID = blog.dotComID?.stringValue, - let services = try? PublicizeService.allSupportedServices(in: context), - let connections = blog.connections else { - return false - } - let repository = UserPersistentStoreFactory.instance() - let hideNoConnectionViewKey = DashboardJetpackSocialCardCell.Constants.hideNoConnectionViewKey - let hiddenSites = (repository.dictionary(forKey: hideNoConnectionViewKey) as? [String: Bool]) ?? [:] - let isNoConnectionViewHidden = hiddenSites[dotComID] ?? false - - return blog.supports(.publicize) - && !services.isEmpty - && connections.isEmpty - && !isNoConnectionViewHidden - } - - func updateDisplayState(for blog: Blog) { - if DashboardJetpackSocialCardCell.showNoConnectionView(for: blog) { - displayState = .noConnections - } else if DashboardJetpackSocialCardCell.showNoSharesView(for: blog) { - displayState = .noShares - } else { - displayState = .none - } - } - - func updateUI() { - guard Thread.isMainThread else { - DispatchQueue.main.async { [weak self] in - self?.updateUI() - } - return - } - - var card: UIView? - switch displayState { - case .noConnections: - card = createNoConnectionCard() - case .noShares: - card = createNoSharesCard() - default: - card = nil - } - - for subview in contentView.subviews { - subview.removeFromSuperview() - } - if let card { - contentView.addSubview(card) - contentView.pinSubviewToAllEdges(card) - contentView.layoutIfNeeded() - } - } - - func createNoConnectionCard() -> UIView? { - guard let context = blog?.managedObjectContext, - let services = try? PublicizeService.allSupportedServices(in: context) else { - // Note: The context and publicize services are checked prior to this call in - // `showNoConnectionView`. This scenario *shouldn't* be possible. - assertionFailure("No managed object context or publicize services") - let error = JetpackSocialError.noConnectionViewInvalidState - CrashLogging.main.logError(error, userInfo: ["source": Constants.trackingSource]) - return nil - } - WPAnalytics.track(.jetpackSocialNoConnectionCardDisplayed, - properties: ["source": Constants.trackingSource]) - let card = cardFrameView - let viewModel = JetpackSocialNoConnectionViewModel(services: services, - padding: Constants.cardInsets, - hideNotNow: true, - bodyTextColor: .secondaryLabel, - onConnectTap: onConnectTap()) - let noConnectionViewController = JetpackSocialNoConnectionView.createHostController(with: viewModel) - card.add(subview: noConnectionViewController.view) - return card - } - - func onConnectTap() -> () -> Void { - return { [weak self] in - WPAnalytics.track(.jetpackSocialNoConnectionCTATapped, - properties: ["source": Constants.trackingSource]) - guard let self, - let blog = self.blog, - let controller = SharingViewController(blog: blog, delegate: self) else { - return - } - self.dashboardViewController?.navigationController?.pushViewController(controller, animated: true) - } - } - - static func showNoSharesView(for blog: Blog) -> Bool { - guard let sharingLimit = blog.sharingLimit, - let dotComID = blog.dotComID?.stringValue, - let connections = blog.connections else { - return false - } - let repository = UserPersistentStoreFactory.instance() - let hideNoSharesViewKey = DashboardJetpackSocialCardCell.Constants.hideNoSharesViewKey - let hiddenSites = (repository.dictionary(forKey: hideNoSharesViewKey) as? [String: Bool]) ?? [:] - let isNoSharesViewHidden = hiddenSites[dotComID] ?? false - - return blog.supports(.publicize) - && connections.contains { !$0.requiresUserAction() } - && !isNoSharesViewHidden - && sharingLimit.remaining == 0 - } - - func createNoSharesCard() -> UIView? { - guard let connections = blog?.connections as? Set else { - assertionFailure("No social connections") - let error = JetpackSocialError.noSharesViewInvalidState - CrashLogging.main.logError(error, userInfo: ["source": "social_dashboard_card"]) - return nil - } - WPAnalytics.track(.jetpackSocialShareLimitDisplayed, - properties: ["source": Constants.trackingSource]) - let card = cardFrameView - let filteredConnections = connections.filter { !$0.requiresUserAction() } - let services = filteredConnections.reduce(into: [PublicizeService.ServiceName]()) { partialResult, connection in - guard let service = PublicizeService.ServiceName(rawValue: connection.service) else { - return - } - if !partialResult.contains(service) { - partialResult.append(service) - } - } - let viewModel = JetpackSocialNoSharesViewModel(services: services, - totalServiceCount: filteredConnections.count, - onSubscribeTap: onSubscribeTap()) - let noSharesView = UIView.embedSwiftUIView(JetpackSocialNoSharesView(viewModel: viewModel)) - card.add(subview: noSharesView) - return card - } - - func onSubscribeTap() -> () -> Void { - return { [weak self] in - WPAnalytics.track(.jetpackSocialUpgradeLinkTapped, - properties: ["source": Constants.trackingSource]) - guard let blog = self?.blog, - let hostname = blog.hostname, - let url = URL(string: "https://wordpress.com/checkout/\(hostname)/jetpack_social_basic_yearly") else { - return - } - let webViewController = WebViewControllerFactory.controller(url: url, - blog: blog, - source: "dashboard_card_no_shares_subscribe_now") { - self?.checkoutDismissed() - } - let navigationController = UINavigationController(rootViewController: webViewController) - self?.dashboardViewController?.present(navigationController, animated: true) - } - } - - func checkoutDismissed() { - guard let blog else { - return - } - let coreDataStack = ContextManager.shared - let service = BlogService(coreDataStack: coreDataStack) - service.syncBlog(blog) { [weak self] in - let sharingLimit: PublicizeInfo.SharingLimit? = coreDataStack.performQuery { context in - guard let dotComID = blog.dotComID, - let blog = Blog.lookup(withID: dotComID, in: context) else { - return nil - } - return blog.sharingLimit - } - if sharingLimit == nil || sharingLimit?.remaining ?? 0 > 0 { - self?.dashboardViewController?.reloadCardsLocally() - } - } failure: { error in - DDLogError("Failed to sync blog after dismissing checkout webview due to error: \(error)") - } - } - - @objc func handleNotification() { - guard let blog else { - return - } - updateDisplayState(for: blog) - } - - func hideCard(for state: DisplayState) { - switch state { - case .noConnections: - isNoConnectionViewHidden = true - WPAnalytics.track(.jetpackSocialNoConnectionCardDismissed, - properties: ["source": Constants.trackingSource]) - case .noShares: - isNoSharesViewHidden = true - WPAnalytics.track(.jetpackSocialShareLimitDismissed, - properties: ["source": Constants.trackingSource]) - default: - break - } - dashboardViewController?.reloadCardsLocally() - } - - func observeManagedObjectsChange() { - NotificationCenter.default.addObserver( - self, - selector: #selector(handleObjectsChange), - name: .NSManagedObjectContextObjectsDidChange, - object: ContextManager.shared.mainContext - ) - } - - @objc func handleObjectsChange(_ notification: Foundation.Notification) { - guard let blog else { - return - } - let updated = notification.userInfo?[NSUpdatedObjectsKey] as? Set ?? Set() - let refreshed = notification.userInfo?[NSRefreshedObjectsKey] as? Set ?? Set() - - if updated.contains(blog) || refreshed.contains(blog) { - updateDisplayState(for: blog) - } - } -} - -// MARK: - SharingViewControllerDelegate - -extension DashboardJetpackSocialCardCell: SharingViewControllerDelegate { - - func didChangePublicizeServices() { - dashboardViewController?.reloadCardsLocally() - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift index b9e1e93f9903..d400a2540d01 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift @@ -27,7 +27,7 @@ extension DashboardCard: BlogDashboardPersonalizable { return "activity-log-card-enabled-site-settings" case .pages: return "pages-card-enabled-site-settings" - case .dynamic, .jetpackBadge, .jetpackInstall, .jetpackSocial, .failure, .ghost, .personalize, .empty, + case .dynamic, .jetpackBadge, .jetpackInstall, .failure, .ghost, .personalize, .empty, .extensiveLogging: return nil } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift index 7d2c725e4eb3..b875b6cdee86 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift @@ -18,7 +18,6 @@ enum DashboardCard: String, CaseIterable, Sendable { case freeToPaidPlansDashboardCard case domainRegistration case todaysStats = "todays_stats" - case jetpackSocial case draftPosts case scheduledPosts case pages @@ -68,8 +67,6 @@ enum DashboardCard: String, CaseIterable, Sendable { return DashboardPagesListCardCell.self case .activityLog: return DashboardActivityLogCardCell.self - case .jetpackSocial: - return DashboardJetpackSocialCardCell.self case .googleDomains: return DashboardGoogleDomainsCardCell.self } @@ -137,8 +134,6 @@ enum DashboardCard: String, CaseIterable, Sendable { case .activityLog: return DashboardActivityLogCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) - case .jetpackSocial: - return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) case .googleDomains: return FeatureFlag.googleDomainsCard.enabled && isJetpack case .dynamic: diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift index de8e10e129c6..b47948b869e3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift @@ -351,12 +351,12 @@ extension BlogDetailsViewController { if !blog.supports(.publicize) { // if publicize is disabled, show the sharing buttons settings. sharingVC = SharingButtonsViewController(blog: blog) - } else if FeatureFlag.socialSharingV2.enabled, - let manage = ManageConnectionsHostingController.make(for: blog) - { + } else if let manage = ManageConnectionsHostingController.make(for: blog) { sharingVC = manage } else { - sharingVC = SharingViewController(blog: blog, delegate: nil) + // supports(.publicize) implies a linked WP.com account, so the + // service should always resolve; guards a broken auth state. + return wpAssertionFailure("social connections service unavailable") } trackEvent(.openedSharingManagement, from: source) diff --git a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift index 3bb1714b0e49..488ee565deb6 100644 --- a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift @@ -115,7 +115,7 @@ private extension DashboardCard { case .dynamic, .ghost, .failure, .personalize, .jetpackBadge, .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, - .domainRegistration, .jetpackSocial, + .domainRegistration, .googleDomains, .extensiveLogging: assertionFailure("\(self) card should not appear in the personalization menus") return "" // These cards don't appear in the personalization menus diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift deleted file mode 100644 index e3bfe45ad078..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import UIKit -import WordPressData -import WordPressUI - -public typealias JetpackModuleHelperViewController = JetpackModuleHelperDelegate & UIViewController - -/// Shows a NoResultsViewController on a given VC and handle enabling -/// a Jetpack module -@objc public class JetpackModuleHelper: NSObject { - private weak var viewController: JetpackModuleHelperViewController? - - private let moduleName: String - private let blog: Blog - private let service: BlogJetpackSettingsService - private let blogService: BlogService - - private var noResultsViewController: NoResultsViewController? - - /// Creates a Jetpack Module Wall that gives the user the option to enable a module - /// - Parameters: - /// - viewController: a UIViewController that conforms to JetpackModuleHelperDelegate - /// - moduleName: a `String` representing the name of the module - /// - blog: a `Blog` - @objc public init(viewController: JetpackModuleHelperViewController, moduleName: String, blog: Blog) { - self.viewController = viewController - self.moduleName = moduleName - self.blog = blog - self.service = BlogJetpackSettingsService(coreDataStack: ContextManager.shared) - self.blogService = BlogService(coreDataStack: ContextManager.shared) - } - - /// Show the No Results View Controller - /// - Parameters: - /// - title: A `String` to display on the title of the NVC - /// - subtitle: A `String` to display as the subtitle of the NVC - @objc public func show(title: String, subtitle: String) { - noResultsViewController = NoResultsViewController.controller() - noResultsViewController?.configure( - title: title, - attributedTitle: nil, - noConnectionTitle: nil, - buttonTitle: NSLocalizedString("Enable", comment: "Title of button to enable publicize."), - subtitle: subtitle, - noConnectionSubtitle: nil, - attributedSubtitle: nil, - attributedSubtitleConfiguration: nil, - image: "mysites-nosites", - subtitleImage: nil, - accessoryView: nil - ) - - if let noResultsViewController, let viewController { - noResultsViewController.delegate = self - - viewController.addChild(noResultsViewController) - viewController.view.addSubview(withFadeAnimation: noResultsViewController.view) - noResultsViewController.didMove(toParent: viewController) - noResultsViewController.view.frame = viewController.view.bounds - } - } -} - -extension JetpackModuleHelper: NoResultsViewControllerDelegate { - public func actionButtonPressed() { - service.updateJetpackModuleActiveSettingForBlog(blog, - module: moduleName, - active: true, - success: { [weak self] in - guard let self else { - return - } - - self.blogService.syncBlogAndAllMetadata(self.blog) { - self.noResultsViewController?.removeFromView() - self.viewController?.jetpackModuleEnabled() - } - }, - failure: { [weak self] _ in - self?.viewController?.displayNotice(title: Constants.error) - }) - } -} - -private extension JetpackModuleHelper { - struct Constants { - static let error = NSLocalizedString("The module couldn't be activated.", comment: "Error shown when a module can not be enabled") - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift deleted file mode 100644 index a5bb8cb3bdda..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation -import WordPressData -import WordPressKit -import WordPressShared - -/// KeyringAccount is used to normalize the list of avaiable accounts while -/// preserving the owning keyring connection. -/// -struct KeyringAccount { - var name: String // The account name - var externalID: String? // The actual externalID value that should be passed when creating/updating a publicize connection. - var externalIDForConnection: String // The effective external ID that should be used for comparing a keyring account with a PublicizeConnection. - var keyringConnection: KeyringConnection -} - -@objc public class KeyringAccountHelper: NSObject { - - @objc public class ValidationError: NSObject { - @objc public let header: String - @objc public let body: String - @objc public let continueTitle: String - @objc public let cancelTitle: String - - @objc public let continueURL: URL? - - init(header: String, body: String, continueTitle: String, cancelTitle: String, continueURL: URL? = nil) { - self.header = header - self.body = body - self.continueTitle = continueTitle - self.cancelTitle = cancelTitle - self.continueURL = continueURL - } - } - - /// Validate which account type has an error and return the information for an alert - /// - /// - Parameters: - /// - connections: An array of `KeyringConnection` instances to normalize. - /// - publicizeService: The publicize service for the fetched keyring connections. - /// - /// - Returns: An instance of `ValidationError` object, this is a plain object just with the information for an alert. - /// - @objc public func validateConnections(_ connections: [KeyringConnection], with publicizeService: PublicizeService) -> ValidationError? { - let accounts = accountsFromKeyringConnections(connections, with: publicizeService) - - if publicizeService.serviceID == PublicizeService.facebookServiceID, accounts.isEmpty { - return facebookPublicizeAlertFields() - } - - return nil - } - - /// Normalizes available accounts for a KeyringConnection and its `additionalExternalUsers` - /// - /// - Parameter connections: An array of `KeyringConnection` instances to normalize. - /// - /// - Returns: An array of `KeyringAccount` objects. - /// - func accountsFromKeyringConnections(_ connections: [KeyringConnection], with publicizeService: PublicizeService) -> [KeyringAccount] { - var accounts = [KeyringAccount]() - - for connection in connections { - let acct = KeyringAccount(name: connection.externalDisplay, externalID: nil, externalIDForConnection: connection.externalID, keyringConnection: connection) - - // Do not include the service if it only supports external users. - if !publicizeService.externalUsersOnly { - accounts.append(acct) - } - - for externalUser in connection.additionalExternalUsers { - let acct = KeyringAccount(name: externalUser.externalName, externalID: externalUser.externalID, externalIDForConnection: externalUser.externalID, keyringConnection: connection) - accounts.append(acct) - } - } - - return accounts - } -} - -// MARK: - Alert Message Types - -private extension KeyringAccountHelper { - - func facebookPublicizeAlertFields() -> ValidationError { - let alertHeaderMessage = NSLocalizedString("Not Connected", - comment: "Error title for alert, shown to a user who is trying to share to Facebook but does not have any available Facebook Pages.") - let alertBodyMessage = NSLocalizedString("The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages.", - comment: "Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages.") - let continueActionTitle = NSLocalizedString("Learn more", comment: "A button title.") - let cancelActionTitle = SharedStrings.Button.ok - - return ValidationError(header: alertHeaderMessage, - body: alertBodyMessage, - continueTitle: continueActionTitle, - cancelTitle: cancelActionTitle, - continueURL: URL(string: "https://en.support.wordpress.com/publicize/#facebook-pages")) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift deleted file mode 100644 index 591f1faecbcd..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift +++ /dev/null @@ -1,100 +0,0 @@ -import UIKit -import WordPressData -import WordPressUI -import AsyncImageKit - -public final class PublicizeServiceCell: UITableViewCell { - let iconView = AsyncImageView() - let titleLabel = UILabel() - let detailsLabel = UILabel() - - @objc public class var cellId: String { "PublicizeServiceCell" } - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - detailsLabel.font = .preferredFont(forTextStyle: .footnote) - detailsLabel.textColor = .secondaryLabel - - let stackView = UIStackView(alignment: .center, spacing: 12, [ - iconView, - UIStackView(axis: .vertical, alignment: .leading, spacing: 2, [titleLabel, detailsLabel]) - ]) - contentView.addSubview(stackView) - stackView.pinEdges(to: contentView.layoutMarginsGuide) - - NSLayoutConstraint.activate([ - iconView.widthAnchor.constraint(equalToConstant: 28), - iconView.heightAnchor.constraint(equalToConstant: 28), - ]) - iconView.layer.cornerRadius = 8 - iconView.layer.masksToBounds = true - iconView.backgroundColor = UIColor.white - - iconView.contentMode = .scaleAspectFit - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func prepareForReuse() { - super.prepareForReuse() - - accessoryView = .none - iconView.prepareForReuse() - } - - @objc public func configure(with service: PublicizeService, connections: [PublicizeConnection]) { - let name = service.name - if name != .unknown && !name.hasModernRemoteLogo { - iconView.image = name.localIconImage - } else if let imageURL = URL(string: service.icon) { - iconView.setImage(with: imageURL) - } else { - iconView.image = UIImage(named: "social-default") - } - - titleLabel.text = service.label - - detailsLabel.isHidden = connections.isEmpty - if connections.count > 2 { - detailsLabel.text = String(format: Strings.numberOfAccounts, connections.count) - } else { - detailsLabel.text = connections - .map(\.externalDisplay) - .joined(separator: ", ") - } - - if service.isSupported { - if connections.contains(where: { $0.requiresUserAction() }) { - accessoryView = WPStyleGuide.sharingCellWarningAccessoryImageView() - } - } else { - accessoryView = WPStyleGuide.sharingCellErrorAccessoryImageView() - } - } -} - -private extension PublicizeService.ServiceName { - /// We no longer need to provide local overrides for these on this screen - /// as the remote images are good. - var hasModernRemoteLogo: Bool { - [ - PublicizeService.ServiceName.instagram, - PublicizeService.ServiceName.mastodon - ].contains(self) - } -} - -private enum Strings { - static let numberOfAccounts = NSLocalizedString("socialSharing.connectionDetails.nAccount", value: "%d accounts", comment: "The number of connected accounts on a third party sharing service connected to the user's blog. The '%d' is a placeholder for the number of accounts.") -} - -extension PublicizeService.ServiceName { - - /// Returns the local image for the icon representing the social network. - var localIconImage: UIImage { - WPStyleGuide.socialIcon(for: rawValue as NSString) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift deleted file mode 100644 index 888b4d027651..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import WordPressData - -@objc public final class PublicizeServicesState: NSObject { - private var connections = Set() -} - -// MARK: - Public Methods -@objc public extension PublicizeServicesState { - func addInitialConnections(_ connections: [PublicizeConnection]) { - connections.forEach { self.connections.insert($0) } - } - - func hasAddedNewConnectionTo(_ connections: [PublicizeConnection]) -> Bool { - guard !connections.isEmpty else { - return false - } - - if connections.count > self.connections.count { - return true - } - - for connection in connections where !self.connections.contains(connection) { - return true - } - - return false - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift deleted file mode 100644 index d8d733c4a367..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift +++ /dev/null @@ -1,284 +0,0 @@ -import UIKit -import Gridicons -import WordPressData -import WordPressKit -import WordPressShared -import WordPressUI - -/// Displays a list of available keyring connection accounts that can be used to -/// forge a publicize connection. -/// -@objc open class SharingAccountViewController: UITableViewController { - @objc var publicizeService: PublicizeService - @objc var keyringConnections: [KeyringConnection] - @objc var existingPublicizeConnections: [PublicizeConnection]? - @objc var immutableHandler: ImmuTableViewHandler! - @objc public weak var delegate: SharingAccountSelectionDelegate? - private let keyringAccountHelper = KeyringAccountHelper() - - fileprivate lazy var noResultsViewController: NoResultsViewController = { - let controller = NoResultsViewController.controller() - controller.view.frame = view.frame - addChild(controller) - view.addSubview(controller.view) - controller.didMove(toParent: self) - return controller - }() - - // MARK: - Lifecycle Methods - - @objc public init( - service: PublicizeService, - connections: [KeyringConnection], - existingConnections: [PublicizeConnection]? - ) { - publicizeService = service - keyringConnections = connections - existingPublicizeConnections = existingConnections - - super.init(style: .grouped) - - navigationItem.title = publicizeService.label - } - - required public init?(coder aDecoder: NSCoder) { - // TODO: - fatalError("init(coder:) has not been implemented") - } - - open override func viewDidLoad() { - super.viewDidLoad() - - configureNavbar() - configureTableView() - } - - // MARK: - Configuration - - /// Configures the appearance of the nav bar. - /// - fileprivate func configureNavbar() { - let image = UIImage.gridicon(.cross) - let closeButton = UIBarButtonItem( - image: image, - style: .plain, - target: self, - action: #selector(SharingAccountViewController.handleCloseTapped(_:)) - ) - closeButton.tintColor = UIAppColor.appBarTint - navigationItem.leftBarButtonItem = closeButton - } - - /// Configures the `UITableView` - /// - fileprivate func configureTableView() { - WPStyleGuide.configureColors(view: view, tableView: tableView) - ImmuTable.registerRows([TextRow.self], tableView: tableView) - - immutableHandler = ImmuTableViewHandler(takeOver: self) - immutableHandler.viewModel = tableViewModel() - } - - fileprivate func showNoResultsViewController() { - let title = NSLocalizedString( - "No Accounts Found", - comment: "Title of an error message. There were no third-party service accounts found to setup sharing." - ) - let message = NSLocalizedString( - "Sorry. The social service did not tell us which account could be used for sharing.", - comment: - "An error message shown if a third-party social service does not specify any accounts that an be used with publicize sharing." - ) - noResultsViewController.configure(title: title, subtitle: message) - } - - // MARK: - View Model Wrangling - - /// Builds and returns the ImmuTable view model. - /// - /// - Returns: An ImmuTable instance. - /// - fileprivate func tableViewModel() -> ImmuTable { - var sections = [ImmuTableSection]() - var connectedAccounts = [KeyringAccount]() - var accounts = keyringAccountHelper.accountsFromKeyringConnections(keyringConnections, with: publicizeService) - - if accounts.isEmpty { - showNoResultsViewController() - return ImmuTable(sections: []) - } - - // Filter out connected accounts into a different Array - for (idx, acct) in accounts.enumerated() { - if accountIsConnected(acct) { - connectedAccounts.append(acct) - accounts.remove(at: idx) - break - } - } - - // Build the section for unconnected accounts - var rows = rowsForUnconnectedKeyringAccounts(accounts) - if let section = sectionForUnconnectedKeyringAccountRows(rows) { - sections.append(section) - } - - // Build the section for connected accounts - rows = rowsForConnectedKeyringAccounts(connectedAccounts) - if !rows.isEmpty { - let title = NSLocalizedString( - "Connected", - comment: "Adjective. The title of a list of third-part sharing service account names." - ) - let section = ImmuTableSection(headerText: title, rows: rows, footerText: nil) - sections.append(section) - } - - return ImmuTable(sections: sections) - } - - /// Builds the ImmuTableSection that displays unconnected keyring accounts. - /// - /// - Parameter rows: An array of ImmuTableRow objects appearing in the section. - /// - /// - Returns: An ImmuTableSection or `nil` if there were no rows. - /// - fileprivate func sectionForUnconnectedKeyringAccountRows(_ rows: [ImmuTableRow]) -> ImmuTableSection? { - if rows.isEmpty { - return nil - } - - var title = NSLocalizedString( - "Connecting %@", - comment: - "Connecting is a verb. Title of Publicize account selection. The %@ is a placeholder for the service's name" - ) - title = NSString(format: title as NSString, publicizeService.label) as String - - let manyAccountFooter = NSLocalizedString( - "Select the account you would like to authorize. Note that your posts will be automatically shared to the selected account.", - comment: "Instructional text about the Sharing feature." - ) - let oneAccountFooter = NSLocalizedString( - "Confirm this is the account you would like to authorize. Note that your posts will be automatically shared to this account.", - comment: "Instructional text about the Sharing feature." - ) - let footer = rows.count > 1 ? manyAccountFooter : oneAccountFooter - - return ImmuTableSection(headerText: title, rows: rows, footerText: footer) - } - - /// Builds the ImmuTableSection that displays connected keyring accounts. - /// - /// - Parameter rows: An array of ImmuTableRow objects appearing in the section. - /// - /// - Returns: An ImmuTableSection or `nil` if there were no rows. - /// - fileprivate func rowsForUnconnectedKeyringAccounts(_ accounts: [KeyringAccount]) -> [ImmuTableRow] { - var rows = [ImmuTableRow]() - for acct in accounts { - let row = KeyringRow(title: acct.name, value: "", action: actionForRow(acct)) - - rows.append(row) - } - - return rows - } - - /// Builds an ImmuTableAction that should be performed when a specific row is selected. - /// - /// - Parameter keyringAccount: The keyring account for the row. - /// - /// - Returns: An ImmuTableAction instance. - /// - fileprivate func actionForRow(_ keyringAccount: KeyringAccount) -> ImmuTableAction { - { [unowned self] _ in - self.tableView.deselectSelectedRowWithAnimation(true) - - self.delegate? - .sharingAccountViewController( - self, - selectedKeyringConnection: keyringAccount.keyringConnection, - externalID: keyringAccount.externalID - ) - } - } - - /// Builds ImmuTableRows for the specified keyring accounts. - /// - /// - Parameter accounts: An array of KeyringAccount objects. - /// - /// - Returns: An array of ImmuTableRows representing the keyring accounts. - /// - fileprivate func rowsForConnectedKeyringAccounts(_ accounts: [KeyringAccount]) -> [ImmuTableRow] { - var rows = [ImmuTableRow]() - for acct in accounts { - let row = TextRow(title: acct.name, value: "") - rows.append(row) - } - - return rows - } - - /// Checks if the specified keyring account is connected. - /// - /// - Parameter keyringAccount: The keyring account to check. - /// - /// - Returns: true if the keyring account is being used by an existing publicize connection. False otherwise. - /// - fileprivate func accountIsConnected(_ keyringAccount: KeyringAccount) -> Bool { - guard let existingConnections = existingPublicizeConnections else { - return false - } - - let keyringConnection = keyringAccount.keyringConnection - for existingConnection in existingConnections { - if existingConnection.keyringConnectionID == keyringConnection.keyringID - && existingConnection.keyringConnectionUserID == keyringConnection.userID - && existingConnection.externalID == keyringAccount.externalIDForConnection - { - return true - } - } - - return false - } - - // MARK: - Actions - - /// Notifies the delegate that the user has clicked the close button to dismiss the controller. - /// - /// - Parameter sender: The close button that was tapped. - /// - @objc func handleCloseTapped(_ sender: UIBarButtonItem) { - delegate?.didDismissSharingAccountViewController(self) - } - - /// An ImmuTableRow class. - /// - struct KeyringRow: ImmuTableRow { - static let cell = ImmuTableCell.class(WPTableViewCellValue1.self) - - let title: String - let value: String - let action: ImmuTableAction? - - func configureCell(_ cell: UITableViewCell) { - cell.textLabel?.text = title - cell.detailTextLabel?.text = value - - WPStyleGuide.configureTableViewCell(cell) - } - } -} - -/// Delegate protocol. -/// -@objc public protocol SharingAccountSelectionDelegate: NSObjectProtocol { - func didDismissSharingAccountViewController(_ controller: SharingAccountViewController) - func sharingAccountViewController( - _ controller: SharingAccountViewController, - selectedKeyringConnection keyringConnection: KeyringConnection, - externalID: String? - ) -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h deleted file mode 100644 index b184f86f0838..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h +++ /dev/null @@ -1,71 +0,0 @@ -@import UIKit; - -@protocol SharingAuthorizationHelperDelegate; -@class PublicizeService; -@class KeyringConnection; -@class PublicizeConnection; -@class Blog; -@class ValidationError; - -/** - A helper class for managing aspects of a publicize service. Supports creating, - updating and deleting connections to a publicize service for a specified blog. - */ -@interface SharingAuthorizationHelper : NSObject - -@property (nonatomic, weak) iddelegate; -@property (nonatomic, strong) UIView *popoverSourceView; - -/** - Returns a new instance. - - @param viewController: A view controller to act as the presenter for the modal auth and account selection view controllers. - @param blog: The blog whose publicize connections are being managed. - @param publicizeServices: The list of available publicize services. - - @return A new instance - */ -- (instancetype)initWithViewController:(UIViewController *)viewController blog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService; - -/** - Starts the process of connecting to publicize service passed when the instance was created. - A `SharingAuthorizationWebViewController` is presented modally allowing the user - to establish the necessary oAuth handshake with a publicize service. - */ -- (void)connectPublicizeService; - -/** - Starts the process of reconnecting a broken publicize connection. - A `SharingAuthorizationWebViewController` is presented modally allowing the user - to repair the necessary oAuth handshake with a publicize service. - - @param publicizeConnection: An existing publicize connection to the publicize service - being managed. - */ -- (void)reconnectPublicizeConnection:(PublicizeConnection *)publicizeConnection; - -@end - -@protocol SharingAuthorizationHelperDelegate - -@optional - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper authorizationSucceededForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper authorizationFailedForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper authorizationCancelledForService:(PublicizeService *)service; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper willFetchKeyringsForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didFetchKeyringsForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper keyringFetchFailedForService:(PublicizeService *)service; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper willConnectToService:(PublicizeService *)service usingKeyringConnection:(KeyringConnection *)keyringConnection; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didConnectToService:(PublicizeService *)service withPublicizeConnection:(PublicizeConnection *)keyringConnection; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper connectionFailedForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper connectionCancelledForService:(PublicizeService *)service; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper - requestToShowValidationError:(ValidationError *)validationError - fromViewController:(UIViewController *)viewController; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m deleted file mode 100644 index 9c1228efd5e6..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m +++ /dev/null @@ -1,440 +0,0 @@ -@import WordPressShared; - -#import "SharingAuthorizationHelper.h" -#import "BlogService.h" - -#import "WordPress-Swift.h" -@import WordPressData; - - - -@interface SharingAuthorizationHelper() -@property (nonatomic, strong) Blog *blog; -@property (nonatomic, strong) PublicizeService *publicizeService; -@property (nonatomic, weak) UIViewController *viewController; -@property (nonatomic, strong) UINavigationController *navController; -@property (nonatomic) BOOL reconnecting; -@end - -@implementation SharingAuthorizationHelper - -#pragma mark - Lifecycle Methods - -- (instancetype)initWithViewController:(UIViewController *)viewController blog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService -{ - self = [self init]; - if (self) { - _blog = blog; - _publicizeService = publicizeService; - _viewController = viewController; - } - return self; -} - - -#pragma mark - Instance Methods - -- (NSArray *)connectionsForService -{ - // TODO: Make a blog extension to get connections for a specific publicize service. - // Use it where ever this method was duplicated. - NSMutableArray *connections = [NSMutableArray array]; - for (PublicizeConnection *pubConn in self.blog.connections) { - if ([pubConn.service isEqualToString:self.publicizeService.serviceID]) { - [connections addObject:pubConn]; - } - } - return [NSArray arrayWithArray:connections]; -} - -/** - Dismisses the currently presented modal view controller. - */ -- (void)dismissNavViewController -{ - [self.navController dismissViewControllerAnimated:YES completion:nil]; -} - -/** - Dismisses the modal and informs the user that a reconnect attempt was successful. - */ -- (void)handleReconnectSucceeded -{ - [self dismissNavViewController]; - NSString *message = NSLocalizedString(@"%@ was reconnected.", @"Let's the user know that a third party sharing service was reconnected. The %@ is a placeholder for the service name."); - message = [NSString stringWithFormat:message, self.publicizeService.label]; - [SVProgressHUD showDismissibleSuccessWithStatus:message]; -} - - -#pragma mark - Authorization Methods - -- (void)connectPublicizeService -{ - self.reconnecting = NO; - [self authorizeWithConnectionURL:[NSURL URLWithString:self.publicizeService.connectURL]]; -} - -- (void)reconnectPublicizeConnection:(PublicizeConnection *)publicizeConnection -{ - self.reconnecting = YES; - [self authorizeWithConnectionURL:[NSURL URLWithString:publicizeConnection.refreshURL]]; -} - -/** - A helper method for presenting an instance of the `SharingAuthorizationWebViewController` - - @param connectionURL: The URL to pass to the SharingAuthorizationWebViewController's constructor. - It should be the REST API URL to either connect or refresh a publicize service connection. - */ -- (void)authorizeWithConnectionURL:(NSURL *)connectionURL -{ - UIViewController *webViewController = [ObjCBridge makeSharingAuthorizationViewControllerWithPublicizer:self.publicizeService url:connectionURL blog:self.blog delegate:self]; - - self.navController = [[UINavigationController alloc] initWithRootViewController:webViewController]; - self.navController.modalPresentationStyle = UIModalPresentationFormSheet; - [self.viewController presentViewController:self.navController animated:YES completion:nil]; -} - - -#pragma mark - Authorization Delegate Methods - -- (void)authorizeDidSucceed:(PublicizeService *)publicizer -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:authorizationSucceededForService:)]) { - [self.delegate sharingAuthorizationHelper:self authorizationSucceededForService:self.publicizeService]; - } - - if (self.reconnecting) { - // Resync publicize connections. - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - [sharingService syncPublicizeConnectionsForBlog:self.blog success:^{ - [self handleReconnectSucceeded]; - } failure:^(NSError *error) { - DDLogError([error description]); - // Even if there is an error syncing the reconnect attempt still succeeded. - [self handleReconnectSucceeded]; - }]; - - } else { - [self fetchKeyringConnectionsForService:publicizer]; - } - -} - -/** - Dismisses the modal view controller prompting the user the connection failed. - - @param publicizer: The publicize service that failed to connect. - @param error: An error with details regarding the connection failure. - */ -- (void)authorize:(PublicizeService *)publicizer didFailWithError:(NSError *)error -{ - DDLogError([error description]); - [self dismissNavViewController]; - - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:authorizationFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self authorizationFailedForService:self.publicizeService]; - return; - } - - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection failed", @"Message to show when Publicize authorization failed")]; -} - -/** - Dismisses the modal view controller prompting the user the connection was canceled. - - @param publicizer: The publicize service that failed to connect. - */ -- (void)authorizeDidCancel:(PublicizeService *)publicizer -{ - [self dismissNavViewController]; - - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:authorizationCancelledForService:)]) { - [self.delegate sharingAuthorizationHelper:self authorizationCancelledForService:self.publicizeService]; - return; - } - - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection canceled", @"Message to show when Publicize authorization is canceled")]; -} - - -#pragma mark - Keyring Account Selection Methods - -/** - Fetches keyring connections for the specified service. Once keyring connections have been - fetched `showAccountSelectorForKeyrings:` is called. - - @param pubServ: The publicize service for the fetched keyring connections. - */ -- (void)fetchKeyringConnectionsForService:(PublicizeService *)pubServ -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:willFetchKeyringsForService:)]) { - [self.delegate sharingAuthorizationHelper:self willFetchKeyringsForService:self.publicizeService]; - } - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - __weak __typeof__(self) weakSelf = self; - [sharingService fetchKeyringConnectionsForBlog:self.blog success:^(NSArray *keyringConnections) { - if ([weakSelf.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didFetchKeyringsForService:)]) { - [weakSelf.delegate sharingAuthorizationHelper:weakSelf didFetchKeyringsForService:weakSelf.publicizeService]; - } - - // Fiter matches - NSMutableArray *marr = [NSMutableArray array]; - for (KeyringConnection *keyConn in keyringConnections) { - if ([keyConn.service isEqualToString:pubServ.serviceID]) { - [marr addObject:keyConn]; - } - } - - if ([marr count] == 0) { - DDLogDebug(@"No keyring connections matched serviceID: %@, Returned connections: %@", pubServ.serviceID, keyringConnections); - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:keyringFetchFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self keyringFetchFailedForService:self.publicizeService]; - return; - } - NSString *status = [NSString stringWithFormat:NSLocalizedString(@"No connections found for %@", @"Message to show when Keyring connection synchronization succeeded but no matching connections were found. %@ is a service name like Facebook or Twitter"), self.publicizeService.label]; - [SVProgressHUD showDismissibleErrorWithStatus:status]; - return; - } - - //Check errors over accounts and show alert if it's needed - KeyringAccountHelper *keyringAccount = [KeyringAccountHelper new]; - ValidationError *validationError = [keyringAccount validateConnections:marr with:weakSelf.publicizeService]; - if (validationError) { - [weakSelf.delegate sharingAuthorizationHelper:weakSelf - requestToShowValidationError:validationError - fromViewController:weakSelf.navController]; - return; - } - - [weakSelf showAccountSelectorForKeyrings:marr]; - } failure:^(NSError * __unused error) { - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:keyringFetchFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self keyringFetchFailedForService:self.publicizeService]; - return; - } - - NSString *status = [NSString stringWithFormat:NSLocalizedString(@"We had trouble loading connections for %@", @"Message to show when Keyring connection synchronization failed. %@ is a service name like Facebook or Twitter"), self.publicizeService.label]; - [SVProgressHUD showDismissibleErrorWithStatus:status]; - }]; -} - -/** - Presents a modal `SharingAccountViewController` to let the user confirm the third party - account to use for the publicize connection. - - @param keyringConnections: An array of `KeyringConnection` instances. - */ -- (void)showAccountSelectorForKeyrings:(NSArray *)keyringConnections -{ - NSParameterAssert([[keyringConnections firstObject] isKindOfClass:[KeyringConnection class]]); - - SharingAccountViewController *controller = [[SharingAccountViewController alloc] initWithService:self.publicizeService - connections:keyringConnections - existingConnections:[self connectionsForService]]; - controller.delegate = self; - - // Set the view controller stack vs push so there is no back button to contend with. - // There should be no reason for the user to click back to the authorization vc. - [self.navController setViewControllers:@[controller] animated:YES]; -} - - -#pragma mark - SharingAccountSelection Methods - -/** - Returns one or more existing `PublicizeConnections` derived from the specified - keyringConnection. - - @param keyringConnections: An array of `KeyringConnection` instances. - */ -- (PublicizeConnection *)publicizeConnectionUsingKeyringConnection:(KeyringConnection *)keyringConnection -{ - for (PublicizeConnection *connection in self.blog.connections) { - // If the publicize connection is using the keyring connection, and the publicize connection's externalID is either the - if ([connection.keyringConnectionID isEqualToNumber:keyringConnection.keyringID]) { - return connection; - } - } - return nil; -} - -/** - Checks if the specified publicize connection is derived form the specified - keyring connection, and uses the supplied external ID. Returns true if there - is a match. - - @param connection: A `PublicizeConnection` instance. - @param keyringConnections: An array of `KeyringConnection` instances. - @param externalID: The external id of keyring connection, or one of its additional external accounts. - */ -- (BOOL)publicizeConnection:(PublicizeConnection *)connection usesKeyringConnection:(KeyringConnection *)keyringConnection withExternalID:(NSString *)externalID -{ - // if the specified externalUserID matches the connection's externalID, - // or if the externalUserID is nil, and the connection's externalID is equal to the keyring connections externalID - // then the user choose an already connected account. - if ([externalID isEqualToString:connection.externalID] || - (externalID == nil && [connection.externalID isEqualToString:keyringConnection.externalID])) { - return YES; - } - return NO; -} - -/** - Some KeyringConnections that have addtional external accounts. PublicizeConnections - derived from such a keyring connection can be connected to only one of the keyring - connection's accounts at a time (either the main one or one of the additional ones). - Before updating the external ID of such a PublicizeConnection, prompt the user and - inform them the connection to their curret account will be replaced by their selection. - - @param keyringConnection: The keyring connection in question - @param externalID: The external ID on the keyring connection or one of its additional accounts. - @param currentPublicizeConnection: The existing publicize connection derived from the keyring connection. - */ -- (void)confirmNewConnection:(KeyringConnection *)keyringConnection withExternalID:(NSString *)externalID disconnectsCurrentConnection:(PublicizeConnection *)currentPublicizeConnection -{ - NSString *accountName = keyringConnection.externalDisplay; - if (![keyringConnection.externalID isEqualToString:externalID]) { - for (KeyringConnectionExternalUser *externalUser in keyringConnection.additionalExternalUsers) { - if ([externalUser.externalID isEqualToString:externalID]) { - accountName = externalUser.externalName; - break; - } - } - } - - NSString *title = NSLocalizedString(@"Connecting %@", @"Connecting is a verb. Title of Publicize account selection. The %@ is a placeholder for the service's name."); - NSString *message = NSLocalizedString(@"Connecting %@ will replace the existing connection to %@.", @"Informs the user of consequences of chooseing a new account to connect to publicize. The %@ characters are placeholders for account names."); - NSString *cancel = NSLocalizedString(@"Cancel", @"Verb. Tapping cancels the publicize account selection."); - NSString *connect = NSLocalizedString(@"Connect", @"Verb. Tapping connects an account to Publicize."); - - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:title, self.publicizeService.label] - message:[NSString stringWithFormat:message, accountName, currentPublicizeConnection.externalDisplay] - preferredStyle:UIAlertControllerStyleAlert]; - - [alertController addCancelActionWithTitle:cancel handler:nil]; - - [alertController addDefaultActionWithTitle:connect handler:^(UIAlertAction * __unused action) { - [self updateConnection:currentPublicizeConnection forKeyringConnection:keyringConnection withExternalID:externalID]; - }]; - - [alertController presentFromRootViewController]; -} - -/** - Updates an existing publicize connection to use the specified external ID. - - @param publicizeConnection: The publicize connection to be modified. - @param keyringConnection: The keyring connection from which the publicize connection is derived - @param externalID: The external id of the keyring connection or one of its additional external accounts. - */ -- (void)updateConnection:(PublicizeConnection *)publicizeConnection forKeyringConnection:(KeyringConnection *)keyringConnection withExternalID:(NSString *)externalID -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:willConnectToService:usingKeyringConnection:)]) { - [self.delegate sharingAuthorizationHelper:self willConnectToService:self.publicizeService usingKeyringConnection:keyringConnection]; - } - - [self dismissNavViewController]; - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - - [sharingService updateExternalID:externalID forBlog:self.blog forPublicizeConnection:publicizeConnection success:^{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didConnectToService:withPublicizeConnection:)]) { - [self.delegate sharingAuthorizationHelper:self didConnectToService:self.publicizeService withPublicizeConnection:publicizeConnection]; - } - } failure:^(NSError *error) { - [self connectionFailedWithError:error]; - }]; -} - -/** - Forges a publicize connection between the blog and publicize servcie with which - the `SharingAuthorizationHelper` was initialized. - - @param keyConn: The keyring connection from which to create a publicize connection - @param externalUserID: The external ID of one of the keyring connection's additional external accounts. - Should be nil if not connecting to one of the additional external accounts. - */ -- (void)connectToServiceWithKeyringConnection:(KeyringConnection *)keyConn andExternalUserID:(NSString *)externalUserID -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:willConnectToService:usingKeyringConnection:)]) { - [self.delegate sharingAuthorizationHelper:self willConnectToService:self.publicizeService usingKeyringConnection:keyConn]; - } - - [self dismissNavViewController]; - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService createPublicizeConnectionForBlog:self.blog - keyring:keyConn - externalUserID:externalUserID - success:^(PublicizeConnection *pubConn) { - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didConnectToService:withPublicizeConnection:)]) { - [self.delegate sharingAuthorizationHelper:self didConnectToService:self.publicizeService withPublicizeConnection:pubConn]; - } - } - failure:^(NSError *error) { - [self connectionFailedWithError:error]; - }]; -} - -/** - A convenience method for handling an error when making a publicize connection. - - @param error: The error that occurred. - */ -- (void)connectionFailedWithError:(NSError *)error -{ - DDLogError([error description]); - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:connectionFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self connectionFailedForService:self.publicizeService]; - return; - } - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection failed", @"Message to show when Publicize connect failed")]; -} - - -#pragma mark - SharingAccount Delegate Methods - -- (void)didDismissSharingAccountViewController:(SharingAccountViewController *)controller -{ - [self dismissNavViewController]; - - NSString *str = [NSString stringWithFormat:@"The %@ connection could not be made because no account was selected.", self.publicizeService.label]; - DDLogDebug(str); - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:connectionCancelledForService:)]) { - [self.delegate sharingAuthorizationHelper:self connectionCancelledForService:self.publicizeService]; - return; - } - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection canceled", @"Message to show when Publicize connection is canceled by the user.")]; -} - -- (void)sharingAccountViewController:(SharingAccountViewController *)controller - selectedKeyringConnection:(KeyringConnection *)keyringConnection - externalID:(NSString *)externalID -{ - PublicizeConnection *connection = [self publicizeConnectionUsingKeyringConnection:keyringConnection]; - - if (!connection) { - [self connectToServiceWithKeyringConnection:keyringConnection andExternalUserID:externalID]; - return; - } - - // Check to see if the chosen connection and external ID matches an existin PublicizeConnection. - // If this is true the user has selected the already connected account and we can just treat it as a success. - if ([self publicizeConnection:connection usesKeyringConnection:keyringConnection withExternalID:externalID]) { - // The user selected the existing connection. Treat this as success and bail. - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didConnectToService:withPublicizeConnection:)]) { - [self.delegate sharingAuthorizationHelper:self didConnectToService:self.publicizeService withPublicizeConnection:connection]; - } - [self dismissNavViewController]; - return; - } - - // The user has selected a different account on an keyring connection that is already in use. - // We need to ask the user to confirm, because connecting the new account will disconnect the old one. - [self confirmNewConnection:keyringConnection withExternalID:externalID disconnectsCurrentConnection:connection]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift deleted file mode 100644 index c8bdadb62fce..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift +++ /dev/null @@ -1,190 +0,0 @@ -import JetpackSocial -import WebKit -import CoreMedia -import WordPressData - -@objc -public protocol SharingAuthorizationDelegate: NSObjectProtocol { - @objc - func authorize(_ publicizer: PublicizeService, didFailWithError error: NSError) - - @objc - func authorizeDidSucceed(_ publicizer: PublicizeService) - - @objc - func authorizeDidCancel(_ publicizer: PublicizeService) -} - -class SharingAuthorizationWebViewController: WebKitViewController { - private static let loginURL = "https://wordpress.com/wp-login.php" - - /// Verification loading -- dismiss on completion - /// - private var loadingVerify: Bool = false - - /// Publicize service being authorized - /// - private let publicizer: PublicizeService - - private var hosts = [String]() - - private weak var delegate: SharingAuthorizationDelegate? - - init(with publicizer: PublicizeService, url: URL, for blog: Blog, delegate: SharingAuthorizationDelegate) { - self.delegate = delegate - self.publicizer = publicizer - - let configuration = WebViewControllerConfiguration(url: url) - configuration.authenticate(blog: blog) - configuration.secureInteraction = true - - super.init(configuration: configuration) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View Lifecycle - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - cleanupCookies() - } - - // MARK: - Cookies Management - - /// Saves the host from the specidied URL for cleaning up cookies when done. - /// - /// - Parameters: - /// - url: the URL to retrieve the host from. - /// - func saveHostForCookiesCleanup(from url: URL) { - guard let host = url.host, - !host.contains("wordpress"), - !hosts.contains(host) - else { - return - } - - let components = host.components(separatedBy: ".") - - // A bit of paranioa here. The components should never be less than two but just in case... - guard let hostName = components.count > 1 ? components[components.count - 2] : components.first else { - return - } - - hosts.append(hostName) - } - - /// Cleanup cookies - /// - func cleanupCookies() { - let storage = HTTPCookieStorage.shared - - guard let cookies = storage.cookies else { - // Nothing to cleanup - return - } - - for cookie in cookies { - for host in hosts { - if cookie.domain.contains(host) { - storage.deleteCookie(cookie) - } - } - } - } - - // MARK: - Misc - - override func close() { - guard let delegate else { - super.close() - return - } - - delegate.authorizeDidCancel(publicizer) - } - - private func handleAuthorizationAllowed() { - // Note: There are situations where this can be called in error due to how - // individual services choose to reply to an authorization request. - // Delegates should expect to handle a false positive. - delegate?.authorizeDidSucceed(publicizer) - } -} - -// MARK: - WKNavigationDelegate - -extension SharingAuthorizationWebViewController { - override func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - decidePolicy(webView: webView, navigationAction: navigationAction, decisionHandler: decisionHandler) - } - - private func decidePolicy( - webView: WKWebView, - navigationAction: WKNavigationAction, - decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void - ) { - // Prevent a second verify load by someone happy clicking. - guard !loadingVerify, - let url = navigationAction.request.url - else { - decisionHandler(.cancel) - return - } - - let action = PublicizeConnectionURLMatcher.authorizeAction(for: url) - - switch action { - case .none: - fallthrough - case .unknown: - fallthrough - case .request: - super.webView(webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) - return - case .verify: - loadingVerify = true - decisionHandler(.allow) - return - case .deny: - decisionHandler(.cancel) - close() - return - } - } - - override func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation!, - withError error: Error - ) { - if loadingVerify && (error as NSError).code == NSURLErrorCancelled { - // Authenticating to Facebook and Twitter can return an false - // NSURLErrorCancelled (-999) error. However the connection still succeeds. - handleAuthorizationAllowed() - return - } - - super.webView(webView, didFailProvisionalNavigation: navigation, withError: error) - } - - override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if let url = webView.url { - saveHostForCookiesCleanup(from: url) - } - - if loadingVerify { - handleAuthorizationAllowed() - } else { - super.webView(webView, didFinish: navigation) - } - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h deleted file mode 100644 index ee3365597da0..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h +++ /dev/null @@ -1,20 +0,0 @@ -#import - -@class Blog; -@class PublicizeService; - -/** - * @brief Controller to display Calypso sharing options - */ -@interface SharingConnectionsViewController : UITableViewController - -/** - * @brief Convenience initializer - * - * @param blog the blog from where to read the information from - * - * @return New instance of SharingViewController - */ -- (instancetype)initWithBlog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m deleted file mode 100644 index de4cbc6d5a4f..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m +++ /dev/null @@ -1,300 +0,0 @@ -#import "SharingConnectionsViewController.h" - -#import "BlogService.h" -#import "SharingDetailViewController.h" -#import "SharingAuthorizationHelper.h" -#import "WordPress-Swift.h" -@import WordPressData; -@import WordPressShared; - -static NSString *const CellIdentifier = @"CellIdentifier"; - -@interface SharingConnectionsViewController () - -@property (nonatomic, strong, readonly) Blog *blog; -@property (nonatomic, strong) PublicizeService *publicizeService; -@property (nonatomic, strong) SharingAuthorizationHelper *helper; -@property (nonatomic, assign) BOOL connecting; - -@end - -@implementation SharingConnectionsViewController - -#pragma mark - Life Cycle Methods - -- (void)dealloc -{ - self.helper.delegate = nil; -} - -- (instancetype)initWithBlog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - self = [self initWithStyle:UITableViewStyleInsetGrouped]; - if (self) { - _blog = blog; - _publicizeService = publicizeService; - _helper = [[SharingAuthorizationHelper alloc] initWithViewController:self blog:blog publicizeService:publicizeService]; - _helper.delegate = self; - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.navigationItem.title = self.publicizeService.label; - - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - self.tableView.cellLayoutMarginsFollowReadableWidth = YES; - [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:CellIdentifier]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self.tableView reloadData]; -} - - -#pragma mark - Instance Methods - -- (NSArray *)connectionsForService -{ - NSMutableArray *connections = [NSMutableArray array]; - for (PublicizeConnection *pubConn in self.blog.connections) { - if ([pubConn.service isEqualToString:self.publicizeService.serviceID]) { - [connections addObject:pubConn]; - } - } - return [NSArray arrayWithArray:connections]; -} - -- (BOOL)hasConnectedAccounts -{ - return [[self connectionsForService] count] > 0; -} - -- (void)showDetailForConnection:(PublicizeConnection *)connection -{ - SharingDetailViewController *controller = [[SharingDetailViewController alloc] initWithBlog:self.blog - publicizeConnection:connection]; - [self.navigationController pushViewController:controller animated:YES]; -} - - -#pragma mark - TableView Delegate Methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - if ([self.publicizeService.serviceID isEqualToString:PublicizeService.googlePlusServiceID]) { - if ([self hasConnectedAccounts]) { - return 1; - } else { - return 0; - } - } else if ([self hasConnectedAccounts] && self.publicizeService.isSupported) { - return 2; - } - - return 1; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - NSString *title; - if ([self hasConnectedAccounts] && section == 0) { - title = NSLocalizedString(@"Connected Accounts", @"Noun. Title. Title for the list of accounts for third party sharing services."); - } else { - NSString *format = NSLocalizedString(@"Share post to %@", @"Title. `The `%@` is a placeholder for the service name."); - title = [NSString stringWithFormat:format, self.publicizeService.label]; - } - return title; -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - if ([self hasConnectedAccounts] && section == 0) { - return nil; - } - NSString *title = NSLocalizedString(@"Connect to automatically share your blog posts to %@", @"Instructional text appearing below a `Connect` button. The `%@` is a placeholder for the name of a third-party sharing service."); - return [NSString stringWithFormat:title, self.publicizeService.label]; -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section -{ - if (self.publicizeService.isSupported) { - return nil; - } - - TwitterDeprecationTableFooterView *footerView = [TwitterDeprecationTableFooterView new]; - footerView.presentingViewController = self; - footerView.source = @"social_connection_detail"; - - return footerView; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - if ([self hasConnectedAccounts] && section == 0) { - return [[self connectionsForService] count]; - } - - return 1; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - WPTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; - // resets the cell - [WPStyleGuide configureTableViewCell:cell]; - cell.accessoryType = UITableViewCellAccessoryNone; - cell.accessoryView = nil; - cell.textLabel.textAlignment = NSTextAlignmentNatural; - - if ([self hasConnectedAccounts] && indexPath.section == 0) { - [self configurePublicizeCell:cell atIndexPath:indexPath]; - - } else { - [self configureConnectionCell:cell atIndexPath:indexPath]; - } - - return cell; -} - -- (void)configureConnectionCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath -{ - [WPStyleGuide configureTableViewActionCell:cell]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.text = [self titleForConnectionCell]; - if (self.connecting) { - UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; - cell.accessoryView = activityView; - cell.selectionStyle = UITableViewCellSelectionStyleNone; - [activityView startAnimating]; - } else { - cell.accessoryView = nil; - cell.selectionStyle = UITableViewCellSelectionStyleGray; - } -} - -- (NSString *)titleForConnectionCell -{ - if (self.connecting) { - return NSLocalizedString(@"Connecting...", @"Verb. Text label. Allows the user to connect to a third-party sharing service like Facebook or Twitter."); - } - if ([self hasConnectedAccounts]) { - return NSLocalizedString(@"Connect Another Account", @"Verb. Text label. Allows the user to connect to a third-party sharing service like Facebook or Twitter."); - } - return NSLocalizedString(@"Connect", @"Verb. Text label. Allows the user to connect to a third-party sharing service like Facebook or Twitter."); -} - -- (void)configurePublicizeCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath -{ - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - PublicizeConnection *connection = [[self connectionsForService] objectAtIndex:indexPath.row]; - cell.textLabel.text = connection.externalDisplay; - - if (![self.publicizeService isSupported]) { - cell.accessoryView = [WPStyleGuide sharingCellErrorAccessoryImageView]; - } else if ([connection requiresUserAction]) { - cell.accessoryView = [WPStyleGuide sharingCellWarningAccessoryImageView]; - } -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - - if ([self hasConnectedAccounts] && indexPath.section == 0) { - PublicizeConnection *connection = [[self connectionsForService] objectAtIndex:indexPath.row]; - [self showDetailForConnection:connection]; - return; - } - - if (self.connecting) { - return; - } - - [self handleConnectTapped:indexPath]; -} - - -#pragma mark - Actions - -- (void)handleConnectTapped:(NSIndexPath *)indexPath -{ - if ([UIDevice isPad]) { - UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; - self.helper.popoverSourceView = cell.textLabel; - } - - [self.helper connectPublicizeService]; -} - -- (void)handleContinueURLTapped:(NSURL*)url -{ - UIApplication *application = [UIApplication sharedApplication]; - - if ([application canOpenURL:url]) { - [application openURL:url options:@{} completionHandler:nil]; - } -} - - -#pragma mark - SharingAuthorizationHelper Delegate Methods - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper connectionFailedForService:(PublicizeService *)service -{ - self.connecting = NO; - [self.tableView reloadData]; -} - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper willConnectToService:(PublicizeService *)service usingKeyringConnection:(KeyringConnection *)keyringConnection -{ - self.connecting = YES; - [self.tableView reloadData]; -} - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didConnectToService:(PublicizeService *)service withPublicizeConnection:(PublicizeConnection *)keyringConnection -{ - self.connecting = NO; - [self.tableView reloadData]; - [self showDetailForConnection:keyringConnection]; -} - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper - requestToShowValidationError:(ValidationError *)validationError - fromViewController:(UIViewController *)viewController -{ - [self dismissViewControllerAnimated:YES completion:nil]; - - UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:validationError.header - message:validationError.body - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:validationError.cancelTitle - style:UIAlertActionStyleCancel - handler:nil]; - __weak SharingConnectionsViewController *sharingConnectionsVC = self; - UIAlertAction* continueAction = [UIAlertAction actionWithTitle:validationError.continueTitle - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * __unused action) { - if (validationError.continueURL) { - [sharingConnectionsVC handleContinueURLTapped: validationError.continueURL]; - } - }]; - - [alertVC addAction:continueAction]; - [alertVC addAction:cancelAction]; - [self presentViewController:alertVC animated:YES completion:nil]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h deleted file mode 100644 index 5c2f48450e3d..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h +++ /dev/null @@ -1,22 +0,0 @@ -#import - -@class Blog; -@class PublicizeConnection; - -/** - * @brief Controller to display Calypso sharing options - */ -@interface SharingDetailViewController : UITableViewController - -/** - * @brief Convenience initializer - * - * @param blog the blog from where to read the information from - * @param connection the relevant publicize connection - * - * @return New instance of SharingDetailViewController - */ -- (instancetype)initWithBlog:(Blog *)blog - publicizeConnection:(PublicizeConnection *)connection; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m deleted file mode 100644 index 9a1027836e59..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m +++ /dev/null @@ -1,304 +0,0 @@ -#import "SharingDetailViewController.h" -#import "BlogService.h" -#import "SharingAuthorizationHelper.h" -#import "WordPress-Swift.h" -@import WordPressData; -@import WordPressShared; - -static NSString *const CellIdentifier = @"CellIdentifier"; - -@interface SharingDetailViewController () - -@property (nonatomic, strong, readonly) Blog *blog; -@property (nonatomic, strong) PublicizeConnection *publicizeConnection; -@property (nonatomic, strong) PublicizeService *publicizeService; -@property (nonatomic, strong) SharingAuthorizationHelper *helper; -@end - -@implementation SharingDetailViewController - -- (void)dealloc -{ - self.helper.delegate = nil; -} - -- (instancetype)initWithBlog:(Blog *)blog - publicizeConnection:(PublicizeConnection *)connection -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - NSParameterAssert([connection isKindOfClass:[PublicizeConnection class]]); - self = [super initWithStyle:UITableViewStyleGrouped]; - if (self) { - _blog = blog; - _publicizeConnection = connection; - PublicizeService *publicizeService = [PublicizeService lookupPublicizeServiceNamed:connection.service inContext:[self managedObjectContext]]; - if (publicizeService) { - self.helper = [[SharingAuthorizationHelper alloc] initWithViewController:self - blog:self.blog - publicizeService:publicizeService]; - self.helper.delegate = self; - self.publicizeService = publicizeService; - } - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.navigationItem.title = self.publicizeConnection.externalDisplay; - - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:CellIdentifier]; -} - - -#pragma mark - Instance Methods - -- (void)openFacebookFAQ -{ - NSURL *url = [NSURL URLWithString:@"https://en.blog.wordpress.com/2018/07/23/sharing-options-from-wordpress-com-to-facebook-are-changing/"]; - [[UIApplication sharedApplication] openURL:url options:[NSDictionary new] completionHandler:nil]; -} - -- (NSString *)textForFacebookFooter -{ - NSString *title = NSLocalizedString(@"As of August 1, 2018, Facebook no longer allows direct sharing of posts to Facebook Profiles. Connections to Facebook Pages remain unchanged.", @"Message shown to users who have an old publicize connection to a facebook profile."); - return [NSString stringWithFormat:title, self.publicizeConnection.label]; -} - -- (NSString *)textForBrokenConnectionFooter -{ - NSString *title = NSLocalizedString(@"There is an issue connecting to %@. Reconnect to continue publicizing.", @"Informs the user about an issue connecting to the third-party sharing service. The `%@` is a placeholder for the service name."); - return [NSString stringWithFormat:title, self.publicizeConnection.label]; -} - -- (void)configureReconnectCell: (UITableViewCell *)cell -{ - cell.textLabel.text = NSLocalizedString(@"Reconnect", @"Verb. Text label. Tapping attempts to reconnect a third-party sharing service to the user's blog."); - [WPStyleGuide configureTableViewActionCell:cell]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.textColor = [UIColor murielPrimary]; -} - -- (void)configureLearnMoreCell: (UITableViewCell *)cell -{ - cell.textLabel.text = NSLocalizedString(@"Learn More", @"Title of a button. Tapping allows the user to learn more about the specific error."); - [WPStyleGuide configureTableViewActionCell:cell]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.textColor = [UIColor murielPrimary]; -} - -- (void)configureDisconnectCell: (UITableViewCell *)cell -{ - cell.textLabel.text = NSLocalizedString(@"Disconnect", @"Verb. Text label. Tapping disconnects a third-party sharing service from the user's blog."); - [WPStyleGuide configureTableViewDestructiveActionCell:cell]; -} - -- (NSManagedObjectContext *)managedObjectContext -{ - return self.blog.managedObjectContext; -} - -/// Returns true if the service is supported by Jetpack Social, but the connection is broken. -- (BOOL)isSupportedConnectionBroken -{ - return [self.publicizeConnection isBroken] && self.publicizeService.isSupported; -} - - -#pragma mark - TableView Delegate Methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - if ([self.publicizeConnection requiresUserAction] && self.publicizeService.isSupported) { - return 3; - } - - return 2; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - if (section == 0) { - return NSLocalizedString(@"Settings", @"Section title"); - } - - return nil; -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - if (section == 0) { - return NSLocalizedString(@"Allow this connection to be used by all admins and users of your site.", @""); - } - - if (section == 1) { - if ([self.publicizeConnection mustDisconnectFacebook]) { - return [self textForFacebookFooter]; - } - - if ([self isSupportedConnectionBroken]) { - return [self textForBrokenConnectionFooter]; - } - } - - return nil; -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - return 1; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if (indexPath.section == 0) { - cell = [self switchTableViewCell]; - - } else if (indexPath.section == 1 && [self isSupportedConnectionBroken]) { - [self configureReconnectCell:cell]; - - } else if (indexPath.section == 1 && [self.publicizeConnection mustDisconnectFacebook]) { - [self configureLearnMoreCell:cell]; - - } else { - [self configureDisconnectCell:cell]; - } - - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - - // Do nothing when the service is unsupported. - // The first section's cell has a tap recognizer applied on the entire cell, preventing touch events from - // bubbling up to the table view delegate. But when the cell's interaction is disabled, this method will be called. - if (indexPath.section == 0 && !self.publicizeService.isSupported) { - return; - } - - if (indexPath.section == 1 && [self isSupportedConnectionBroken]) { - [self reconnectPublicizeConnection]; - } else if (indexPath.section == 1 && [self.publicizeConnection mustDisconnectFacebook]) { - [self openFacebookFAQ]; - } else { - [self promptToConfirmDisconnect]; - } -} - -- (SwitchTableViewCell *)switchTableViewCell -{ - SwitchTableViewCell *cell = [[SwitchTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; - cell.textLabel.text = NSLocalizedString(@"Available to all users", @""); - cell.on = self.publicizeConnection.shared; - - // disable interaction if the service is unsupported. - if (!self.publicizeService.isSupported) { - [cell.textLabel setTextColor:[UIColor secondaryLabelColor]]; - cell.userInteractionEnabled = NO; - } - - __weak __typeof(self) weakSelf = self; - cell.onChange = ^(BOOL value) { - [weakSelf updateSharedGlobally:value]; - }; - - return cell; -} - - -#pragma mark - Publicize Connection Methods - -- (void)updateSharedGlobally:(BOOL)shared -{ - __weak __typeof(self) weakSelf = self; - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService updateSharedForBlog:self.blog - shared:shared - forPublicizeConnection:self.publicizeConnection - success:nil - failure:^(NSError *error) { - DDLogError([error description]); - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Change failed", @"Message to show when Publicize globally shared setting failed")]; - [weakSelf.tableView reloadData]; - }]; -} - -- (void)reconnectPublicizeConnection -{ - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - - __weak __typeof(self) weakSelf = self; - if (self.helper == nil) { - [sharingService syncPublicizeServicesForBlog:self.blog - success:^{ - [[weakSelf helper] reconnectPublicizeConnection:weakSelf.publicizeConnection]; - } - failure:^(NSError * _Nullable error) { - [WPError showNetworkingAlertWithError:error]; - }]; - } else { - [self.helper reconnectPublicizeConnection:weakSelf.publicizeConnection]; - } -} - -- (void)disconnectPublicizeConnection -{ - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService deletePublicizeConnectionForBlog:self.blog pubConn:self.publicizeConnection success:nil failure:^(NSError *error) { - DDLogError([error description]); - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Disconnect failed", @"Message to show when Publicize disconnect failed")]; - }]; - - // Since the service optimistically deletes the connection, go ahead and pop. - [self.navigationController popViewControllerAnimated:YES]; -} - -- (void)promptToConfirmDisconnect -{ - NSString *message = NSLocalizedString(@"Disconnecting this account means published posts will no longer be automatically shared to %@", @"Explanatory text for the user. The `%@` is a placeholder for the name of a third-party sharing service."); - message = [NSString stringWithFormat:message, self.publicizeConnection.label]; - UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil - message:message - preferredStyle:UIAlertControllerStyleActionSheet]; - [alert addDestructiveActionWithTitle:NSLocalizedString(@"Disconnect", @"Verb. Title of a button. Tapping disconnects a third-party sharing service from the user's blog.") - handler:^(UIAlertAction * __unused action) { - [self disconnectPublicizeConnection]; - }]; - - [alert addCancelActionWithTitle:NSLocalizedString(@"Cancel", @"Verb. A button title.") handler:nil]; - - if ([UIDevice isPad]) { - alert.modalPresentationStyle = UIModalPresentationPopover; - [self presentViewController:alert animated:YES completion:nil]; - - NSUInteger section = [self isSupportedConnectionBroken] ? 2 : 1; - UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; - UIPopoverPresentationController *presentationController = alert.popoverPresentationController; - presentationController.permittedArrowDirections = UIPopoverArrowDirectionAny; - presentationController.sourceView = cell.textLabel; - presentationController.sourceRect = cell.textLabel.bounds; - } else { - [self presentViewController:alert animated:YES completion:nil]; - } -} - - -#pragma mark - SharingAuthenticationHelper Delegate Methods - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didConnectToService:(PublicizeService *)service withPublicizeConnection:(PublicizeConnection *)keyringConnection -{ - [SVProgressHUD showDismissibleSuccessWithStatus:NSLocalizedString(@"Reconnected", @"Message shwon to confirm a publicize connection has been successfully reconnected.")]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h deleted file mode 100644 index 6132645f5ff4..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h +++ /dev/null @@ -1,31 +0,0 @@ -#import - -@protocol SharingViewControllerDelegate - -- (void)didChangePublicizeServices; - -@end - -@protocol JetpackModuleHelperDelegate - -- (void)jetpackModuleEnabled; - -@end - -@class Blog; - -/** - * @brief Controller to display Calypso sharing options - */ -@interface SharingViewController : UITableViewController - -/** - * @brief Convenience initializer - * - * @param blog the blog from where to read the information from - * - * @return New instance of SharingViewController - */ -- (instancetype)initWithBlog:(Blog *)blog delegate:(id)delegate; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m deleted file mode 100644 index 7ee4c37807f0..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m +++ /dev/null @@ -1,454 +0,0 @@ -#import "SharingViewController.h" -#import "BlogService.h" -#import "SharingConnectionsViewController.h" -#import "WordPress-Swift.h" -@import WordPressData; -@import WordPressUI; -@import WordPressShared; - -typedef NS_ENUM(NSInteger, SharingSectionType) { - SharingSectionUndefined = 1000, - SharingSectionAvailableServices, - SharingSectionUnsupported, - SharingSectionSharingButtons -}; - -static NSString *const CellIdentifier = @"CellIdentifier"; - -@interface SharingViewController () - -@property (nonatomic, strong, readonly) Blog *blog; -@property (nonatomic, strong) NSArray *publicizeServices; - -// Contains Publicize services that are currently available for use. -@property (nonatomic, strong) NSArray *supportedServices; - -// Contains unsupported Publicize services that are deprecated or temporarily disabled. -@property (nonatomic, strong) NSArray *unsupportedServices; - -// A list of `SharingSectionType` that represents the sections displayed in the table view. -@property (nonatomic, strong) NSArray *sections; - -@property (nonatomic, weak) id delegate; -@property (nonatomic) PublicizeServicesState *publicizeServicesState; -@property (nonatomic) JetpackModuleHelper *jetpackModuleHelper; - -@end - -@implementation SharingViewController - -- (instancetype)initWithBlog:(Blog *)blog delegate:(id)delegate -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - self = [self initWithStyle:UITableViewStyleInsetGrouped]; - if (self) { - _blog = blog; - _publicizeServices = [NSMutableArray new]; - _supportedServices = @[]; - _unsupportedServices = @[]; - _delegate = delegate; - _publicizeServicesState = [PublicizeServicesState new]; - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.navigationItem.title = NSLocalizedString(@"Sharing", @"Title for blog detail sharing screen."); - - if (self.isModal) { - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone - target:self - action:@selector(doneButtonTapped)]; - } - - [self.tableView registerClass:[PublicizeServiceCell class] forCellReuseIdentifier:PublicizeServiceCell.cellId]; - self.tableView.cellLayoutMarginsFollowReadableWidth = YES; - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - [self.publicizeServicesState addInitialConnections:[self allConnections]]; - - self.navigationController.presentationController.delegate = self; - - if ([self.blog supports:BlogFeaturePublicize]) { - [self syncServices]; - } else { - self.jetpackModuleHelper = [[JetpackModuleHelper alloc] initWithViewController:self moduleName:@"publicize" blog:self.blog]; - - [self.jetpackModuleHelper showWithTitle:NSLocalizedString(@"Enable Publicize", "Text shown when the site doesn't have the Publicize module enabled.") subtitle:NSLocalizedString(@"In order to share your published posts to your social media you need to enable the Publicize module.", "Title of button to enable publicize.")]; - - self.tableView.dataSource = NULL; - } -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self.tableView reloadData]; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - [ReachabilityUtils dismissNoInternetConnectionNotice]; -} - -- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController -{ - [self notifyDelegatePublicizeServicesChangedIfNeeded]; -} - -- (void)refreshPublicizers -{ - self.publicizeServices = [PublicizeService allPublicizeServicesInContext:[self managedObjectContext] error:nil]; - - // Separate supported and unsupported Publicize services. - NSPredicate *supportedPredicate = [NSPredicate predicateWithFormat:@"status == %@", PublicizeService.defaultStatus]; - self.supportedServices = [self.publicizeServices filteredArrayUsingPredicate:supportedPredicate]; - - NSPredicate *unsupportedPredicate = [NSPredicate predicateWithFormat:@"status == %@", PublicizeService.unsupportedStatus]; - NSArray *unsupportedList = [self.publicizeServices filteredArrayUsingPredicate:unsupportedPredicate]; - - // only list unsupported services with existing connections. - NSMutableArray *unsupportedServicesWithConnections = [NSMutableArray new]; - for (PublicizeService *service in unsupportedList) { - if ([self connectionsForService:service].count > 0) { - [unsupportedServicesWithConnections addObject:service]; - } - } - self.unsupportedServices = unsupportedServicesWithConnections; - - // Refresh table sections in case anything changes. - [self refreshSections]; - - [self.tableView reloadData]; -} - -- (void)doneButtonTapped -{ - [self notifyDelegatePublicizeServicesChangedIfNeeded]; - [self dismissViewControllerAnimated:YES completion:nil]; -} - -#pragma mark - Table view sections - -- (void)refreshSections { - NSMutableArray *sections = [NSMutableArray new]; - - if ([self.supportedServices count] > 0) { - [sections addObject:@(SharingSectionAvailableServices)]; - } - - if (self.unsupportedServices.count > 0) { - [sections addObject:@(SharingSectionUnsupported)]; - } - - if ([self.blog supports:BlogFeatureShareButtons]) { - [sections addObject:@(SharingSectionSharingButtons)]; - } - - self.sections = sections; -} - -- (SharingSectionType)sectionTypeForIndex:(NSInteger)index -{ - if (index >= self.sections.count) { - return SharingSectionUndefined; - } - - return [self.sections objectAtIndex:index].intValue; -} - -#pragma mark - UITableView Delegate methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return self.sections.count; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - switch (sectionType) { - case SharingSectionAvailableServices: - return NSLocalizedString(@"Jetpack Social Connections", @"Section title for Publicize services in Sharing screen"); - - case SharingSectionUnsupported: - return NSLocalizedStringWithDefaultValue( - @"social.section.disabledTwitter.header", - nil, - [NSBundle mainBundle], - @"Twitter Auto-Sharing Is No Longer Available", - @"Section title for the disabled Twitter service in the Social screen"); - - case SharingSectionSharingButtons: - return NSLocalizedString(@"Sharing Buttons", @"Section title for the sharing buttons section in the Sharing screen"); - - default: - return nil; - } -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - if (sectionType == SharingSectionAvailableServices) { - return NSLocalizedString(@"Connect your favorite social media services to automatically share new posts with friends.", @""); - } - return nil; -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - switch (sectionType) { - case SharingSectionAvailableServices: - return self.supportedServices.count; - case SharingSectionUnsupported: - return self.unsupportedServices.count; - case SharingSectionSharingButtons: - return 1; - default: - return 0; - } -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - // TODO: Remove? - if ([self.publicizeServices count] > 0) { - PublicizeService *publicizer = self.publicizeServices[indexPath.row]; - NSArray *connections = [self connectionsForService:publicizer]; - if ([publicizer.serviceID isEqualToString:PublicizeService.googlePlusServiceID] && [connections count] == 0) { // Temporarily hiding Google+ - return 0; - } - } - - return UITableViewAutomaticDimension; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; - switch (sectionType) { - case SharingSectionAvailableServices: // fallthrough - case SharingSectionUnsupported: - return [self makePublicizeCellAtIndexPath:indexPath]; - case SharingSectionSharingButtons: - return [self makeManageButtonCell]; - default: - return [UITableViewCell new]; - } -} - -- (PublicizeService *)publicizeServiceForIndexPath:(NSIndexPath *)indexPath -{ - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; - switch (sectionType) { - case SharingSectionAvailableServices: - return self.supportedServices[indexPath.row]; - case SharingSectionUnsupported: - return self.unsupportedServices[indexPath.row]; - default: - return nil; - } -} - -- (UITableViewCell *)makePublicizeCellAtIndexPath:(NSIndexPath *)indexPath -{ - PublicizeServiceCell *cell = [self.tableView dequeueReusableCellWithIdentifier:PublicizeServiceCell.cellId]; - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - - PublicizeService *publicizer = [self publicizeServiceForIndexPath:indexPath]; - NSArray *connections = [self connectionsForService:publicizer]; - - // TODO: Remove? - if ([publicizer.serviceID isEqualToString:PublicizeService.googlePlusServiceID] && [connections count] == 0) { // Temporarily hiding Google+ - cell.hidden = YES; - return cell; - } - - [cell configureWith:publicizer connections:connections]; - return cell; -} - -- (UITableViewCell *)makeManageButtonCell -{ - WPTableViewCell *cell = [self. - tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if (!cell) { - cell = [[WPTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; - } - - [WPStyleGuide configureTableViewCell:cell]; - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - cell.textLabel.text = NSLocalizedString(@"Manage", @"Verb. Text label. Tapping displays a screen where the user can configure 'share' buttons for third-party services."); - cell.detailTextLabel.text = nil; - cell.imageView.image = nil; - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - UIViewController *controller; - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; - switch (sectionType) { - case SharingSectionAvailableServices: // fallthrough - case SharingSectionUnsupported: { - PublicizeService *publicizer = [self publicizeServiceForIndexPath:indexPath]; - controller = [[SharingConnectionsViewController alloc] initWithBlog:self.blog publicizeService:publicizer]; - [WPAppAnalytics track:WPAnalyticsStatSharingOpenedPublicize withBlog:self.blog]; - break; - } - - case SharingSectionSharingButtons: - controller = [ObjCBridge makeSharingButtonsViewControllerWithBlog:self.blog]; - [WPAppAnalytics track:WPAnalyticsStatSharingOpenedSharingButtonSettings withBlog:self.blog]; - break; - - default: - return; - } - - [self.navigationController pushViewController:controller animated:YES]; -} - -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - switch (sectionType) { - case SharingSectionUnsupported: - return [self makeTwitterDeprecationFooterView]; - - case SharingSectionSharingButtons: - if ([SharingViewController jetpackBrandingVisibile]) { - return [self makeJetpackBadge]; - } - break; - - default: - break; - } - - return nil; -} - -#pragma mark - JetpackModuleHelper - -- (void)jetpackModuleEnabled -{ - self.tableView.dataSource = self; - [self syncServices]; -} - -#pragma mark - Publicizer management - -// TODO: Good candidate for a helper method -- (NSArray *)connectionsForService:(PublicizeService *)publicizeService -{ - NSMutableArray *connections = [NSMutableArray array]; - for (PublicizeConnection *pubConn in self.blog.connections) { - if ([pubConn.service isEqualToString:publicizeService.serviceID]) { - [connections addObject:pubConn]; - } - } - return [NSArray arrayWithArray:connections]; -} - -- (NSArray *)allConnections -{ - NSMutableArray *allConnections = [NSMutableArray new]; - for (PublicizeService *service in self.publicizeServices) { - NSArray *connections = [self connectionsForService:service]; - if (connections.count > 0) { - [allConnections addObjectsFromArray:connections]; - } - } - return allConnections; -} - --(void)notifyDelegatePublicizeServicesChangedIfNeeded -{ - if ([self.publicizeServicesState hasAddedNewConnectionTo:[self allConnections]]) { - [self.delegate didChangePublicizeServices]; - } -} - -- (NSManagedObjectContext *)managedObjectContext -{ - return self.blog.managedObjectContext; -} - --(void)syncServices -{ - // Optimistically sync the sharing buttons. - [self syncSharingButtonsIfNeeded]; - - // Refreshes the tableview. - [self refreshPublicizers]; - - // Syncs servcies and connections. - [self syncPublicizeServices]; - -} - --(void)showConnectionError -{ - [ReachabilityUtils showNoInternetConnectionNoticeWithMessage: ReachabilityUtils.noConnectionMessage]; -} - -- (void)syncPublicizeServices -{ - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - __weak __typeof__(self) weakSelf = self; - [sharingService syncPublicizeServicesForBlog:self.blog success:^{ - [weakSelf syncConnections]; - } failure:^(NSError * __unused error) { - if (!ReachabilityUtils.isInternetReachable) { - [weakSelf showConnectionError]; - } else { - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Jetpack Social service synchronization failed", @"Message to show when Publicize service synchronization failed")]; - [weakSelf refreshPublicizers]; - } - }]; -} - -- (void)syncConnections -{ - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - __weak __typeof__(self) weakSelf = self; - [sharingService syncPublicizeConnectionsForBlog:self.blog success:^{ - [weakSelf refreshPublicizers]; - } failure:^(NSError * __unused error) { - if (!ReachabilityUtils.isInternetReachable) { - [weakSelf showConnectionError]; - } else { - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Jetpack Social connection synchronization failed", @"Message to show when Publicize connection synchronization failed")]; - [weakSelf refreshPublicizers]; - } - }]; -} - -- (void)syncSharingButtonsIfNeeded -{ - // Sync sharing buttons if they have never been synced. Otherwise, the - // management vc can worry about fetching the latest sharing buttons. - NSArray *buttons = [SharingButton allSharingButtonsForBlog:self.blog inContext:[self managedObjectContext] error:nil]; - if ([buttons count] > 0) { - return; - } - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService syncSharingButtonsForBlog:self.blog success:nil failure:^(NSError *error) { - DDLogError([error description]); - }]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift deleted file mode 100644 index f4f6c5a29bea..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit - -extension SharingViewController { - - static let jetpackBadgePadding: CGFloat = 30 - - @objc - public static func jetpackBrandingVisibile() -> Bool { - return JetpackBrandingVisibility.all.enabled - } - - @objc - public func makeJetpackBadge() -> UIView { - let textProvider = JetpackBrandingTextProvider(screen: JetpackBadgeScreen.sharing) - let badge = JetpackButton.makeBadgeView(title: textProvider.brandingText(), - topPadding: Self.jetpackBadgePadding, - bottomPadding: Self.jetpackBadgePadding, - target: self, - selector: #selector(presentJetpackOverlay)) - return badge - } - - @objc - public func presentJetpackOverlay() { - JetpackBrandingCoordinator.presentOverlay(from: self) - JetpackBrandingAnalyticsHelper.trackJetpackPoweredBadgeTapped(screen: .sharing) - } - - // MARK: Twitter Deprecation - - @objc - public func makeTwitterDeprecationFooterView() -> TwitterDeprecationTableFooterView { - let footerView = TwitterDeprecationTableFooterView() - footerView.presentingViewController = self - footerView.source = "social_connection_list" - - return footerView - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift deleted file mode 100644 index 2c619b1f54b5..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift +++ /dev/null @@ -1,129 +0,0 @@ -import UIKit -import WordPressUI - -/// A subclass implementation of `UITableViewHeaderFooterView` that displays a text with a tappable link. -/// Specifically used for Twitter deprecation purposes. -/// -@objc public class TwitterDeprecationTableFooterView: UITableViewHeaderFooterView { - - // The view controller that will present the web view. - @objc public weak var presentingViewController: UIViewController? = nil - - // For tracking purposes. See https://wp.me/pctCYC-OI#tracks - @objc public var source: String? = nil - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.font = .preferredFont(forTextStyle: .footnote) - label.textColor = .secondaryLabel - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.lineHeightMultiple - - let attributedString = NSMutableAttributedString(string: "\(Constants.deprecationNoticeText) ", attributes: [ - .paragraphStyle: paragraphStyle - ]) - - if let attachmentURL = Constants.blogPostURL { - let hyperlinkText = NSAttributedString(string: Constants.hyperlinkText, attributes: [ - .paragraphStyle: paragraphStyle, - .attachment: attachmentURL, - .foregroundColor: UIAppColor.primary - ]) - attributedString.append(hyperlinkText) - } - - label.attributedText = attributedString - label.isUserInteractionEnabled = true - - return label - }() - - // MARK: Methods - - public override init(reuseIdentifier: String?) { - super.init(reuseIdentifier: reuseIdentifier) - setupSubviews() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupSubviews() - } -} - -// MARK: - Private methods - -private extension TwitterDeprecationTableFooterView { - - func setupSubviews() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped)) - label.addGestureRecognizer(tapGesture) - - contentView.addSubview(label) - contentView.pinSubviewToAllEdgeMargins(label) - } - - @objc func labelTapped(_ sender: UITapGestureRecognizer) { - guard let presentingViewController, - let source, - let attributedText = label.attributedText else { - return - } - - // detect the tap location within the attributed text. - let location = sender.location(in: label) - let textStorage = NSTextStorage(attributedString: attributedText) - let textContainer = NSTextContainer(size: label.bounds.size) - let layoutManager = NSLayoutManager() - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - - textContainer.lineFragmentPadding = 0 - textContainer.lineBreakMode = label.lineBreakMode - textContainer.maximumNumberOfLines = label.numberOfLines - - let characterIndex = layoutManager.characterIndex(for: location, - in: textContainer, - fractionOfDistanceBetweenInsertionPoints: nil) - - guard characterIndex < textStorage.length else { - return - } - - let attributes = attributedText.attributes(at: characterIndex, effectiveRange: nil) - guard let attachmentURL = attributes[.attachment] as? URL else { - return - } - - WPAnalytics.track(.jetpackSocialTwitterNoticeLinkTapped, properties: ["source": source]) - - // Ideally this shouldn't be the responsibility of this class, but I'm keeping it simple since it's temporary. - let webViewController = WebViewControllerFactory.controller(url: attachmentURL, source: source) - let navigationController = UINavigationController(rootViewController: webViewController) - presentingViewController.present(navigationController, animated: true) - } - - // MARK: Constants - - enum Constants { - // adjust attributedText's line height. The default settings look slightly stretched. - static let lineHeightMultiple = 0.9 - - static let blogPostURL = URL(string: "https://wordpress.com/blog/2023/04/29/why-twitter-auto-sharing-is-coming-to-an-end/") - - static let deprecationNoticeText = NSLocalizedString( - "social.twitterDeprecation.text", - value: "Twitter auto-sharing is no longer available due to Twitter's changes in terms and pricing.", - comment: "A smallprint that hints the reason behind why Twitter is deprecated." - ) - - static let hyperlinkText = NSLocalizedString( - "social.twitterDeprecation.link", - value: "Find out more", - comment: "Text for a hyperlink that allows the user to learn more about the Twitter deprecation." - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift index 145ff1700ced..b5374bf14986 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift @@ -6,36 +6,6 @@ import WordPressShared /// Sharing feature. /// extension WPStyleGuide { - /// Create an UIImageView showing the notice gridicon. - /// - /// - Returns: A UIImageView - /// - @objc public class func sharingCellWarningAccessoryImageView() -> UIImageView { - let imageSize = 20.0 - let horizontalPadding = 8.0 - let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize + horizontalPadding, height: imageSize)) - - imageView.image = UIImage(named: "sharing-notice") - imageView.tintColor = jazzyOrange() - imageView.contentMode = .right - return imageView - } - - /// Create an UIImageView showing the notice gridicon. - /// - /// - Returns: A UIImageView - /// - @objc public class func sharingCellErrorAccessoryImageView() -> UIImageView { - let imageSize = 20.0 - let horizontalPadding = 8.0 - let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize + horizontalPadding, height: imageSize)) - - imageView.image = UIImage(named: "sharing-notice") - imageView.tintColor = .systemRed - imageView.contentMode = .right - return imageView - } - /// Creates an icon for the specified service, or a the default social icon. /// /// - Parameters: @@ -67,8 +37,4 @@ extension WPStyleGuide { } return image!.withRenderingMode(.alwaysTemplate) } - - public class func socialIcon(for service: NSString) -> UIImage { - UIImage(named: "icon-\(service)") ?? iconForService(service) - } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index b2fec133f27f..7ec478c3d234 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -321,7 +321,6 @@ class GutenbergViewController: UIViewController, PostEditor, PublishingEditor { super.init(nibName: nil, bundle: nil) self.navigationBarManager.delegate = self - disableSocialConnectionsIfNecessary() } required init?(coder aDecoder: NSCoder) { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift deleted file mode 100644 index c338ac4a5abd..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift +++ /dev/null @@ -1,138 +0,0 @@ -import SwiftUI -import AsyncImageKit -import WordPressData -import WordPressUI - -struct JetpackSocialNoConnectionView: View { - - let viewModel: JetpackSocialNoConnectionViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 12.0) { - HStack(spacing: -5.0) { - ForEach(viewModel.icons, id: \.self) { icon in - iconImage(image: icon.image, url: icon.url) - } - } - .accessibilityElement() - .accessibilityLabel(Constants.iconGroupAccessibilityLabel) - Text(Constants.bodyText) - .font(.callout) - .foregroundColor(Color(viewModel.bodyTextColor)) - HStack { - Text(Constants.connectText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onConnectTap?() - } - if !viewModel.hideNotNow { - Spacer() - Text(Constants.notNowText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onNotNowTap?() - } - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding(viewModel.padding) - .background(Color(viewModel.preferredBackgroundColor)) - } - - func iconImage(image: UIImage, url: URL?) -> some View { - CachedAsyncImage(url: url) { image in - image - .icon(backgroundColor: viewModel.preferredBackgroundColor) - } placeholder: { - Image(uiImage: image) - .icon(backgroundColor: viewModel.preferredBackgroundColor) - } - } -} - -// MARK: - JetpackSocialNoConnectionView Extension - -extension JetpackSocialNoConnectionView { - static func createHostController(with viewModel: JetpackSocialNoConnectionViewModel = JetpackSocialNoConnectionViewModel()) -> UIHostingController { - let hostController = UIHostingController(rootView: JetpackSocialNoConnectionView(viewModel: viewModel)) - hostController.view.translatesAutoresizingMaskIntoConstraints = false - hostController.view.backgroundColor = viewModel.preferredBackgroundColor - return hostController - } -} - -// MARK: - Image Extension - -private extension Image { - func icon(backgroundColor: UIColor) -> some View { - self - .resizable() - .frame(width: 32.0, height: 32.0) - .background(Color(backgroundColor)) - .clipShape(Circle()) - .overlay(Circle().stroke(Color(backgroundColor), lineWidth: 2.0)) - } -} - -// MARK: - View model - -struct JetpackSocialNoConnectionViewModel { - - struct IconInfo: Hashable { - let image: UIImage - let url: URL? - } - - let padding: EdgeInsets - let hideNotNow: Bool - let preferredBackgroundColor: UIColor - let bodyTextColor: UIColor - let onConnectTap: (() -> Void)? - let onNotNowTap: (() -> Void)? - let icons: [IconInfo] - - init(services: [PublicizeService] = [], - padding: EdgeInsets = Constants.defaultPadding, - hideNotNow: Bool = false, - preferredBackgroundColor: UIColor? = nil, - bodyTextColor: UIColor = .label, - onConnectTap: (() -> Void)? = nil, - onNotNowTap: (() -> Void)? = nil) { - self.padding = padding - self.hideNotNow = hideNotNow - self.preferredBackgroundColor = preferredBackgroundColor ?? Constants.defaultBackgroundColor - self.bodyTextColor = bodyTextColor - self.onConnectTap = onConnectTap - self.onNotNowTap = onNotNowTap - - var images = [IconInfo]() - for service in services { - let icon = WPStyleGuide.socialIcon(for: service.serviceID as NSString) - let url: URL? = service.name == .unknown ? URL(string: service.icon) : nil - images.append(IconInfo(image: icon, url: url)) - } - self.icons = images - } -} - -// MARK: - Constants - -private struct Constants { - static let defaultPadding = EdgeInsets(top: 16.0, leading: 16.0, bottom: 24.0, trailing: 16.0) - static let defaultBackgroundColor = UIColor.secondarySystemGroupedBackground - static let bodyText = NSLocalizedString("social.noconnection.body", - value: "Increase your traffic by auto-sharing your posts with your friends on social media.", - comment: "Body text for the Jetpack Social no connection view") - static let connectText = NSLocalizedString("social.noconnection.connectAccounts", - value: "Connect accounts", - comment: "Title for the connect button to add social sharing for the Jetpack Social no connection view") - static let notNowText = NSLocalizedString("social.noconnection.notnow", - value: "Not now", - comment: "Title for the not now button to hide the Jetpack Social no connection view") - static let iconGroupAccessibilityLabel = NSLocalizedString("social.noconnection.icons.accessibility.label", - value: "Social media icons", - comment: "Accessibility label for the social media icons in the Jetpack Social no connection view") -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift deleted file mode 100644 index 07aae6fd5481..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift +++ /dev/null @@ -1,84 +0,0 @@ -import SwiftUI -import WordPressData -import WordPressUI - -struct JetpackSocialNoSharesView: View { - - let viewModel: JetpackSocialNoSharesViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 4.0) { - HStack(spacing: -5.0) { - ForEach(viewModel.services, id: \.self) { service in - iconImage(service.localIconImage) - } - } - .padding(.bottom, 8.0) - .accessibilityElement() - .accessibilityLabel(Constants.iconGroupAccessibilityLabel) - Text(bodyText) - .font(.callout) - .foregroundColor(Color(UIColor.secondaryLabel)) - .padding(.bottom, 5.0) - Text(Constants.subscribeText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onSubscribeTap() - } - .accessibilityAddTraits(.isButton) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding(EdgeInsets(top: 0.0, leading: 16.0, bottom: 8.0, trailing: 16.0)) - } - - var bodyText: String { - if viewModel.totalServiceCount > 1 { - return String(format: Constants.pluralShareTextFormat, viewModel.totalServiceCount) - } - return Constants.singularShareText - } - - func iconImage(_ image: UIImage) -> some View { - Image(uiImage: image) - .resizable() - .frame(width: 32.0, height: 32.0) - .opacity(0.36) - .background(Color(UIColor.secondarySystemGroupedBackground)) - .clipShape(Circle()) - .overlay(Circle().stroke(Color(UIColor.secondarySystemGroupedBackground), lineWidth: 2.0)) - } -} - -// MARK: - View model - -struct JetpackSocialNoSharesViewModel { - - let services: [PublicizeService.ServiceName] - let totalServiceCount: Int - let onSubscribeTap: () -> Void - - init(services: [PublicizeService.ServiceName], totalServiceCount: Int, onSubscribeTap: @escaping () -> Void) { - self.services = services - self.totalServiceCount = totalServiceCount - self.onSubscribeTap = onSubscribeTap - } -} - -// MARK: - Constants - -private struct Constants { - - static let pluralShareTextFormat = NSLocalizedString("social.noshares.body.plural", - value: "Your posts won’t be shared to your %1$d social accounts.", - comment: "Plural body text for the Jetpack Social no shares dashboard card. %1$d is the number of social accounts the user has.") - static let singularShareText = NSLocalizedString("social.noshares.body.singular", - value: "Your posts won’t be shared to your social account.", - comment: "Singular body text for the Jetpack Social no shares dashboard card.") - static let subscribeText = NSLocalizedString("social.noshares.subscribe", - value: "Subscribe to share more", - comment: "Title for the button to subscribe to Jetpack Social on the no shares dashboard card") - static let iconGroupAccessibilityLabel = NSLocalizedString("social.noshares.icons.accessibility.label", - value: "Social media icons", - comment: "Accessibility label for the social media icons in the Jetpack Social no shares dashboard card") -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift deleted file mode 100644 index eb481e6287f5..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift +++ /dev/null @@ -1,68 +0,0 @@ -import SwiftUI -import WordPressUI - -struct JetpackSocialSettingsRemainingSharesView: View { - - let viewModel: JetpackSocialRemainingSharesViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 4.0) { - HStack(spacing: 8.0) { - if viewModel.displayWarning { - Image("icon-warning") - .resizable() - .frame(width: 16.0, height: 16.0) - } - remainingText - } - Text(Constants.subscribeText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onSubscribeTap() - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(EdgeInsets(top: 12.0, leading: 16.0, bottom: 12.0, trailing: 16.0)) - .background(Color(UIColor.secondarySystemGroupedBackground)) - } - - private var remainingText: some View { - let sharesRemainingString = String(format: Constants.remainingTextFormat, viewModel.remaining) - let sharesRemaining = Text(sharesRemainingString).font(.callout) - if viewModel.displayWarning { - return sharesRemaining - } - let remainingDays = Text(Constants.remainingEndText).font(.callout).foregroundColor(.secondary) - return sharesRemaining + remainingDays - } - - private struct Constants { - static let remainingTextFormat = NSLocalizedString("postsettings.social.shares.text.format", - value: "%1$d social shares remaining", - comment: "Beginning text of the remaining social shares a user has left." - + " %1$d is their current remaining shares." - + " This text is combined with ' in the next 30 days' if there is no warning displayed.") - static let remainingEndText = NSLocalizedString("postsettings.social.remainingshares.text.part", - value: " in the next 30 days", - comment: "The second half of the remaining social shares a user has." - + " This is only displayed when there is no social limit warning.") - static let subscribeText = NSLocalizedString("postsettings.social.remainingshares.subscribe", - value: "Subscribe now to share more", - comment: "Title for the button to subscribe to Jetpack Social on the remaining shares view") - } -} - -struct JetpackSocialRemainingSharesViewModel { - let remaining: Int - let displayWarning: Bool - let onSubscribeTap: () -> Void - - init(remaining: Int, - displayWarning: Bool, - onSubscribeTap: @escaping () -> Void) { - self.remaining = remaining - self.displayWarning = displayWarning - self.onSubscribeTap = onSubscribeTap - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift deleted file mode 100644 index 836e83064bc8..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift +++ /dev/null @@ -1,9 +0,0 @@ -import WordPressData - -extension PostEditor { - - // Deprecated: Jetpack Social no longer enforces per-post share limits, and post editing now uses - // connection_id-keyed PostSocialSharingDraft metadata instead of keyring publicize state. - func disableSocialConnectionsIfNecessary() { - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift index e44bd5a3c52c..6fc27c5a0b58 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift @@ -39,7 +39,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting @Published var suggestedTags: [String] = [] @Published var customTaxonomies: [SiteTaxonomy] = [] @Published var parentPageText: String? - @Published var socialSharingState: PostSettingsSocialSharingSectionState? @Published var isShowingDeletedAlert = false private let socialConnectionsService: SiteSocialConnectionsService? @@ -50,7 +49,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting private var addConnectionCoordinator: AddConnectionCoordinator? private let originalSettings: PostSettings - private let preferences: UserPersistentRepository private var cancellables = Set() var onDismiss: (() -> Void)? @@ -199,14 +197,12 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting blog: Blog, socialConnectionsService: SiteSocialConnectionsService?, isStandalone: Bool = false, - context: PostSettingsContext = .settings, - preferences: UserPersistentRepository = UserDefaults.standard + context: PostSettingsContext = .settings ) { self.editorService = editorService self.blog = blog self.isStandalone = isStandalone self.context = context - self.preferences = preferences self.client = editorService.client let capabilities = PostSettingsCapabilities(from: editorService.details) self.capabilities = capabilities @@ -277,16 +273,14 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting editorService: CustomPostEditorService, blog: Blog, isStandalone: Bool = false, - context: PostSettingsContext = .settings, - preferences: UserPersistentRepository = UserDefaults.standard + context: PostSettingsContext = .settings ) { self.init( editorService: editorService, blog: blog, socialConnectionsService: Self.resolveSocialConnectionsService(blog: blog, details: editorService.details), isStandalone: isStandalone, - context: context, - preferences: preferences + context: context ) } @@ -401,8 +395,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting viewController?.navigationController?.pushViewController(categoriesVC, animated: true) } - func showSocialSharingOptions() {} - var v2SocialSharing: V2SocialSharingBinding? { guard let service = socialConnectionsService, settings.status != .publishPrivate @@ -604,7 +596,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting details: PostTypeDetailsWithEditContext ) -> SiteSocialConnectionsService? { guard PostSettingsCapabilities(from: details).supportsPublicize, - FeatureFlag.socialSharingV2.enabled, blog.supports(.publicize), let service = JetpackSocialFactory.shared.connectionsService(for: blog) else { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift index 8244ae94eeb0..6e46e9c0d418 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift @@ -65,7 +65,6 @@ struct PostSettings: Hashable { // MARK: - Post-specific var postFormat: String? var isStickyPost = false - var sharing: PostSocialSharingSettings? /// Publicize draft state used for change detection and REST parameter encoding. /// When available, `connectionsByID` carries the full post-level connection state. var socialSharingDraft: PostSocialSharingDraft? @@ -125,7 +124,6 @@ struct PostSettings: Hashable { if PostSocialSharing.isEligible(for: post) { socialSharingDraft = PostSocialSharingDraft(socialMetadata: PostMetadataContainer(post)) } - // `sharing` (the legacy keyring-keyed model) is intentionally no longer populated. allowComments = post.commentsStatus.map { $0 != RemotePostDiscussionState.closed.rawValue } allowPings = post.pingsStatus.map { $0 != RemotePostDiscussionState.closed.rawValue } case let page as Page: @@ -183,9 +181,6 @@ struct PostSettings: Hashable { parentPageID = post.parent.map { Int($0) } - // Legacy Publicize settings are not available for REST API posts. - sharing = nil - let socialSharingDraft = PostSocialSharingDraft( fromPostAdditionalFields: post.additionalFields, meta: post.meta @@ -642,76 +637,3 @@ extension PostStatus { } } } - -/// A value-type representation of `PublicizeService` for the current blog that's simplified for the auto-sharing flow. -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -struct PostSocialSharingSettings: Hashable { - var services: [Service] - var message: String - var sharingLimit: PublicizeInfo.SharingLimit? - - struct Service: Hashable { - let name: PublicizeService.ServiceName - var connections: [Connection] - } - - struct Connection: Hashable { - let account: String - let keyringID: Int - var enabled: Bool - } - - static func make(for post: Post) -> PostSocialSharingSettings? { - guard let context = post.managedObjectContext else { - wpAssertionFailure("missing moc") - return nil - } - - let connections = post.blog.sortedConnections - - // first, build a dictionary to categorize the connections. - var connectionsMap = [PublicizeService.ServiceName: [PublicizeConnection]]() - connections.filter { !$0.requiresUserAction() } - .forEach { connection in - let name = PublicizeService.ServiceName(rawValue: connection.service) ?? .unknown - var serviceConnections = connectionsMap[name] ?? [] - serviceConnections.append(connection) - connectionsMap[name] = serviceConnections - } - - let publicizeServices: [PublicizeService] - do { - publicizeServices = try PublicizeService.allPublicizeServices(in: context) - } catch { - wpAssertionFailure("failed to fetch services", userInfo: ["error": error.localizedDescription]) - return nil - } - - let services = publicizeServices.compactMap { service -> PostSocialSharingSettings.Service? in - // skip services without connections. - guard let serviceConnections = connectionsMap[service.name], - !serviceConnections.isEmpty - else { - return nil - } - - return PostSocialSharingSettings.Service( - name: service.name, - connections: serviceConnections.map { - .init( - account: $0.externalDisplay, - keyringID: $0.keyringConnectionID.intValue, - enabled: !post.publicizeConnectionDisabledForKeyringID($0.keyringConnectionID) - ) - } - ) - } - - return PostSocialSharingSettings( - services: services, - message: post.publicizeMessage ?? post.titleForDisplay(), - sharingLimit: post.blog.sharingLimit - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 135dc01b0c97..58f10c8cdbb0 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -153,7 +153,7 @@ struct PostSettingsFormContentView: Vi excerptSection } generalSection - socialSharingSectionDispatcher + socialSharingSection accessSection moreOptionsSection } @@ -383,7 +383,7 @@ struct PostSettingsFormContentView: Vi // MARK: - "Social Sharing" Section @ViewBuilder - private var socialSharingSectionDispatcher: some View { + private var socialSharingSection: some View { if let binding = viewModel.v2SocialSharing { // The server early-returns updates to `jetpack_publicize_connections` // for already-published posts, and re-share isn't supported in the @@ -395,28 +395,6 @@ struct PostSettingsFormContentView: Vi onAddConnection: binding.onAddConnection ) } - } else { - socialSharingSection - } - } - - @ViewBuilder - private var socialSharingSection: some View { - if let state = viewModel.socialSharingState { - Section { - switch state { - case .setup(let viewModel): - JetpackSocialNoConnectionView(viewModel: viewModel) - case .connected: - if let settings = viewModel.settings.sharing { - LegacyNavigationLinkRow(action: viewModel.showSocialSharingOptions) { - PrepublishingAutoSharingView(model: settings) - } - } - } - } header: { - SectionHeader(Strings.socialSharing) - } } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 636dad8261e7..2f66e9874b40 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -36,7 +36,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM @Published private(set) var suggestedTags: [String] = [] @Published private(set) var customTaxonomies: [SiteTaxonomy] = [] @Published private(set) var parentPageText: String? - @Published private(set) var socialSharingState: PostSettingsSocialSharingSectionState? @Published var isShowingDeletedAlert = false @@ -170,7 +169,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM } private let originalSettings: PostSettings - private let preferences: UserPersistentRepository private var isSuggestedTagsRefreshNeeded = true private var cancellables = Set() @@ -187,15 +185,13 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM init( post: AbstractPost, isStandalone: Bool = false, - context: PostSettingsContext = .settings, - preferences: UserPersistentRepository = UserDefaults.standard + context: PostSettingsContext = .settings ) { self.post = post self.blog = post.blog self.capabilities = post is Post ? .post() : .page() self.isStandalone = isStandalone self.context = context - self.preferences = preferences self.client = try? WordPressClientFactory.shared.instance(for: .init(blog: post.blog)) self.socialConnectionsService = Self.resolveSocialConnectionsService(for: post) @@ -221,7 +217,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM refreshDisplayedTags() refreshCustomTaxonomies() refreshParentPageText() - refreshSocialSharingState() resolveAbstractPostTerms() WPAnalytics.track(.postSettingsShown) @@ -332,9 +327,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM if old.parentPageID != new.parentPageID { refreshParentPageText() } - if old.status != new.status { - refreshSocialSharingState() - } } private func refreshDisplayedCategories() { @@ -544,110 +536,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM coordinator.start() } - private func refreshSocialSharingState() { - guard settings.status != .publishPrivate, - socialConnectionsService != nil, - let post = post as? Post, - isPostEligibleForSocialSharing(post) - else { - socialSharingState = nil - return - } - if (blog.connections ?? []).isEmpty { - if isSocialConnectionSetupDismissed { - socialSharingState = nil - } else { - socialSharingState = .setup(makeSocialSharingSetupViewModel()) - } - } else { - socialSharingState = .connected - } - } - - private func isPostEligibleForSocialSharing(_ post: Post) -> Bool { - BuildSettings.current.brand == .jetpack - && RemoteFeatureFlag.jetpackSocialImprovements.enabled() - && post.status != .publishPrivate - && !getPublicizeServices().isEmpty - && blog.supports(.publicize) - } - - private func getPublicizeServices() -> [PublicizeService] { - let context = ContextManager.shared.mainContext - return (try? PublicizeService.allSupportedServices(in: context)) ?? [] - } - - /// Convenience variable representing whether the No Connection view has been dismissed. - /// Note: the value is stored per site. - private var isSocialConnectionSetupDismissed: Bool { - get { - guard let blogID = blog.dotComID?.intValue, - let dictionary = preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool], - let value = dictionary["\(blogID)"] - else { - return false - } - return value - } - set { - guard let blogID = blog.dotComID?.intValue else { - return wpAssertionFailure("blogID missing") - } - var dictionary = (preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool]) ?? .init() - dictionary["\(blogID)"] = newValue - preferences.set(dictionary, forKey: Constants.noConnectionKey) - } - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - private func makeSocialSharingSetupViewModel() -> JetpackSocialNoConnectionViewModel { - JetpackSocialNoConnectionViewModel( - services: getPublicizeServices(), - padding: .zero, - onConnectTap: { [weak self] in self?.showSocialSharingSetupScreen() }, - onNotNowTap: { [weak self] in self?.didDismissSocialSharingSetupPrompt() } - ) - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - private func showSocialSharingSetupScreen() { - guard let sharingVC = SharingViewController(blog: blog, delegate: self) else { - return wpAssertionFailure("failed to instantiate SharingVC") - } - track(.jetpackSocialNoConnectionCTATapped) - let navigationVC = UINavigationController(rootViewController: sharingVC) - viewController?.present(navigationVC, animated: true) - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - private func didDismissSocialSharingSetupPrompt() { - track(.jetpackSocialNoConnectionCardDismissed) - isSocialConnectionSetupDismissed = true - withAnimation { - socialSharingState = nil - } - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - func showSocialSharingOptions() { - guard let blogID = blog.dotComID?.intValue, - let settings = settings.sharing - else { - return wpAssertionFailure("invalid context") - } - let optionsVC = PrepublishingSocialAccountsViewController( - blogID: blogID, - model: settings, - delegate: self, - coreDataStack: ContextManager.shared - ) - viewController?.navigationController?.pushViewController(optionsVC, animated: true) - } - // MARK: - Navigation func showCategoriesPicker() { @@ -720,40 +608,3 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM } } } - -extension PostSettingsViewModel: @MainActor SharingViewControllerDelegate { - func didChangePublicizeServices() { - refreshSocialSharingState() - } -} - -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -extension PostSettingsViewModel: @MainActor PrepublishingSocialAccountsDelegate { - func didUpdateSharingLimit(with newValue: PublicizeInfo.SharingLimit?) { - settings.sharing?.sharingLimit = newValue - } - - func didFinish(with connectionChanges: [Int: Bool], message: String?) { - guard var settings = settings.sharing else { - return wpAssertionFailure("social sharing settings missing") - } - settings.services = settings.services.map { - var service = $0 - service.connections = service.connections.map { - var connection = $0 - if let isEnabled = connectionChanges[connection.keyringID] { - connection.enabled = isEnabled - } - return connection - } - return service - } - settings.message = message ?? "" - self.settings.sharing = settings - } -} - -private enum Constants { - static let noConnectionKey = "prepublishing-social-no-connection-view-hidden" -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift index 84d4504b7e5c..392a3fe4dddb 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift @@ -34,14 +34,6 @@ enum PostSettingsMoreOption: Identifiable { var id: Self { self } } -/// The state of the social sharing section. -enum PostSettingsSocialSharingSectionState { - /// The initial prompt to set up connections. - case setup(JetpackSocialNoConnectionViewModel) - /// The site has existing connections. - case connected -} - // MARK: - Protocol /// Defines the contract for a post settings view model consumed by `PostSettingsView` @@ -69,7 +61,6 @@ protocol PostSettingsViewModelProtocol: ObservableObject { var suggestedTags: [String] { get } var customTaxonomies: [SiteTaxonomy] { get } var parentPageText: String? { get } - var socialSharingState: PostSettingsSocialSharingSectionState? { get } /// Non-nil when the host screen should render the v2 social sharing /// section. Custom post settings and eligible `AbstractPost` settings @@ -117,7 +108,6 @@ protocol PostSettingsViewModelProtocol: ObservableObject { func didSelectSuggestedTag(_ tag: String) func didSelectTags(_ tags: [TagsViewModel.SelectedTerm]) func didSelectTerms(_ terms: [TagsViewModel.SelectedTerm], forTaxonomySlug: String) - func showSocialSharingOptions() func showCategoriesPicker() func parentPagePickerDestination() -> ParentPagePickerDestination? } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 92e4a35b415a..d70e0aef88b1 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -263,11 +263,6 @@ enum PrepublishingSheetStrings { value: "Tags", comment: "Label for a cell in the pre-publishing sheet" ) - static let jetpackSocial = NSLocalizedString( - "prepublishing.jetpackSocial", - value: "Jetpack Social", - comment: "Label for a cell in the pre-publishing sheet" - ) static let immediately = NSLocalizedString( "prepublishing.publishDateImmediately", value: "Immediately", diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift b/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift deleted file mode 100644 index 800d9fb3ef80..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift +++ /dev/null @@ -1,230 +0,0 @@ -import SwiftUI -import WordPressUI - -struct PrepublishingAutoSharingView: View { - - let model: PostSocialSharingSettings - - @Environment(\.sizeCategory) private var sizeCategory - - @ScaledMetric(relativeTo: .subheadline) private var warningIconLength = 16.0 - - @ScaledMetric(relativeTo: .body) private var imageLength = 28.0 - - var body: some View { - Group { - if shouldStackContentVertically { - VStack(alignment: .leading, spacing: 8.0) { content } - } else { - HStack { content } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .accessibilityElement(children: .combine) - .accessibilityAddTraits(.isButton) - } - - private var content: some View { - Group { - textStack - if !model.services.isEmpty { - if !shouldStackContentVertically { - Spacer(minLength: .zero) // to push the icons to the trailing side in horizontal layout. - } - socialIconsView - } - } - } - - private var textStack: some View { - VStack(alignment: .leading, spacing: 2.0) { - Text(model.labelText) - .font(.body) - .foregroundColor(Color(.label)) - if let text = model.remainingSharesText { - remainingSharesLabel(text: text, showsWarning: model.showsWarning) - } - } - .lineLimit(1) - } - - @ViewBuilder - private func remainingSharesLabel(text: String, showsWarning: Bool) -> some View { - Label { - Text(text) - .font(.subheadline) - .foregroundColor(Color(showsWarning ? Constants.warningColor : .secondaryLabel)) - } icon: { - if showsWarning { - Image("icon-warning") - .resizable() - .frame(width: warningIconLength, height: warningIconLength) - .accessibilityElement() - .accessibilityLabel(Constants.warningIconAccessibilityText) - } - } - .accessibilityLabel(showsWarning ? "\(Constants.warningIconAccessibilityText), \(text)" : text) - } - - private var socialIconsView: some View { - HStack(spacing: -6) { - ForEach(model.services, id: \.self) { service in - iconImage(service.name.localIconImage, opaque: service.usesOpaqueIcon) - } - } - } - - private func iconImage(_ uiImage: UIImage, opaque: Bool) -> some View { - Image(uiImage: uiImage) - .resizable() - .frame(width: imageLength, height: imageLength) - .opacity(opaque ? 1.0 : Constants.disabledSocialIconOpacity) - .background(Color(.secondarySystemGroupedBackground)) - .clipShape(Circle()) - } -} - -// MARK: - View Helpers - -private extension PrepublishingAutoSharingView { - - var shouldStackContentVertically: Bool { - (model.services.count > Constants.maxServicesForHorizontalLayout) || sizeCategory.isAccessibilityCategory - } - - enum Constants { - static let maxServicesForHorizontalLayout = 3 - static let disabledSocialIconOpacity: CGFloat = 0.36 - static let warningColor = UIAppColor.yellow(.shade50) - - static let warningIconAccessibilityText = NSLocalizedString( - "prepublishing.social.warningIcon.accessibilityHint", - value: "Warning", - comment: "a VoiceOver description for the warning icon to hint that the remaining shares are low." - ) - } -} - -// MARK: - PrepublishingAutoSharingModel Private Extensions - -private extension PostSocialSharingSettings.Service { - /// Whether the icon for this service should be opaque or transparent. - /// If at least one account is enabled, an opaque version should be shown. - var usesOpaqueIcon: Bool { - connections.reduce(false) { partialResult, connection in - return partialResult || connection.enabled - } - } - - var enabledConnections: [PostSocialSharingSettings.Connection] { - connections.filter { $0.enabled } - } -} - -private extension PostSocialSharingSettings { - var enabledConnectionsCount: Int { - services.reduce(0) { $0 + $1.enabledConnections.count } - } - - var totalConnectionsCount: Int { - services.reduce(0) { $0 + $1.connections.count } - } - - var showsWarning: Bool { - guard let remaining = sharingLimit?.remaining else { - return false - } - return totalConnectionsCount > remaining - } - - var labelText: String { - switch (enabledConnectionsCount, totalConnectionsCount) { - case (let enabled, _) where enabled == 0: - // not sharing to any social media - return Strings.notSharingText - - case (let enabled, let total) where enabled == total && total == 1: - // sharing to the one and only connection - guard let account = services.first?.connections.first?.account else { - return String() - } - return String(format: Strings.singleConnectionTextFormat, account) - - case (let enabled, let total) where enabled == total && total > 1: - // sharing to all connections - return String(format: Strings.multipleConnectionsTextFormat, enabled) - - case (let enabled, let total) where enabled < total && total > 1: - // sharing to some connections - return String(format: Strings.partialConnectionsTextFormat, enabled, total) - - default: - return String() - } - } - - var remainingSharesText: String? { - guard let remaining = sharingLimit?.remaining else { - return nil - } - - return String(format: Strings.remainingSharesTextFormat, remaining) - } - - enum Strings { - static let notSharingText = NSLocalizedString( - "prepublishing.social.label.notSharing", - value: "Not sharing to social", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the blog post will not be shared to any social accounts. - """ - ) - - static let singleConnectionTextFormat = NSLocalizedString( - "prepublishing.social.label.singleConnection", - value: "Sharing to %1$@", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the blog post will be shared to a social media account. - %1$@ is a placeholder for the account name. - Example: Sharing to @wordpress - """ - ) - - static let multipleConnectionsTextFormat = NSLocalizedString( - "prepublishing.social.label.multipleConnections", - value: "Sharing to %1$d accounts", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the number of social accounts that will be sharing the blog post. - %1$d is a placeholder for the number of social network accounts that will be auto-shared. - Example: Sharing to 3 accounts - """ - ) - - static let partialConnectionsTextFormat = NSLocalizedString( - "prepublishing.social.label.partialConnections", - value: "Sharing to %1$d of %2$d accounts", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the number of social accounts that will be sharing the blog post. - This string is displayed when some of the social accounts are turned off for auto-sharing. - %1$d is a placeholder for the number of social media accounts that will be sharing the blog post. - %2$d is a placeholder for the total number of social media accounts connected to the user's blog. - Example: Sharing to 2 of 3 accounts - """ - ) - - static let remainingSharesTextFormat = NSLocalizedString( - "prepublishing.social.remainingShares.format", - value: "%1$d social shares remaining", - comment: """ - A subtext that's shown below the primary label in the auto-sharing row on the pre-publishing sheet. - Informs the remaining limit for post auto-sharing. - %1$d is a placeholder for the remaining shares. - Example: 27 social shares remaining - """ - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift b/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift deleted file mode 100644 index f9b60644b184..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift +++ /dev/null @@ -1,115 +0,0 @@ -import UIKit -import SwiftUI -import WordPressUI - -class PrepublishingSocialAccountsTableFooterView: UITableViewHeaderFooterView, Reusable { - - init(remaining: Int, - showsWarning: Bool, - onButtonTap: (() -> Void)?, - reuseIdentifier: String? = PrepublishingSocialAccountsTableFooterView.defaultReuseID) { - super.init(reuseIdentifier: reuseIdentifier) - - let footerView = PrepublishingSocialAccountsFooterView(remaining: remaining, - showsWarning: showsWarning, - onButtonTap: onButtonTap) - let viewToEmbed = UIView.embedSwiftUIView(footerView) - contentView.addSubview(viewToEmbed) - contentView.pinSubviewToAllEdges(viewToEmbed) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SwiftUI View - -struct PrepublishingSocialAccountsFooterView: View { - - @State var remaining: Int - - @State var showsWarning: Bool - - @ScaledMetric(relativeTo: .callout) private var warningIconLength = 16.0 - - var onButtonTap: (() -> Void)? - - var body: some View { - VStack(spacing: 16.0) { - remainingSharesLabel - subscribeButton - } - .padding(EdgeInsets(top: 16.0, leading: 0, bottom: 0, trailing: 0)) - } - - var remainingSharesLabel: some View { - Label { - Text(sharesText) - .font(.callout) - .foregroundColor(Color(showsWarning ? Constants.warningColor : .label)) - } icon: { - if showsWarning { - Image("icon-warning") - .resizable() - .frame(width: warningIconLength, height: warningIconLength) - } - } - .accessibilityLabel(showsWarning ? "\(Constants.warningIconAccessibilityText), \(sharesText)" : sharesText) - } - - var subscribeButton: some View { - Button { - onButtonTap?() - } label: { - Text(Constants.subscribeButtonText) - .padding(12.0) - .frame(maxWidth: .infinity) // needs to be set here to make the button stretch full-width. - } - .buttonStyle(SubscribeButtonStyle()) - } - - private var sharesText: String { - String(format: Constants.remainingSharesLabelTextFormat, remaining) - } - - private struct SubscribeButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(Constants.buttonLabelFont) - .foregroundColor(.white) - .background(Color(configuration.isPressed ? Constants.buttonHighlightedColor : Constants.buttonColor)) - .clipShape(RoundedRectangle(cornerRadius: 8.0)) - } - } - - private enum Constants { - static let buttonLabelFont = Font.title3.weight(.medium) - static let buttonColor = UIAppColor.primary - static let buttonHighlightedColor = UIAppColor.jetpackGreen(.shade70) - static let warningColor = UIAppColor.warning(.shade50) - - static let remainingSharesLabelTextFormat = NSLocalizedString( - "prepublishing.socialAccounts.footer.remainingShares.text", - value: "%1$d social shares remaining in the next 30 days", - comment: """ - Text shown below the list of social accounts to indicate how many social shares available for the site. - Note that the '30 days' part is intended to be a static value. - %1$d is a placeholder for the amount of remaining shares. - Example: 27 social shares remaining in the next 30 days - """ - ) - - static let subscribeButtonText = NSLocalizedString( - "prepublishing.socialAccounts.footer.button.text", - value: "Subscribe to share more", - comment: "The label for a call-to-action button in the social accounts' footer section." - ) - - static let warningIconAccessibilityText = NSLocalizedString( - "prepublishing.socialAccounts.footer.warningIcon.accessibilityHint", - value: "Warning", - comment: "a VoiceOver description for the warning icon to hint that the remaining shares are low." - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift deleted file mode 100644 index 3cc7b3c13ec3..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift +++ /dev/null @@ -1,405 +0,0 @@ -import UIKit -import SwiftUI -import WordPressData -import WordPressShared -import WordPressUI - -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -protocol PrepublishingSocialAccountsDelegate: NSObjectProtocol { - - func didUpdateSharingLimit(with newValue: PublicizeInfo.SharingLimit?) - - func didFinish(with connectionChanges: [Int: Bool], message: String?) -} - -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -class PrepublishingSocialAccountsViewController: UITableViewController { - - // MARK: Properties - - private let coreDataStack: CoreDataStackSwift - - private let service: BlogService - - private weak var delegate: PrepublishingSocialAccountsDelegate? - - private let blogID: Int - - private let connections: [Connection] - - private let originalMessage: String - - private var connectionChanges = [Int: Bool]() - - private var sharingLimit: PublicizeInfo.SharingLimit? { - didSet { - toggleInteractivityIfNeeded() - tableView.reloadData() - delegate?.didUpdateSharingLimit(with: sharingLimit) - } - } - - private var shareMessage: String { - didSet { - tableView.reloadData() - } - } - - /// Stores the interaction state for disabled connections. - /// The value is stored in order to perform table operations *only* when the value changes. - private var canInteractWithDisabledConnections: Bool { - didSet { - guard oldValue != canInteractWithDisabledConnections else { - return - } - // only reload connections that are turned off. - // the last toggled row is skipped so it can perform its full switch animation. - tableView.reloadRows(at: indexPathsForDisabledConnections.filter { $0.row != lastToggledRow }, with: .none) - lastToggledRow = -1 // reset once the reload completes. - } - } - - /// Stores the last table row toggled by the user. - /// - /// This property is only used for visual purposes, to allow the toggled cell's switch animation to complete - /// instead of having it abruptly stopped due to the table view reload. - private var lastToggledRow: Int = -1 - - // MARK: Methods - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - init( - blogID: Int, - model: PostSocialSharingSettings, - delegate: PrepublishingSocialAccountsDelegate?, - coreDataStack: CoreDataStackSwift = ContextManager.shared, - blogService: BlogService? = nil - ) { - self.blogID = blogID - self.connections = model.services.flatMap { service in - service.connections.map { - .init(service: service.name, account: $0.account, keyringID: $0.keyringID, isOn: $0.enabled) - } - } - self.originalMessage = model.message - self.shareMessage = originalMessage - self.sharingLimit = model.sharingLimit - self.delegate = delegate - self.coreDataStack = coreDataStack - self.service = blogService ?? BlogService(coreDataStack: coreDataStack) - self.canInteractWithDisabledConnections = model.enabledConnectionsCount < (sharingLimit?.remaining ?? .max) - - super.init(style: .insetGrouped) - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = Strings.navigationTitle - - tableView.register(SwitchTableViewCell.self, forCellReuseIdentifier: Constants.accountCellIdentifier) - tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.messageCellIdentifier) - - // setting a custom spacer view will override the default 34pt padding from the grouped table view style. - tableView.tableHeaderView = UIView(frame: .init(x: 0, y: 0, width: 0, height: Constants.tableTopPadding)) - } - - deinit { - // only call the delegate method if the user has made some changes. - if hasChanges { - delegate?.didFinish(with: connectionChanges, message: shareMessage) - } - } -} - -// MARK: - UITableView - -extension PrepublishingSocialAccountsViewController { - - override func numberOfSections(in tableView: UITableView) -> Int { - 2 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case 0: connections.count - case 1: 1 - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch indexPath.section { - case 0: - return accountCell(for: indexPath) - case 1: - let cell = tableView.dequeueReusableCell(withIdentifier: Constants.messageCellIdentifier, for: indexPath) - cell.contentConfiguration = UIHostingConfiguration { - SocialSharingMessageRow(text: shareMessage) - } - cell.accessoryType = .disclosureIndicator - return cell - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // interactions for the account switches are absorbed by the tap gestures set up in the SwitchTableViewCell, - // so it shouldn't trigger this method. In any case, we should only care about handling taps on the message row. - switch indexPath.section { - case 0: break // Do nothing - case 1: showEditMessageScreen() - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch section { - case 0: Strings.servicesSectionTitle - case 1: Strings.messageCellLabelText - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - guard let sharingLimit else { - return nil - } - - return PrepublishingSocialAccountsTableFooterView( - remaining: sharingLimit.remaining, - showsWarning: shouldDisplayWarning, - onButtonTap: { [weak self] in - self?.subscribeButtonTapped() - } - ) - } -} - -// MARK: - Private Helpers - -private extension PrepublishingSocialAccountsViewController { - var enabledCount: Int { - connections - .filter { connectionChanges[$0.keyringID] ?? $0.isOn } - .count - } - - var indexPathsForDisabledConnections: [IndexPath] { - connections.indices.compactMap { index in - valueForConnection(at: index) ? nil : IndexPath(row: index, section: .zero) - } - } - - var shouldDisplayWarning: Bool { - connections.count >= (sharingLimit?.remaining ?? .max) - } - - var hasChanges: Bool { - !connectionChanges.isEmpty || shareMessage != originalMessage - } - - func accountCell(for indexPath: IndexPath) -> UITableViewCell { - guard var connection = connections[safe: indexPath.row], - let cell = tableView.dequeueReusableCell(withIdentifier: Constants.accountCellIdentifier) - as? SwitchTableViewCell - else { - return UITableViewCell() - } - - cell.textLabel?.text = connection.account - cell.textLabel?.numberOfLines = 1 - cell.textLabel?.adjustsFontForContentSizeCategory = true - cell.imageView?.image = connection.imageForCell - cell.on = valueForConnection(at: indexPath.row) - cell.onChange = { [weak self] newValue in - self?.updateConnection(at: indexPath.row, value: newValue) - } - - let isInteractionAllowed = cell.on || canInteractWithDisabledConnections - isInteractionAllowed ? cell.enable() : cell.disable() - cell.imageView?.alpha = isInteractionAllowed ? 1.0 : Constants.disabledCellImageOpacity - - cell.accessibilityLabel = "\(connection.service.description), \(connection.account)" - - return cell - } - - func valueForConnection(at index: Int) -> Bool { - guard let connection = connections[safe: index] else { - return false - } - return connectionChanges[connection.keyringID] ?? connection.isOn - } - - func updateConnection(at index: Int, value: Bool) { - guard let connection = connections[safe: index] else { - return - } - - let originalValue = connection.isOn - - if value == originalValue { - connectionChanges.removeValue(forKey: connection.keyringID) - } else { - connectionChanges[connection.keyringID] = value - } - - lastToggledRow = index - toggleInteractivityIfNeeded() - - WPAnalytics.track( - .jetpackSocialConnectionToggled, - properties: ["source": Constants.trackingSource, "value": value] - ) - } - - func toggleInteractivityIfNeeded() { - canInteractWithDisabledConnections = enabledCount < (sharingLimit?.remaining ?? .max) - } - - func showEditMessageScreen() { - let multiTextViewController = SettingsMultiTextViewController( - text: shareMessage, - placeholder: nil, - hint: Strings.editShareMessageHint, - isPassword: false - ) - - multiTextViewController.title = Strings.editShareMessageNavigationTitle - multiTextViewController.onValueChanged = { [weak self] newValue in - self?.shareMessage = newValue - } - - self.navigationController?.pushViewController(multiTextViewController, animated: true) - } - - func subscribeButtonTapped() { - guard let checkoutViewController = makeCheckoutViewController() else { - return - } - - WPAnalytics.track(.jetpackSocialUpgradeLinkTapped, properties: ["source": Constants.trackingSource]) - - let navigationController = UINavigationController(rootViewController: checkoutViewController) - show(navigationController, sender: nil) - } - - func makeCheckoutViewController() -> UIViewController? { - coreDataStack.performQuery { [weak self] context in - guard let self, - let blog = try? Blog.lookup(withID: self.blogID, in: context), - let host = blog.hostname, - let url = URL(string: "https://wordpress.com/checkout/\(host)/jetpack_social_basic_yearly") - else { - return nil - } - - return WebViewControllerFactory.controller(url: url, blog: blog, source: Constants.webViewSource) { - self.checkoutDismissed() - } - } - } - - /// When the checkout web view is dismissed, try to sync the latest sharing limit in case the user did make - /// a purchase. We can make this assumption if the returned `sharingLimit` is nil, which means there's no longer - /// any sharing limit for the site. - func checkoutDismissed() { - assert(Thread.isMainThread, "\(#function) must be called from the main thread") - - guard let blog = try? Blog.lookup(withID: blogID, in: coreDataStack.mainContext), - ReachabilityUtils.isInternetReachable() - else { - return - } - - service.syncBlog(blog) { [weak self] in - guard let self else { - return - } - - // re-fetch the blog after sync completes to check if the sharing limit for the blog has been removed. - self.sharingLimit = self.coreDataStack.performQuery { context in - guard let blog = try? Blog.lookup(withID: self.blogID, in: context) else { - return nil - } - return blog.sharingLimit - } - } failure: { error in - DDLogError("Failed to sync blog after dismissing checkout webview due to error: \(error)") - } - } - - /// Convenient model that represents the user's Publicize connections. - struct Connection { - let service: PublicizeService.ServiceName - let account: String - let keyringID: Int - let isOn: Bool - - lazy var imageForCell: UIImage = { - service.localIconImage.resized(to: Constants.cellImageSize, format: .scaleAspectFit) - }() - } - - // MARK: Constants - - enum Constants { - static let disabledCellImageOpacity = 0.36 - static let cellImageSize = CGSize(width: 28.0, height: 28.0) - - static let tableTopPadding: CGFloat = 16.0 - - static let accountCellIdentifier = "AccountCell" - static let messageCellIdentifier = "MessageCell" - - static let webViewSource = "prepublishing_social_accounts_subscribe" - static let trackingSource = "pre_publishing" - } -} - -private enum Strings { - static let navigationTitle = NSLocalizedString( - "prepublishing.socialAccounts.navigationTitle", - value: "Social", - comment: "The navigation title for the pre-publishing social accounts screen." - ) - - static let servicesSectionTitle = NSLocalizedString( - "prepublishing.socialAccounts.servicesSectionTitle", - value: "Services", - comment: "Table section title" - ) - - static let messageCellLabelText = NSLocalizedString( - "prepublishing.socialAccounts.message.label", - value: "Message", - comment: "Table section title" - ) - - static let editShareMessageNavigationTitle = NSLocalizedString( - "prepublishing.socialAccounts.editMessage.navigationTitle", - value: "Customize message", - comment: "The navigation title for a screen that edits the sharing message for the post." - ) - - static let editShareMessageHint = NSLocalizedString( - "prepublishing.socialAccounts.editMessage.hint", - value: """ - Customize the message you want to share. - If you don't add your own text here, we'll use the post's title as the message. - """, - comment: "A hint shown below the text field when editing the sharing message from the pre-publishing flow." - ) -} - -private extension PostSocialSharingSettings { - var enabledConnectionsCount: Int { - services.flatMap { $0.connections }.filter { $0.enabled }.count - } -} diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift index eda76d5720cd..9799c0d556bb 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift @@ -1,4 +1,5 @@ import UIKit +import JetpackSocial import WordPressData import WordPressKit import WordPressFlux @@ -8,7 +9,7 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { weak var bannerView: JetpackBannerView? var isGrowAudienceShowing: Bool { - return insightsToShow.contains(.growAudience) + insightsToShow.contains(.growAudience) } private var insightsChangeReceipt: Receipt? @@ -41,7 +42,7 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { private var displayingEmptyView = false private lazy var mainContext: NSManagedObjectContext = { - return ContextManager.shared.mainContext + ContextManager.shared.mainContext }() private var viewModel: SiteStatsInsightsViewModel? @@ -49,7 +50,7 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { private let analyticsTracker = BottomScrollAnalyticsTracker() private lazy var tableHandler: ImmuTableDiffableViewHandler = { - return ImmuTableDiffableViewHandler(takeOver: self, with: analyticsTracker) + ImmuTableDiffableViewHandler(takeOver: self, with: analyticsTracker) }() // MARK: - View @@ -109,8 +110,11 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { dismissCustomizeCard() } - let controller = InsightsManagementViewController(insightsDelegate: self, - insightsManagementDelegate: self, insightsShown: insightsToShow.compactMap { $0.statSection }) + let controller = InsightsManagementViewController( + insightsDelegate: self, + insightsManagementDelegate: self, + insightsShown: insightsToShow.compactMap { $0.statSection } + ) let navigationController = UINavigationController(rootViewController: controller) navigationController.modalPresentationStyle = .formSheet // Has to be set before the delegate navigationController.presentationController?.delegate = self @@ -123,11 +127,13 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { private extension SiteStatsInsightsTableViewController { func initViewModel() { - viewModel = SiteStatsInsightsViewModel(insightsToShow: insightsToShow, - insightsDelegate: self, - viewsAndVisitorsDelegate: self, - insightsStore: insightsStore, - pinnedItemStore: pinnedItemStore) + viewModel = SiteStatsInsightsViewModel( + insightsToShow: insightsToShow, + insightsDelegate: self, + viewsAndVisitorsDelegate: self, + insightsStore: insightsStore, + pinnedItemStore: pinnedItemStore + ) } func addViewModelListeners() { @@ -135,11 +141,12 @@ private extension SiteStatsInsightsTableViewController { return } - insightsChangeReceipt = viewModel?.onChange { [weak self] in - self?.refreshGrowAudienceCardIfNecessary() - self?.displayEmptyViewIfNecessary() - self?.refreshTableView() - } + insightsChangeReceipt = viewModel? + .onChange { [weak self] in + self?.refreshGrowAudienceCardIfNecessary() + self?.displayEmptyViewIfNecessary() + self?.refreshTableView() + } } func removeViewModelListeners() { @@ -147,25 +154,27 @@ private extension SiteStatsInsightsTableViewController { } func tableRowTypes() -> [ImmuTableRow.Type] { - return [ViewsVisitorsRow.self, - GrowAudienceRow.self, - CustomizeInsightsRow.self, - LatestPostSummaryRow.self, - TwoColumnStatsRow.self, - PostingActivityRow.self, - TabbedTotalsStatsRow.self, - TopTotalsInsightStatsRow.self, - MostPopularTimeInsightStatsRow.self, - TotalInsightStatsRow.self, - AddInsightRow.self, - TableFooterRow.self, - StatsErrorRow.self, - StatsGhostGrowAudienceImmutableRow.self, - StatsGhostChartImmutableRow.self, - StatsGhostTwoColumnImmutableRow.self, - StatsGhostTopImmutableRow.self, - StatsGhostTabbedImmutableRow.self, - StatsGhostPostingActivitiesImmutableRow.self] + [ + ViewsVisitorsRow.self, + GrowAudienceRow.self, + CustomizeInsightsRow.self, + LatestPostSummaryRow.self, + TwoColumnStatsRow.self, + PostingActivityRow.self, + TabbedTotalsStatsRow.self, + TopTotalsInsightStatsRow.self, + MostPopularTimeInsightStatsRow.self, + TotalInsightStatsRow.self, + AddInsightRow.self, + TableFooterRow.self, + StatsErrorRow.self, + StatsGhostGrowAudienceImmutableRow.self, + StatsGhostChartImmutableRow.self, + StatsGhostTwoColumnImmutableRow.self, + StatsGhostTopImmutableRow.self, + StatsGhostTabbedImmutableRow.self, + StatsGhostPostingActivitiesImmutableRow.self + ] } // MARK: - Table Refreshing @@ -263,9 +272,10 @@ private extension SiteStatsInsightsTableViewController { func refreshGrowAudienceCardIfNecessary() { guard let count = insightsStore.getAllTimeStats()?.viewsCount, - count != self.currentViewCount else { - return - } + count != self.currentViewCount + else { + return + } currentViewCount = count loadPinnedCards() @@ -315,8 +325,9 @@ private extension SiteStatsInsightsTableViewController { let minIndex = isShowingPinnedCard ? 1 : 0 guard let currentIndex = indexOfInsight(insight), - (currentIndex - 1) >= minIndex else { - return false + (currentIndex - 1) >= minIndex + else { + return false } return true @@ -324,15 +335,16 @@ private extension SiteStatsInsightsTableViewController { func canMoveInsightDown(_ insight: InsightType) -> Bool { guard let currentIndex = indexOfInsight(insight), - (currentIndex + 1) < insightsToShow.endIndex else { - return false + (currentIndex + 1) < insightsToShow.endIndex + else { + return false } return true } func indexOfInsight(_ insight: InsightType) -> Int? { - return insightsToShow.firstIndex(of: insight) + insightsToShow.firstIndex(of: insight) } enum ManageInsightConstants { @@ -356,7 +368,10 @@ private extension SiteStatsInsightsTableViewController { extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func displayWebViewWithURL(_ url: URL) { - let webViewController = WebViewControllerFactory.controllerAuthenticatedWithDefaultAccount(url: url, source: "site_stats_insights") + let webViewController = WebViewControllerFactory.controllerAuthenticatedWithDefaultAccount( + url: url, + source: "site_stats_insights" + ) let navController = UINavigationController(rootViewController: webViewController) present(navController, animated: true) } @@ -368,7 +383,9 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { } func showShareForPost(postID: NSNumber, fromView: UIView) { - guard let blogId = SiteStatsInformation.sharedInstance.siteID, let blog = Blog.lookup(withID: blogId, in: mainContext) else { + guard let blogId = SiteStatsInformation.sharedInstance.siteID, + let blog = Blog.lookup(withID: blogId, in: mainContext) + else { DDLogInfo("Failed to get blog with id \(String(describing: SiteStatsInformation.sharedInstance.siteID))") return } @@ -396,7 +413,7 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func showPostingActivityDetails() { let yearData = insightsStore.getYearlyPostingActivity(from: Date()) let postingActivityViewController = PostingActivityViewController.loadFromStoryboard { coder in - return PostingActivityViewController(coder: coder, yearData: yearData) + PostingActivityViewController(coder: coder, yearData: yearData) } navigationController?.pushViewController(postingActivityViewController, animated: true) } @@ -422,8 +439,12 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { // When displaying Annual details, start from the most recent year available. var selectedDate: Date? if statSection == .insightsAnnualSiteStats, - let year = viewModel?.annualInsightsYear() { - var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: StatsDataHelper.currentDateForSite()) + let year = viewModel?.annualInsightsYear() + { + var dateComponents = Calendar.current.dateComponents( + [.year, .month, .day], + from: StatsDataHelper.currentDateForSite() + ) dateComponents.year = Int(year) selectedDate = Calendar.current.date(from: dateComponents) } @@ -440,7 +461,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func segueToInsightsDetails(statSection: StatSection, selectedDate: Date?, selectedPeriod: StatsPeriodUnit?) { let detailTableViewController = SiteStatsInsightsDetailsTableViewController() - detailTableViewController.configure(statSection: statSection, selectedDate: selectedDate, selectedPeriod: selectedPeriod) + detailTableViewController.configure( + statSection: statSection, + selectedDate: selectedDate, + selectedPeriod: selectedPeriod + ) navigationController?.pushViewController(detailTableViewController, animated: true) } @@ -453,9 +478,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func showPostStats(postID: Int, postTitle: String?, postURL: URL?) { removeViewModelListeners() - let postStatsTableViewController = PostStatsTableViewController.withJPBannerForBlog(postID: postID, - postTitle: postTitle, - postURL: postURL) + let postStatsTableViewController = PostStatsTableViewController.withJPBannerForBlog( + postID: postID, + postTitle: postTitle, + postURL: postURL + ) navigationController?.pushViewController(postStatsTableViewController, animated: true) } @@ -475,16 +502,17 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func growAudienceEnablePostSharingButtonTapped() { guard let blogId = SiteStatsInformation.sharedInstance.siteID, - let blog = Blog.lookup(withID: blogId, in: mainContext) else { + let blog = Blog.lookup(withID: blogId, in: mainContext) + else { DDLogInfo("Failed to get blog with id \(String(describing: SiteStatsInformation.sharedInstance.siteID))") return } - guard let sharingVC = SharingViewController(blog: blog, delegate: self) else { - return + guard let manageVC = ManageConnectionsHostingController.make(for: blog) else { + return wpAssertionFailure("social connections service unavailable") } - let navigationController = UINavigationController(rootViewController: sharingVC) + let navigationController = UINavigationController(rootViewController: manageVC) present(navigationController, animated: true) applyTableUpdates() @@ -494,15 +522,18 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func growAudienceBloggingRemindersButtonTapped() { guard let blogId = SiteStatsInformation.sharedInstance.siteID, - let blog = Blog.lookup(withID: blogId, in: mainContext) else { + let blog = Blog.lookup(withID: blogId, in: mainContext) + else { DDLogInfo("Failed to get blog with id \(String(describing: SiteStatsInformation.sharedInstance.siteID))") return } - BloggingRemindersFlow.present(from: self, - for: blog, - source: .statsInsights, - delegate: self) + BloggingRemindersFlow.present( + from: self, + for: blog, + source: .statsInsights, + delegate: self + ) applyTableUpdates() @@ -526,13 +557,22 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { self.navigationController?.popToRootViewController(animated: false) RootViewCoordinator.sharedPresenter.showReader(path: .discover) - Notice(title: NSLocalizedString("Comment to start making connections.", comment: "Hint for users to grow their audience by commenting on other blogs.")).post() + Notice( + title: NSLocalizedString( + "Comment to start making connections.", + comment: "Hint for users to grow their audience by commenting on other blogs." + ) + ) + .post() } let nc = UINavigationController(rootViewController: vc) nc.modalPresentationStyle = .formSheet present(nc, animated: true) { [weak self] in - let text = NSLocalizedString("Follow topics you're interested in and we'll find some blogs you might like.", comment: "Guide for users to follow topics.") + let text = NSLocalizedString( + "Follow topics you're interested in and we'll find some blogs you might like.", + comment: "Guide for users to follow topics." + ) self?.displayNotice(title: text) } @@ -545,8 +585,9 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func addInsightSelected(_ insight: StatSection) { guard let insightType = insight.insightType, - !insightsToShow.contains(insightType) else { - return + !insightsToShow.contains(insightType) + else { + return } WPAnalytics.track(.statsItemSelectedAddInsight, withProperties: ["insight": insight.title]) @@ -573,7 +614,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { // newly added card will be penultimate row, above the 'Add Stats Card' row let newCardRow = max(self.tableView.numberOfRows(inSection: lastSection) - 2, 0) - self.tableView.scrollToRow(at: IndexPath(row: newCardRow, section: lastSection), at: .middle, animated: true) + self.tableView.scrollToRow( + at: IndexPath(row: newCardRow, section: lastSection), + at: .middle, + animated: true + ) } } @@ -586,9 +631,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { WPAnalytics.track(.statsItemTappedManageInsight) - let alert = UIAlertController(title: insight.title, - message: nil, - preferredStyle: .actionSheet) + let alert = UIAlertController( + title: insight.title, + message: nil, + preferredStyle: .actionSheet + ) if canMoveInsightUp(insightType) { alert.addDefaultActionWithTitle(ManageInsightConstants.moveUp) { [weak self] _ in @@ -645,7 +692,8 @@ extension SiteStatsInsightsTableViewController: StatsInsightsViewsAndVisitorsDel extension SiteStatsInsightsTableViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { guard let navigationController = presentationController.presentedViewController as? UINavigationController, - let controller = navigationController.topViewController as? InsightsManagementViewController else { + let controller = navigationController.topViewController as? InsightsManagementViewController + else { return } @@ -653,15 +701,6 @@ extension SiteStatsInsightsTableViewController: UIAdaptivePresentationController } } -// MARK: - SharingViewControllerDelegate - -extension SiteStatsInsightsTableViewController: SharingViewControllerDelegate { - func didChangePublicizeServices() { - markCurrentNudgeAsCompleted() - trackNudgeEvent(.statsPublicizeNudgeCompleted) - } -} - // MARK: - BloggingRemindersFlowDelegate extension SiteStatsInsightsTableViewController: BloggingRemindersFlowDelegate { @@ -695,15 +734,18 @@ extension SiteStatsInsightsTableViewController: NoResultsViewHost { return } - configureAndDisplayNoResults(on: tableView, - title: NoResultConstants.errorTitle, - subtitle: NoResultConstants.errorSubtitle, - buttonTitle: NoResultConstants.refreshButtonTitle, customizationBlock: { [weak self] noResults in - noResults.delegate = self - if !noResults.isReachable { - noResults.resetButtonText() - } - }) + configureAndDisplayNoResults( + on: tableView, + title: NoResultConstants.errorTitle, + subtitle: NoResultConstants.errorSubtitle, + buttonTitle: NoResultConstants.refreshButtonTitle, + customizationBlock: { [weak self] noResults in + noResults.delegate = self + if !noResults.isReachable { + noResults.resetButtonText() + } + } + ) } private func displayEmptyViewIfNecessary() { @@ -714,22 +756,42 @@ extension SiteStatsInsightsTableViewController: NoResultsViewHost { } displayingEmptyView = true - configureAndDisplayNoResults(on: tableView, - title: NoResultConstants.noInsightsTitle, - subtitle: NoResultConstants.noInsightsSubtitle, - buttonTitle: NoResultConstants.manageInsightsButtonTitle, - image: "wp-illustration-stats-outline") { [weak self] noResults in - noResults.delegate = self + configureAndDisplayNoResults( + on: tableView, + title: NoResultConstants.noInsightsTitle, + subtitle: NoResultConstants.noInsightsSubtitle, + buttonTitle: NoResultConstants.manageInsightsButtonTitle, + image: "wp-illustration-stats-outline" + ) { [weak self] noResults in + noResults.delegate = self } } private enum NoResultConstants { - static let errorTitle = NSLocalizedString("Stats not loaded", comment: "The loading view title displayed when an error occurred") - static let errorSubtitle = NSLocalizedString("There was a problem loading your data, refresh your page to try again.", comment: "The loading view subtitle displayed when an error occurred") - static let refreshButtonTitle = NSLocalizedString("Refresh", comment: "The loading view button title displayed when an error occurred") - static let noInsightsTitle = NSLocalizedString("No insights added yet", comment: "Title displayed when the user has removed all Insights from display.") - static let noInsightsSubtitle = NSLocalizedString("Only see the most relevant stats. Add insights to fit your needs.", comment: "Subtitle displayed when the user has removed all Insights from display.") - static let manageInsightsButtonTitle = NSLocalizedString("Add stats card", comment: "Button title displayed when the user has removed all Insights from display.") + static let errorTitle = NSLocalizedString( + "Stats not loaded", + comment: "The loading view title displayed when an error occurred" + ) + static let errorSubtitle = NSLocalizedString( + "There was a problem loading your data, refresh your page to try again.", + comment: "The loading view subtitle displayed when an error occurred" + ) + static let refreshButtonTitle = NSLocalizedString( + "Refresh", + comment: "The loading view button title displayed when an error occurred" + ) + static let noInsightsTitle = NSLocalizedString( + "No insights added yet", + comment: "Title displayed when the user has removed all Insights from display." + ) + static let noInsightsSubtitle = NSLocalizedString( + "Only see the most relevant stats. Add insights to fit your needs.", + comment: "Subtitle displayed when the user has removed all Insights from display." + ) + static let manageInsightsButtonTitle = NSLocalizedString( + "Add stats card", + comment: "Button title displayed when the user has removed all Insights from display." + ) } } @@ -739,7 +801,8 @@ private extension SiteStatsInsightsTableViewController { func trackNudgeEvent(_ event: WPAnalyticsEvent) { if let blogId = SiteStatsInformation.sharedInstance.siteID, - let blog = Blog.lookup(withID: blogId, in: mainContext) { + let blog = Blog.lookup(withID: blogId, in: mainContext) + { WPAnalytics.track(event, properties: [:], blog: blog) } else { WPAnalytics.track(event) diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ab53a122a86a..6d6a9b5c0415 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -835,7 +835,6 @@ Utility/Sharing/WPActivityDefaults.h, Utility/UIAlertControllerProxy.h, Utility/WPError.h, - ViewRelated/Blog/Sharing/SharingViewController.h, "ViewRelated/Blog/Site Settings/SettingTableViewCell.h", "ViewRelated/Blog/Site Settings/SiteSettingsViewController.h", ViewRelated/Comments/Controllers/CommentsViewController.h,