diff --git a/EditorConfig/Templates/install.sh b/EditorConfig/Templates/install.sh index 7a18f868..01e82419 100755 --- a/EditorConfig/Templates/install.sh +++ b/EditorConfig/Templates/install.sh @@ -1,2 +1,4 @@ #! /bin/bash -cp -R EmbeddedSocial\ Swift\ File.xctemplate ~/Library/Developer/Xcode/Templates/Custom/ +destination=$HOME"/Library/Developer/Xcode/Templates/Custom/" +mkdir -p $destination +cp -R EmbeddedSocial\ Swift\ File.xctemplate $destination diff --git a/EmbeddedSocial.xcodeproj/project.pbxproj b/EmbeddedSocial.xcodeproj/project.pbxproj index c1351611..dc45e9a6 100644 --- a/EmbeddedSocial.xcodeproj/project.pbxproj +++ b/EmbeddedSocial.xcodeproj/project.pbxproj @@ -786,6 +786,7 @@ EB45E0D87E3DCF49B5A22162 /* CommentRepliesInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC43B539B5A91FF6D4EAB9B /* CommentRepliesInitializer.swift */; }; EC75A126A73CC5F294926613 /* CommentRepliesConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C6D69159F11B9D4D3F7000 /* CommentRepliesConfigurator.swift */; }; ED9BC53643CA7A3E7A3106DC /* CommentCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F824A8069868818F477BD /* CommentCellConfigurator.swift */; }; + F8ED92AE1F72DF3E0026AC3E /* ActivityPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ED92AD1F72DF3E0026AC3E /* ActivityPresenterTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1631,6 +1632,7 @@ E63643937A3B405C1B4E0B49 /* CommentCellInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentCellInteractorInput.swift; sourceTree = ""; }; EC1ECDB428A92745248FE72C /* CommentCellRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentCellRouterInput.swift; sourceTree = ""; }; F35AC4A39014F4C18D39AA62 /* ActivityRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActivityRouter.swift; sourceTree = ""; }; + F8ED92AD1F72DF3E0026AC3E /* ActivityPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityPresenterTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -4338,6 +4340,7 @@ children = ( 9CEE0EDD1F6FE3F7008B1104 /* ActivityInteractorTests.swift */, 9CCE405C1F72827A003A51D9 /* ActivityTests.swift */, + F8ED92AD1F72DF3E0026AC3E /* ActivityPresenterTests.swift */, ); path = Activity; sourceTree = ""; @@ -5765,6 +5768,7 @@ 881F7E621F263888003EB37A /* MockKeyValueStorage.swift in Sources */, 88B94ED11F62C3FA002392F9 /* MockSearchTopicsInteractor.swift in Sources */, 9C985E531F505BDF00514F85 /* FeedCacheTests.swift in Sources */, + F8ED92AE1F72DF3E0026AC3E /* ActivityPresenterTests.swift in Sources */, 884C9C341F4C3E940004907F /* UserListPresenterTests.swift in Sources */, 932898D41F65929F001F3BC2 /* ReportReplyAPITests.swift in Sources */, 9C30243C1F55AB2100675FE9 /* FeedModuleRouterMock.swift in Sources */, diff --git a/EmbeddedSocial/Sources/Common/SectionModel.swift b/EmbeddedSocial/Sources/Common/SectionModel.swift index d20e8bca..4a507440 100644 --- a/EmbeddedSocial/Sources/Common/SectionModel.swift +++ b/EmbeddedSocial/Sources/Common/SectionModel.swift @@ -8,19 +8,23 @@ import Foundation protocol SectionModelType { associatedtype Item - var items: [Item] { get } + var items: [Item] { get set } } struct SectionModel: SectionModelType { typealias Item = ItemType - let model: Section - let items: [Item] + var model: Section + var items: [Item] init(model: Section, items: [Item]) { self.model = model self.items = items } + + mutating func erase() { + items = [] + } } extension SectionModel: CustomStringConvertible { diff --git a/EmbeddedSocial/Sources/Modules/Activity/Entities/ActivityEntities.swift b/EmbeddedSocial/Sources/Modules/Activity/Entities/ActivityEntities.swift index 85ec182c..7a9abbbf 100644 --- a/EmbeddedSocial/Sources/Modules/Activity/Entities/ActivityEntities.swift +++ b/EmbeddedSocial/Sources/Modules/Activity/Entities/ActivityEntities.swift @@ -91,6 +91,12 @@ struct PendingRequestItem { var userHandle: String } +extension PendingRequestItem { + static func mock(seed: Int) -> PendingRequestItem { + return PendingRequestItem(userName: "username \(seed)", userHandle: "handle \(seed)") + } +} + // Helpers enum Change { @@ -166,7 +172,7 @@ class ActivityViewModelBuilder { class SectionsConfigurator { - func build(section: ActivityPresenter.State) -> [ActivityPresenter.Section] { + func build(section: ActivityPresenter.State) -> [Section] { switch section { case .my: @@ -174,21 +180,22 @@ class SectionsConfigurator { case .others: return others } + } - private var my: [ActivityPresenter.Section] { + private var my: [Section] { let sectionHeader = SectionHeader(name: "Section 1", identifier: "") let model = PendingRequestItem(userName: "User", userHandle: "User handle") let item = ActivityItem.pendingRequest(model) - let section = ActivityPresenter.Section(model: sectionHeader, items: [item]) + let section = Section(model: sectionHeader, items: [item]) return [section] } - private var others: [ActivityPresenter.Section] { + private var others: [Section] { let sectionHeader = SectionHeader(name: "Section 2", identifier: "") let model = ActionItem.mock(seed: 0) let item = ActivityItem.follower(model) - let section = ActivityPresenter.Section(model: sectionHeader, items: [item]) + let section = Section(model: sectionHeader, items: [item]) return [section] } diff --git a/EmbeddedSocial/Sources/Modules/Activity/Interactor/ActivityInteractor.swift b/EmbeddedSocial/Sources/Modules/Activity/Interactor/ActivityInteractor.swift index a3e239b6..866325a8 100644 --- a/EmbeddedSocial/Sources/Modules/Activity/Interactor/ActivityInteractor.swift +++ b/EmbeddedSocial/Sources/Modules/Activity/Interactor/ActivityInteractor.swift @@ -11,6 +11,10 @@ protocol ActivityInteractorOutput: class { protocol ActivityInteractorInput { + func loadAll() + func loadNextPageFollowingActivities(completion: ((Result<[ActionItem]>) -> Void)?) + func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)?) + } protocol ActivityService: class { @@ -190,37 +194,39 @@ extension ActivityInteractor: ActivityInteractorInput { let pageID = UUID().uuidString loadingPages.insert(pageID) - service.loadFollowingActivities(cursor: followersList.cursor, - limit: followersList.limit) { [weak self] (result: Result) in - - defer { - loadingPages.remove(pageID) - } - - // exit on released or canceled - guard let strongSelf = self, strongSelf.loadingPages.contains(pageID) else { - return - } - - // must have data - guard let response = result.value else { - completion?(.failure(ActivityError.noData)) - return - } - - // map data into page - guard let page = strongSelf.process(response: response, pageID: pageID) else { - completion?(.failure(ActivityError.notParsable)) - return - } - - strongSelf.followersList.add(page: page) - - completion?(.success(page.items)) + service.loadFollowingActivities( + cursor: followersList.cursor, + limit: followersList.limit) { [weak self] (result: Result) in + + defer { + loadingPages.remove(pageID) + } + + // exit on released or canceled + guard let strongSelf = self, strongSelf.loadingPages.contains(pageID) else { + return + } + + // must have data + guard let response = result.value else { + completion?(.failure(ActivityError.noData)) + return + } + + // map data into page + guard let page = strongSelf.process(response: response, pageID: pageID) else { + completion?(.failure(ActivityError.notParsable)) + return + } + + strongSelf.followersList.add(page: page) + + completion?(.success(page.items)) } } + // TODO: remake using generics func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)? = nil) { let pageID = UUID().uuidString diff --git a/EmbeddedSocial/Sources/Modules/Activity/Presenter/ActivityPresenter.swift b/EmbeddedSocial/Sources/Modules/Activity/Presenter/ActivityPresenter.swift index a5ef5cbd..7fa8b37c 100644 --- a/EmbeddedSocial/Sources/Modules/Activity/Presenter/ActivityPresenter.swift +++ b/EmbeddedSocial/Sources/Modules/Activity/Presenter/ActivityPresenter.swift @@ -7,14 +7,14 @@ protocol ActivityModuleInput: class { } +typealias Section = SectionModel + class ActivityPresenter { - typealias Section = SectionModel - weak var view: ActivityViewInput! var interactor: ActivityInteractorInput! var router: ActivityRouterInput! - + enum State: Int { case my case others @@ -44,9 +44,9 @@ class ActivityPresenter { sections[.my] = sectionsConfigurator.build(section: .my) sections[.others] = sectionsConfigurator.build(section: .others) } - + fileprivate func loadNextPage() { - + } } @@ -59,13 +59,98 @@ extension ActivityPresenter: ActivityInteractorOutput { } + + +protocol DataSourceProtocol { + func loadMore() + var section: Section { get } +} + +class DataSource: DataSourceProtocol { + var interactor: ActivityInteractorInput + var section: Section + var errorHandler: ((Error) -> Void)? + + func loadMore() { } + + init(interactor: ActivityInteractorInput, section: Section, errorHandler: ((Error) -> Void)? = nil) { + self.interactor = interactor + self.section = section + self.errorHandler = errorHandler + } +} + + +class MyPendingRequests: DataSource { + + override func loadMore() { + + // load pendings + interactor.loadNextPagePendigRequestItems { [weak self] (result) in + switch result { + case let .failure(error): + self?.errorHandler?(error) + case let .success(models): + let items = models.map { ActivityItem.pendingRequest($0) } + self?.section.items.append(contentsOf: items) + } + } + } + +} + +class MyFollowersActivity: DataSource { + + override func loadMore() { + // load activity + interactor.loadNextPageFollowingActivities { [weak self] (result) in + switch result { + case let .failure(error): + self?.errorHandler?(error) + case let .success(models): + let items = models.map { ActivityItem.follower($0) } + self?.section.items.append(contentsOf: items) + } + } + } +} + +class MyFollowingsActivity: DataSource { + + override func loadMore() { + + } +} + + extension ActivityPresenter: ActivityViewOutput { func load() { - + interactor.loadAll() } - func loadMore(for section: Int) { + func loadMore() { + + interactor.loadNextPageFollowingActivities { (result) in + + switch result { + case let .success(items): + + // sections[state] + break + + case let .failure(error): + + break + + } + + + } + + interactor.loadNextPageFollowingActivities { (result) in + + } } @@ -86,7 +171,7 @@ extension ActivityPresenter: ActivityViewOutput { } func numberOfSections() -> Int { - return sections.count + return sections[state]!.count } func numberOfItems(in section: Int) -> Int { diff --git a/EmbeddedSocial/Sources/Modules/Activity/View/ActivityViewController.swift b/EmbeddedSocial/Sources/Modules/Activity/View/ActivityViewController.swift index 44ff0d94..704a4c07 100644 --- a/EmbeddedSocial/Sources/Modules/Activity/View/ActivityViewController.swift +++ b/EmbeddedSocial/Sources/Modules/Activity/View/ActivityViewController.swift @@ -7,7 +7,8 @@ import UIKit protocol ActivityViewInput: class { func setupInitialState() - func registerCell(cell: AnyObject.Type, id: String) + func registerCell(cell: UITableViewCell.Type, id: String) + func showError(_ error: Error) } protocol ActivityViewOutput: class { @@ -44,11 +45,15 @@ class ActivityViewController: UIViewController { extension ActivityViewController: ActivityViewInput { + func showError(_ error: Error) { + Logger.log(error, event: .veryImportant) + } + func setupInitialState() { } - func registerCell(cell: AnyObject.Type, id: String) { + func registerCell(cell: UITableViewCell.Type, id: String) { } diff --git a/EmbeddedSocialTests/Modules/Activity/ActivityPresenterTests.swift b/EmbeddedSocialTests/Modules/Activity/ActivityPresenterTests.swift new file mode 100644 index 00000000..39db5a3e --- /dev/null +++ b/EmbeddedSocialTests/Modules/Activity/ActivityPresenterTests.swift @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// + +import XCTest +@testable import EmbeddedSocial + +class ActivityInteractorMock: ActivityInteractorInput { + + var followingActivityItemsResult: Result<[ActionItem]>! + var pendingRequestsItemsResult: Result<[PendingRequestItem]>! + + func loadAll() { + + } + + func loadNextPageFollowingActivities(completion: ((Result<[ActionItem]>) -> Void)? = nil) { + completion?(followingActivityItemsResult) + } + + func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)? = nil) { + completion?(pendingRequestsItemsResult) + } + +} + +class ActivityPresenterTests: XCTestCase { + + var sut: ActivityPresenter! + var interactor : ActivityInteractorMock! + + + override func setUp() { + super.setUp() + + sut = ActivityPresenter() + sut.interactor = ActivityInteractorMock() + + } + + override func tearDown() { + super.tearDown() + } + + func test() { + + // given + + let mockItems = Array(1..<5).map { PendingRequestItem.mock(seed: $0) } + let result: Result<[PendingRequestItem]> = .success(mockItems) + + let header = SectionHeader(name: "", identifier: "") + let section = Section(model: header, items: []) + let pendingRequestsDataSource = MyPendingRequests(interactor: interactor, + section: section) + + interactor.pendingRequestsItemsResult = result + + pendingRequestsDataSource.loadMore() + + pendingRequestsDataSource.section.items.count + + + + } + +} + diff --git a/EmbeddedSocialTests/Modules/Activity/ActivityTests.swift b/EmbeddedSocialTests/Modules/Activity/ActivityTests.swift index 9a933167..082d6cb3 100644 --- a/EmbeddedSocialTests/Modules/Activity/ActivityTests.swift +++ b/EmbeddedSocialTests/Modules/Activity/ActivityTests.swift @@ -87,6 +87,25 @@ class ActivityEntitiesTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } + func testThatPaggingWorksCorrectly() { + + // given + let service = ActivityServiceMock() + let followingActivitiesResponse = buildActivitiResponseMock() + service.followingActivitiesResponse = .success(followingActivitiesResponse) + + let interactor = ActivityInteractor() + interactor.service = service + + let pendingRequests = expectation(description: #file) + + // when + +// interactor. + + // then + } + func buildActivitiResponseMock() -> FeedResponseActivityView { return FeedResponseActivityView().mockResponse() }