Merge branch 'develop' into olkol/copyable_label

This commit is contained in:
Sharad Agarwal 2017-11-06 16:12:16 -08:00
Родитель c285e0e989 d6c3226b85
Коммит bf3c406120
28 изменённых файлов: 211 добавлений и 58 удалений

Просмотреть файл

@ -2,7 +2,7 @@ Pod::Spec.new do |s|
s.platform = :ios
s.ios.deployment_target = '9.0'
s.name = 'EmbeddedSocial'
s.version = '0.7.5'
s.version = '0.7.10'
s.summary = 'SDK for interacting with the Microsoft Embedded Social service from inside your iOS app.'
s.description = 'This is an SDK that works with the Microsoft Embedded Social service to provide social networking functionality inside your iOS application.'
s.homepage = 'https://github.com/Microsoft/EmbeddedSocial-iOS-SDK'
@ -54,5 +54,6 @@ Pod::Spec.new do |s|
s.dependency 'FBSDKCoreKit', '~> 4.24.0'
s.dependency 'FBSDKLoginKit', '~> 4.24.0'
s.dependency 'FBSDKShareKit', '~> 4.24.0'
s.dependency 'BMACollectionBatchUpdates', '~> 1.1'
end

Просмотреть файл

@ -883,6 +883,7 @@
88C6826E1F7A6E64004BD291 /* PaginatedListProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C6826D1F7A6E64004BD291 /* PaginatedListProcessor.swift */; };
88C682711F7A71EE004BD291 /* FollowRequestsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C682701F7A71EE004BD291 /* FollowRequestsAPI.swift */; };
88CEDD861FA76C110015B122 /* SettingsInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CEDD851FA76C110015B122 /* SettingsInteractorTests.swift */; };
88CF3A831FAB432500F607F8 /* MockSearchPeopleModuleOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CF3A821FAB432500F607F8 /* MockSearchPeopleModuleOutput.swift */; };
88D1230C1F73AA9F001523D1 /* OutgoingCommandsUploadStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D1230B1F73AA9F001523D1 /* OutgoingCommandsUploadStrategy.swift */; };
88D1230E1F73AB6F001523D1 /* FetchOutgoingCommandsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D1230D1F73AB6F001523D1 /* FetchOutgoingCommandsOperation.swift */; };
88D123131F73F566001523D1 /* OutgoingCommandsRelatedHandleUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D123121F73F566001523D1 /* OutgoingCommandsRelatedHandleUpdater.swift */; };
@ -2166,6 +2167,7 @@
88C6826D1F7A6E64004BD291 /* PaginatedListProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginatedListProcessor.swift; sourceTree = "<group>"; };
88C682701F7A71EE004BD291 /* FollowRequestsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FollowRequestsAPI.swift; sourceTree = "<group>"; };
88CEDD851FA76C110015B122 /* SettingsInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractorTests.swift; sourceTree = "<group>"; };
88CF3A821FAB432500F607F8 /* MockSearchPeopleModuleOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSearchPeopleModuleOutput.swift; sourceTree = "<group>"; };
88D1230B1F73AA9F001523D1 /* OutgoingCommandsUploadStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingCommandsUploadStrategy.swift; sourceTree = "<group>"; };
88D1230D1F73AB6F001523D1 /* FetchOutgoingCommandsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchOutgoingCommandsOperation.swift; sourceTree = "<group>"; };
88D123121F73F566001523D1 /* OutgoingCommandsRelatedHandleUpdater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingCommandsRelatedHandleUpdater.swift; sourceTree = "<group>"; };
@ -4778,6 +4780,7 @@
children = (
889119B21F4AE771005B3B32 /* MockSearchPeopleInteractor.swift */,
889119B41F4AE7F5005B3B32 /* MockSearchPeopleView.swift */,
88CF3A821FAB432500F607F8 /* MockSearchPeopleModuleOutput.swift */,
);
path = Mocks;
sourceTree = "<group>";
@ -8033,6 +8036,7 @@
9C30243C1F55AB2100675FE9 /* FeedModuleRouterMock.swift in Sources */,
88C682411F794D6B004BD291 /* MockTopicServicePredicateBuilder.swift in Sources */,
88F7A7861F2788DE005FEC5F /* LoginInteractorTests.swift in Sources */,
88CF3A831FAB432500F607F8 /* MockSearchPeopleModuleOutput.swift in Sources */,
639A94401F5D2FE800EB0253 /* MockCommentCellInteractor.swift in Sources */,
886826111F6A948700F54731 /* OutgoingCommandTests.swift in Sources */,
889119C31F4AFBB1005B3B32 /* MockSearchView.swift in Sources */,

Просмотреть файл

@ -5,9 +5,22 @@
import Foundation
class DateFormatterTool {
protocol DateFormatterProtocol {
func timeAgo(since: Date) -> String?
}
class DateFormatterTool: DateFormatterProtocol {
lazy var shortStyle: DateComponentsFormatter = {
static let shared: DateFormatterTool = DateFormatterTool()
func timeAgo(since then: Date) -> String? {
let now = Date()
let interval: TimeInterval = now.timeIntervalSince(then)
return DateFormatterTool.short.string(from: interval)
}
static let short: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
@ -19,21 +32,4 @@ class DateFormatterTool {
return formatter
}()
static func timeAgo(since: Date) -> String? {
return short.string(from: since, to: Date())
}
static var short:DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.includesApproximationPhrase = false
formatter.zeroFormattingBehavior = .dropAll
formatter.maximumUnitCount = 1
formatter.allowsFractionalUnits = false
return formatter
}
}

Просмотреть файл

@ -7,12 +7,19 @@ import UIKit
class OfflineView: UILabel {
private let statusBarHeight = UIApplication.shared.statusBarFrame.size.height
private let oldOSVersion = 10
private let labelHeight: CGFloat = 30
private let fontSize: CGFloat = 13
func show(in controller: UIViewController) {
if self.superview == nil {
self.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 30)
let offsetY = ProcessInfo().operatingSystemVersion.majorVersion > oldOSVersion ? (controller.navigationController?.navigationBar.frame.height ?? 0) + statusBarHeight : 0
self.frame = CGRect(x: 0, y: offsetY, width: UIScreen.main.bounds.size.width, height: labelHeight)
self.textAlignment = .center
self.text = L10n.Error.noInternetConnection
self.font = UIFont.systemFont(ofSize: 13)
self.font = UIFont.systemFont(ofSize: fontSize)
self.backgroundColor = UIColor(red: 34/255 , green: 139/255, blue: 34/255, alpha: 1)
self.textColor = .white
controller.view.addSubview(self)

Просмотреть файл

@ -103,16 +103,6 @@ class SocialService: BaseService, SocialServiceType {
outgoingActionsExecutor.execute(command: command, builder: builder, completion: completion)
}
private func processResponse(_ data: Object?, _ error: Error?, _ completion: @escaping (Result<Void>) -> Void) {
DispatchQueue.main.async {
if error == nil {
completion(.success())
} else {
self.errorHandler.handle(error: error, completion: completion)
}
}
}
func getMyFollowing(cursor: String?, limit: Int, completion: @escaping (Result<UsersListResponse>) -> Void) {
let builder = SocialAPI.myFollowingGetFollowingUsersWithRequestBuilder(
authorization: authorization,

Просмотреть файл

@ -43,7 +43,7 @@ struct PostViewModel {
cellType: String,
actionHandler: ActionHandler? = nil) {
let formatter = DateFormatterTool()
let formatter = DateFormatterTool.shared
self.isTrimmed = isTrimmed
topicHandle = post.topicHandle
userName = User.fullName(firstName: post.firstName, lastName: post.lastName)
@ -71,7 +71,7 @@ struct PostViewModel {
totalCommentsShort = "\(post.totalComments)"
if let createdTime = post.createdTime {
timeCreated = formatter.shortStyle.string(from: createdTime, to: Date()) ?? ""
timeCreated = formatter.timeAgo(since: createdTime) ?? ""
} else {
timeCreated = ""
}

Просмотреть файл

@ -166,7 +166,7 @@ enum ActivityError: Int, Error {
extension ActivityView {
func createdTimeAgo() -> String? {
guard let date = self.createdTime else { return nil }
return DateFormatterTool.timeAgo(since: date)
return DateFormatterTool.shared.timeAgo(since: date)
}
}

Просмотреть файл

@ -65,7 +65,7 @@ class CommentCell: UICollectionViewCell, CommentCellViewInput {
likesCountButton.setTitle(L10n.Post.likesCount(Int(comment.totalLikes)), for: .normal)
repliesCountButton.setTitle(L10n.Post.repliesCount(Int(comment.totalReplies)), for: .normal)
postedTimeLabel.text = comment.createdTime == nil ? "" : formatter.shortStyle.string(from: comment.createdTime!, to: Date())
postedTimeLabel.text = comment.createdTime == nil ? "" : formatter.timeAgo(since: comment.createdTime!)
if comment.user?.photo?.url == nil {
userPhoto.image = userImagePlaceholder

Просмотреть файл

@ -39,7 +39,7 @@ extension EditProfileViewController: EditProfileViewInput {
}
func showError(_ error: Error) {
showErrorAlert(error)
showErrorAlert(error, ignoreNoConnectionErrors: false)
}
func setSaveButtonEnabled(_ isEnabled: Bool) {

Просмотреть файл

@ -178,7 +178,7 @@ class FeedModulePresenter: FeedModuleInput, FeedModuleViewOutput, FeedModuleInte
fileprivate let limit: Int32 = Int32(Constants.Feed.pageSize)
var currentItems: [Post] = [Post]() {
didSet {
Logger.log("\(oldValue.count) -> \(currentItems.count)", event: .development)
//Logger.log("\(oldValue.count) -> \(currentItems.count)", event: .development)
}
}
fileprivate var fetchRequestsInProgress: Set<String> = Set()
@ -522,8 +522,7 @@ class FeedModulePresenter: FeedModuleInput, FeedModuleViewOutput, FeedModuleInte
newModel: [newSection],
sectionsPriorityOrder: nil,
eliminatesDuplicates: true) { (sections, updates) in
Logger.log(sections.first?.items, updates, event: .development)
self.view.performBatches(updates: updates, withSections: sections)
}

Просмотреть файл

@ -49,7 +49,7 @@ class ReplyCell: UICollectionViewCell, ReplyCellViewInput {
replyLabel.text = reply.text ?? ""
totalLikesButton.setTitle(L10n.Post.likesCount(Int(reply.totalLikes)), for: .normal)
postTimeLabel.text = reply.createdTime == nil ? "" : formatter.shortStyle.string(from: reply.createdTime!, to: Date())
postTimeLabel.text = reply.createdTime == nil ? "" : formatter.timeAgo(since: reply.createdTime!)
if reply.user?.photo?.url == nil {
userPhoto.image = UIImage(asset: AppConfiguration.shared.theme.assets.userPhotoPlaceholder)

Просмотреть файл

@ -74,6 +74,14 @@ extension SearchPresenter: SearchPeopleModuleOutput, SearchTopicsModuleOutput {
func didSelectHashtag(_ hashtag: Hashtag) {
view.search(hashtag: hashtag)
}
func didStartLoadingSearchTopicsQuery() {
view.setTopicsLayoutFlipEnabled(false)
}
func didLoadSearchTopicsQuery() {
view.setTopicsLayoutFlipEnabled(true)
}
}
extension SearchPresenter: SearchModuleInput {

Просмотреть файл

@ -138,6 +138,10 @@ extension SearchViewController: SearchViewInput {
func search(hashtag: Hashtag) {
searchSelectedText(hashtag)
}
func setTopicsLayoutFlipEnabled(_ isEnabled: Bool) {
feedLayoutButton.isEnabled = isEnabled
}
}
extension SearchViewController: UISearchBarDelegate {

Просмотреть файл

@ -16,4 +16,6 @@ protocol SearchViewInput: class {
func setLayoutAsset(_ asset: Asset)
func search(hashtag: Hashtag)
func setTopicsLayoutFlipEnabled(_ isEnabled: Bool)
}

Просмотреть файл

@ -60,6 +60,7 @@ extension SearchPeoplePresenter: UserListModuleOutput {
moduleOutput?.didFailToLoadSuggestedUsers(error)
} else if listView == usersListModule.listView {
moduleOutput?.didFailToLoadSearchQuery(error)
view.setIsEmpty(usersListModule.isListEmpty)
}
}

Просмотреть файл

@ -9,4 +9,8 @@ protocol SearchTopicsModuleOutput: class {
func didFailToLoadSearchQuery(_ error: Error)
func didSelectHashtag(_ hashtag: Hashtag)
func didStartLoadingSearchTopicsQuery()
func didLoadSearchTopicsQuery()
}

Просмотреть файл

@ -63,8 +63,13 @@ extension SearchTopicsPresenter: FeedModuleOutput {
return true
}
func didUpdateFeed() {
func didStartRefreshingData() {
moduleOutput?.didStartLoadingSearchTopicsQuery()
}
func didFinishRefreshingData(_ error: Error?) {
view.setIsEmpty(feedModule.isEmpty)
moduleOutput?.didLoadSearchTopicsQuery()
}
}

Просмотреть файл

@ -15,4 +15,6 @@ protocol UserListModuleInput: class {
func setListHeaderView(_ view: UIView?)
func removeUser(_ user: User)
var isListEmpty: Bool { get }
}

Просмотреть файл

@ -123,6 +123,10 @@ extension UserListPresenter: UserListModuleInput {
return view
}
var isListEmpty: Bool {
return !view.anyItemsShown
}
func setupInitialState() {
view.setupInitialState()
view.setNoDataText(noDataText)

Просмотреть файл

@ -12,11 +12,19 @@ protocol APIErrorHandler {
}
extension APIErrorHandler {
func handle<T>(error: ErrorResponse?, completion: @escaping (Result<T>) -> Void) {
if canHandle(error) {
handle(error)
} else {
completion(.failure(APIError(error: error)))
}
}
func handle<T>(error: Error?, completion: @escaping (Result<T>) -> Void) {
if canHandle(error) {
handle(error)
} else {
completion(.failure(APIError(error: error as? ErrorResponse)))
completion(.failure(error ?? APIError.unknown))
}
}
}

Просмотреть файл

@ -13,8 +13,7 @@ class DateFormatterTests: XCTestCase {
override func setUp() {
super.setUp()
sut = DateFormatterTool()
sut = DateFormatterTool.shared
}
func testThatFormattingIsCorrect() {
@ -24,11 +23,12 @@ class DateFormatterTests: XCTestCase {
var comps = DateComponents()
comps.calendar = cal
comps.day = -14
let to = Date()
let from = cal.date(byAdding: comps, to: to)
let now: Date = Date()
let then: Date = cal.date(byAdding: comps, to: now)!
// when
let result = sut.shortStyle.string(from: from!, to: to)
let result = sut.timeAgo(since: then)
// then
XCTAssert(result == "2w")

Просмотреть файл

@ -8,6 +8,8 @@
class MockUserListModuleInput: UserListModuleInput {
var listView = UIView()
var isListEmpty = false
//MARK: - setupInitialState
var setupInitialStateCalled = false
@ -45,4 +47,5 @@ class MockUserListModuleInput: UserListModuleInput {
removeUserCalled = true
removeUserReceivedUser = user
}
}

Просмотреть файл

@ -47,14 +47,24 @@ class MockSearchView: SearchViewInput {
setLayoutAssetReceivedAsset = asset
}
// MARK: - search
//MARK: - search
var searchHashtagCalled = false
var searchHashtagInputHashtag: Hashtag?
var searchHashtagReceivedHashtag: Hashtag?
func search(hashtag: Hashtag) {
searchHashtagCalled = true
searchHashtagInputHashtag = hashtag
searchHashtagReceivedHashtag = hashtag
}
//MARK: - setTopicsLayoutFlipEnabled
var setTopicsLayoutFlipEnabledCalled = false
var setTopicsLayoutFlipEnabledReceivedIsEnabled: Bool?
func setTopicsLayoutFlipEnabled(_ isEnabled: Bool) {
setTopicsLayoutFlipEnabledCalled = true
setTopicsLayoutFlipEnabledReceivedIsEnabled = isEnabled
}
}

Просмотреть файл

@ -153,7 +153,7 @@ class SearchPresenterTests: XCTestCase {
let hashtag = UUID().uuidString
sut.didSelectHashtag(hashtag)
XCTAssertTrue(view.searchHashtagCalled)
XCTAssertEqual(view.searchHashtagInputHashtag, hashtag)
XCTAssertEqual(view.searchHashtagReceivedHashtag, hashtag)
}
private func makePeopleTab() -> SearchTabInfo {
@ -180,7 +180,7 @@ class SearchPresenterTests: XCTestCase {
XCTAssertEqual(view.setupInitialStateReceivedTab, topicsTab)
XCTAssertTrue(view.searchHashtagCalled)
XCTAssertEqual(view.searchHashtagInputHashtag, "1")
XCTAssertEqual(view.searchHashtagReceivedHashtag, "1")
}
func testSearchHashtagAfterViewIsReadyAndPeopleTabSelected() {
@ -200,6 +200,18 @@ class SearchPresenterTests: XCTestCase {
XCTAssertEqual(view.switchTabsToFromReceivedArguments?.tab, topicsTab)
XCTAssertTrue(view.searchHashtagCalled)
XCTAssertEqual(view.searchHashtagInputHashtag, "1")
XCTAssertEqual(view.searchHashtagReceivedHashtag, "1")
}
func testStartLoadingTopicsQuery() {
sut.didStartLoadingSearchTopicsQuery()
XCTAssertTrue(view.setTopicsLayoutFlipEnabledCalled)
XCTAssertEqual(view.setTopicsLayoutFlipEnabledReceivedIsEnabled, false)
}
func testFinishLoadingTopicsQuery() {
sut.didLoadSearchTopicsQuery()
XCTAssertTrue(view.setTopicsLayoutFlipEnabledCalled)
XCTAssertEqual(view.setTopicsLayoutFlipEnabledReceivedIsEnabled, true)
}
}

Просмотреть файл

@ -0,0 +1,30 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//
@testable import EmbeddedSocial
class MockSearchPeopleModuleOutput: SearchPeopleModuleOutput {
//MARK: - didFailToLoadSuggestedUsers
var didFailToLoadSuggestedUsersCalled = false
var didFailToLoadSuggestedUsersReceivedError: Error?
func didFailToLoadSuggestedUsers(_ error: Error) {
didFailToLoadSuggestedUsersCalled = true
didFailToLoadSuggestedUsersReceivedError = error
}
//MARK: - didFailToLoadSearchQuery
var didFailToLoadSearchQueryCalled = false
var didFailToLoadSearchQueryReceivedError: Error?
func didFailToLoadSearchQuery(_ error: Error) {
didFailToLoadSearchQueryCalled = true
didFailToLoadSearchQueryReceivedError = error
}
}

Просмотреть файл

@ -12,6 +12,7 @@ class SearchPeoplePresenterTests: XCTestCase {
var usersListModule: MockUserListModuleInput!
var backgroundUsersListModule: MockUserListModuleInput!
var sut: SearchPeoplePresenter!
var moduleOutput: MockSearchPeopleModuleOutput!
override func setUp() {
super.setUp()
@ -19,12 +20,14 @@ class SearchPeoplePresenterTests: XCTestCase {
interactor = MockSearchPeopleInteractor()
usersListModule = MockUserListModuleInput()
backgroundUsersListModule = MockUserListModuleInput()
moduleOutput = MockSearchPeopleModuleOutput()
sut = SearchPeoplePresenter()
sut.view = view
sut.interactor = interactor
sut.usersListModule = usersListModule
sut.backgroundUsersListModule = backgroundUsersListModule
sut.moduleOutput = moduleOutput
}
override func tearDown() {
@ -34,6 +37,50 @@ class SearchPeoplePresenterTests: XCTestCase {
usersListModule = nil
backgroundUsersListModule = nil
sut = nil
moduleOutput = nil
}
func testSearchErrorHandlingWhenListIsEmpty() {
testSearchErrorHandling(listIsEmpty: true)
resetModuleOutput()
resetView()
testSearchErrorHandling(listIsEmpty: false)
}
func testSearchErrorHandling(listIsEmpty: Bool) {
usersListModule.isListEmpty = listIsEmpty
sut.didFailToLoadList(listView: usersListModule.listView, error: APIError.unknown)
XCTAssertTrue(moduleOutput.didFailToLoadSearchQueryCalled)
guard let e = moduleOutput.didFailToLoadSearchQueryReceivedError as? APIError, case .unknown = e else {
XCTFail()
return
}
XCTAssertTrue(view.setIsEmptyCalled)
XCTAssertEqual(view.setIsEmptyInputIsEmpty, listIsEmpty)
}
func resetView() {
view = MockSearchPeopleView()
sut.view = view
}
func resetModuleOutput() {
moduleOutput = MockSearchPeopleModuleOutput()
sut.moduleOutput = moduleOutput
}
func testBackgroundListErrorHandling() {
sut.didFailToLoadList(listView: backgroundUsersListModule.listView, error: APIError.unknown)
XCTAssertTrue(moduleOutput.didFailToLoadSuggestedUsersCalled)
guard let e = moduleOutput.didFailToLoadSuggestedUsersReceivedError as? APIError, case .unknown = e else {
XCTFail()
return
}
}
}

Просмотреть файл

@ -26,4 +26,20 @@ class MockSearchTopicsModuleOutput: SearchTopicsModuleOutput {
didSelectHashtagCalled = true
didSelectHashtagReceivedHashtag = hashtag
}
//MARK: - didStartLoadingSearchTopicsQuery
var didStartLoadingSearchTopicsQueryCalled = false
func didStartLoadingSearchTopicsQuery() {
didStartLoadingSearchTopicsQueryCalled = true
}
//MARK: - didLoadSearchTopicsQuery
var didLoadSearchTopicsQueryCalled = false
func didLoadSearchTopicsQuery() {
didLoadSearchTopicsQueryCalled = true
}
}

Просмотреть файл

@ -66,7 +66,7 @@ class SearchTopicsPresenterTests: XCTestCase {
XCTAssertEqual(view.setupInitialStateWithReceivedFeedViewController, feedViewController)
}
func testThatItSetsViewIsEmptyWhenFeedIsUpdated() {
func testThatItSetsViewIsEmptyWhenFeedFinishesRefreshing() {
testThatItSetsViewIsEmptyWhenFeedIsUpdated(feedIsEmpty: true, isViewExpectedToBeEmpty: true)
testThatItSetsViewIsEmptyWhenFeedIsUpdated(feedIsEmpty: false, isViewExpectedToBeEmpty: false)
@ -77,7 +77,7 @@ class SearchTopicsPresenterTests: XCTestCase {
feedModule.isEmpty = feedIsEmpty
// when
sut.didUpdateFeed()
sut.didFinishRefreshingData(APIError.unknown)
// then
XCTAssertTrue(view.setIsEmptyCalled)